Critical:
- migrations: change version 8 → 25 to avoid collision with
MigrationRunner.addObservationHierarchicalFields (uses version 8)
- SessionRoutes: remove duplicate imports that prevent compilation
Major:
- SessionRoutes: call applyTierRouting() before every generator spawn
(stale-recovery and crash-recovery paths were missing it)
- applyTierRouting: clear session.modelOverride at top before re-evaluating
to prevent stale tier from persisting across spawns
Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
Tier Routing:
- Inspect pending queue before starting generator
- Summarize messages → CLAUDE_MEM_TIER_SUMMARY_MODEL (e.g., Opus)
- All simple tools (Read, Glob, Grep, LS) → CLAUDE_MEM_TIER_SIMPLE_MODEL (Haiku)
- Mixed/complex → default model (no override)
- session.modelOverride in ActiveSession, used by SDKAgent.getModelId()
- peekPendingTypes() in PendingMessageStore for non-claiming inspection
- Configurable via CLAUDE_MEM_TIER_ROUTING_ENABLED (default: true)
Feedback Collection (schema only):
- New observation_feedback table via MigrationRunner (schema version 24)
- Tracks signal_type (semantic_inject_hit, search_accessed, etc.)
- Indexes on observation_id and signal_type
- Foundation for future Thompson Sampling optimization
Production data (24h tier routing test):
- 36 Haiku observations in 4 min, quality indistinguishable from Sonnet
- Estimated ~52% cost reduction on SDK Agent usage
- 835 → 6,695 feedback signals collected over 13 days
Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
Follow-up to PR #1568: fix stale doc comment that still said GET, and add
limit parameter validation (default 5, clamped to 1-20 range).
Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
* feat: semantic context injection via Chroma on every UserPromptSubmit
On each prompt, queries ChromaDB for the top-N most relevant past
observations and injects them as additionalContext. Replaces the
recency-based "last N observations" approach with relevance-based
semantic search.
Changes:
- session-init.ts: After session init, query /api/context/semantic
with user's prompt text. If results found, return as
hookSpecificOutput with hookEventName 'UserPromptSubmit'.
- SearchRoutes.ts: New GET /api/context/semantic endpoint that queries
SearchManager with format='json' and formats results as markdown.
- SettingsDefaultsManager.ts: New settings CLAUDE_MEM_SEMANTIC_INJECT
(default: true) and CLAUDE_MEM_SEMANTIC_INJECT_LIMIT (default: 5).
Key behaviors:
- Fires on every UserPromptSubmit (not just SessionStart)
- Minimum prompt length: 20 chars (skips "ok", "yes", etc.)
- Skips media-only prompts
- Graceful degradation: if worker/Chroma unavailable, no injection
- Survives /clear: re-injects on next prompt (not session-bound)
- Uses workerHttpRequest (v10.6.3 API, not raw fetch)
Production data (23 days, 3,400+ observations):
- Before: 8 most recent observations (often irrelevant to current topic)
- After: 5 most relevant observations (semantic match)
- Token cost: ~1800 → ~800-1200 per injection
Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
* fix: address CodeRabbit review on PR #1568
- session-init: don't skip semantic injection when contextInjected=true
(only skip agent re-init, semantic lookup must run every prompt)
- session-init: normalize SEMANTIC_INJECT toggle via String().toLowerCase()
- semantic endpoint: change from GET to POST to avoid URL-length limits
and prompt exposure in access logs. Handler accepts both body and query
for backwards compatibility.
Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
---------
Co-authored-by: Alessandro Costa <alessandro@claudio.dev>
Co-authored-by: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
* fix: resolve 3 upstream bugs in summarize, ChromaSync, and HealthMonitor
1. summarize.ts: Skip summary when transcript has no assistant message.
Prevents error loop where empty transcripts cause repeated failed
summarize attempts (~30 errors/day observed in production).
2. ChromaSync.ts: Fallback to chroma_update_documents when add fails
with "IDs already exist". Handles partial writes after MCP timeout
without waiting for next backfill cycle.
3. HealthMonitor.ts: Replace HTTP-based isPortInUse with atomic socket
bind on Unix. Eliminates TOCTOU race when two sessions start
simultaneously (HTTP check is non-atomic — both see "port free"
before either completes listen()). Updated tests accordingly.
All three bugs are pre-existing in v10.5.5. Confirmed via log analysis
of 543K lines over 17 days of production usage across two servers.
Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
* chore: add CONTRIB_NOTES.md to gitignore
Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
* fix: address CodeRabbit review on PR #1566
- HealthMonitor: add APPROVED OVERRIDE annotation for Win32 HTTP fallback
- ChromaSync: replace chroma_update_documents with delete+add for proper
upsert (update only modifies existing IDs, silently ignores missing ones)
Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
---------
Co-authored-by: Alessandro Costa <alessandro@claudio.dev>
Co-authored-by: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
Bidirectional sync of observations and session summaries between
machines via SSH/SCP. Exports to JSON, transfers, imports with
deduplication by (created_at, title).
Commands:
claude-mem-sync push <remote-host> # local → remote
claude-mem-sync pull <remote-host> # remote → local
claude-mem-sync sync <remote-host> # bidirectional
claude-mem-sync status <remote-host> # compare counts
Features:
- Deduplication prevents duplicates on repeated runs
- Configurable paths via CLAUDE_MEM_DB / CLAUDE_MEM_REMOTE_DB
- Automatic temp file cleanup
- Requires only Python 3 + SSH on both machines
Tested syncing 3,400+ observations between two physical servers.
After sync, a session on the remote server used the transferred
memory to deliver a real feature PR — proving productive
cross-machine workflows.
Co-authored-by: Alessandro Costa <alessandro@claudio.dev>
Co-authored-by: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
* feat: drain orphaned pending messages on session completion (SIGTERM)
When deleteSession() aborts the SDK agent via SIGTERM, pending messages
in the queue are never processed. Without drain, they remain in
'pending' status forever — no future generator picks them up because
the session is already completed.
Adds markAllSessionMessagesAbandoned() call after deleteSession() in
completeByDbId(). This reuses the existing PendingMessageStore method
already used by worker-service.ts terminateSession().
Production evidence: 15 orphaned summarize messages found across
completed sessions (ages 3h to 3 days) before this fix. After fix:
0 orphaned messages over 23 days of operation.
Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
* fix: document best-effort drain limitation per CodeRabbit review #1567
Add comment noting the rare race condition when generators outlive the
30s SIGTERM timeout. Practical risk is negligible (0 orphans over 23 days).
Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
---------
Co-authored-by: Alessandro Costa <alessandro@claudio.dev>
Co-authored-by: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
readGeminiSettings() throws on corrupt JSON since ae6915b, but
checkGeminiCliHooksStatus() called it without catching — violating
its "returns 0 always" contract.
Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
- cpSync now does rmSync before copy to avoid stale file merges
- setupIDEs() returns failed IDE list; install reports partial success
- runSmartInstall() returns boolean status instead of void
- Worker port in next-steps URL reads CLAUDE_MEM_WORKER_PORT env var
- Goose YAML regex stops at column-0 keys (prevents eating sibling sections)
- AGENTS.md uninstall removes header-only stub files
- findBunPath() validated before use in WindsurfHooksInstaller
- Cursor marked unsupported in ide-detection until installer is wired
Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
- Add shebang banner to NPX CLI esbuild config so npx claude-mem works
- Remove manual backslash pre-escaping in WindsurfHooksInstaller (JSON.stringify handles it)
- Scope cache deletion to claude-mem only, not entire vendor namespace
- Use getWorkerPort() in OpenCodeInstaller instead of hard-coded 37777
- Throw on corrupt JSON in readJsonSafe/readGeminiSettings/Windsurf to prevent data loss
- Fix Cursor install stub to warn instead of silently succeeding
- Fix Gemini uninstall to remove individual hooks within groups, not whole groups
- Update tests for new corrupt-file-throws behavior
Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
SessionEnd has a 1.5s hardcoded cap from Claude Code (CLAUDE_CODE_SESSIONEND_HOOKS_TIMEOUT_MS),
making it unsuitable for waiting on async work. Previously, the Stop hook would fire-and-forget
the summarize request, then SessionEnd would immediately call deleteSession — aborting the SDK
agent mid-summary.
Now the Stop hook (120s timeout, no cap) owns the full lifecycle:
1. Queue summarize request
2. Poll new GET /api/sessions/status endpoint until queue drains
3. Call /api/sessions/complete after summary finishes
SessionEnd is now a true fire-and-forget fallback (process.exit(0) immediately).
Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
Resolve merge conflicts in adapter index, gemini-cli adapter, and rebuilt CJS artifacts.
Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
Fixes CursorHooksInstaller ESM compatibility, updates install command
with improved path resolution, and refreshes built plugin artifacts.
Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
System reminders (CLAUDE.md contents, deferred tool lists) were being
stored in memory observations. Add system-reminder to the tag stripping
pipeline alongside <private> and <system_instruction>, and extract the
duplicated regex into a shared SYSTEM_REMINDER_REGEX constant.
Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
Instead of using shell:true with spawn(), use cmd.exe as the command
with /c flag to properly execute bun.cmd on Windows.
Without this, spawn() with shell:true fails because cmd.exe doesn't
know how to handle the bun shell script directly.
Fixes: Stop hook "Failed to start Bun: spawn bun ENOENT"
Windows npm installs both bun (shell script) and bun.cmd (batch file).
When spawning bun, cmd.exe cannot execute the shell script directly.
This change makes findBun() return the full path to bun.cmd on Windows.
Fixes: Stop hook "spawn bun ENOENT" on Windows
Windows npm installs bun as a shell script (C:\Users\...\AppData\Roaming\npm\bun),
not a native executable. Without shell:true, spawn() fails with ENOENT
when trying to execute it.
Fixes Stop hook failure: "Failed to start Bun: spawn bun ENOENT"
The fallback path for CLAUDE_PLUGIN_ROOT was pointing to the old
marketplaces install location which no longer exists. Hooks now first
try to find the latest versioned cache directory
(~/.claude/plugins/cache/thedotmack/claude-mem/<version>/) using ls -dt,
with the marketplaces path kept as a final fallback.
This mirrors the self-resolution pattern already used in bun-runner.js
(resolve(__bun_runner_dirname, '..')) but at the shell level, so node
can find bun-runner.js in the first place.
Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
When an observation response accidentally contains a <summary> tag with
plain text (no <request>/<investigated>/etc. sub-tags), parseSummary was
creating empty SESSION SUMMARY records with all fields as empty strings.
Add an all-null guard AFTER field extraction: if none of the 5 sub-tags
matched, the <summary> match is a false positive and we return null.
This is distinct from the commented-out validation above (which rejected
summaries with SOME missing fields). We only reject when ALL are absent —
real partial summaries are still saved per the maintainer's explicit note.
Closes#1360
Co-Authored-By: Claude <noreply@anthropic.com>
JSON.parse('/path/to/file') throws SyntaxError, crashing the viewer and
any code reading observations with legacy bare-path data in those columns.
- Add parseFileList() helper in observations/files.ts — tries JSON.parse,
falls back to wrapping bare strings in an array
- Replace unsafe JSON.parse calls in files.ts, SessionStore.ts, ChromaSync.ts
- Add 9 unit tests covering null, empty, valid JSON, bare paths, invalid JSON
Closes#1359
Co-Authored-By: Claude <noreply@anthropic.com>
completeByDbId only cleaned up in-memory state, leaving sdk_sessions rows
with status='active' and completed_at=NULL indefinitely. Ghost sessions
accumulated and exhausted the agent pool, causing 60s timeout errors.
- Add SessionStore.markSessionCompleted() to set status/completed_at/completed_at_epoch
- Call it at the start of completeByDbId before in-memory cleanup
- Inject SessionStore into SessionCompletionHandler via constructor
- Add 4 tests covering status, timestamps, isolation, and non-existent IDs
Closes#1532
Co-Authored-By: Claude <noreply@anthropic.com>
The gh issue comment command was interpolating the LLM response via
${{ steps.inference.outputs.response }} directly in the shell, allowing
single-quote escaping if the response contained untrusted content.
RESPONSE was already declared as an env var but unused — now using it.
Closes#1285
Co-Authored-By: Claude <noreply@anthropic.com>
CLAUDE_MEM_MODEL defaulted to the deprecated claude-sonnet-4-5 across source,
installer, tests, and documentation. Updated all references to claude-sonnet-4-6.
Closes#1390
Co-Authored-By: Claude <noreply@anthropic.com>
When CLAUDE_MEM_FOLDER_USE_LOCAL_MD is set to 'true' in settings,
claude-mem writes auto-generated context to CLAUDE.local.md instead
of CLAUDE.md. This separates personal machine-generated context from
shared project instructions, aligning with Claude Code's native
CLAUDE.local.md convention where:
- CLAUDE.md = team-shared project instructions (checked into git)
- CLAUDE.local.md = personal/local context (gitignored)
Changes:
- Add CLAUDE_MEM_FOLDER_USE_LOCAL_MD setting (default: false)
- Add getTargetFilename() helper to resolve target based on settings
- Update writeClaudeMdToFolder() to accept optional target filename
- Update active-file detection to skip folders with either CLAUDE.md
or CLAUDE.local.md being actively read/modified (issue #859 compat)
- Add 8 new tests covering filename selection, write behavior,
content preservation, atomic writes, and active-file detection
Closes#632
When using the OpenClaw integration, a single user message would produce
3 prompt records because session_start, message_received, after_compaction,
and before_agent_start each independently called /api/sessions/init with
different session keys.
Changes:
- Centralize /api/sessions/init to before_agent_start only
- Add canonical session key unification (sessionKey, conversationId,
channelId mapped to a single contentSessionId)
- Add 2-second dedup guard for edge cases
- Fix cwd: "" in tool_result_persist (use workspaceDir fallback chain,
skip + warn if unavailable)
- Add delayed session completion (configurable, default 5s) to avoid
race with in-flight observations
- Clean up all tracking Maps on session_end and gateway_start
Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
- Update POST_SPAWN_WAIT test assertion from 5000 to 15000 to match
the constant change in hook-constants.ts
- Remove redundant readPidFile() from aggressiveStartupCleanup() —
start() writes the new PID before this runs, so it always returns
process.pid (already protected)
- Add waitForReadiness() to the reused-worker path in
ensureWorkerStarted() to prevent concurrent hooks from racing
past a cold-starting worker's initialization guard
Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
Three independent fixes for worker daemon instability:
1. Remove version mismatch auto-restart from ensureWorkerStarted() (#1435).
The marketplace bundle ships with __DEFAULT_PACKAGE_VERSION__ unbaked,
causing BUILT_IN_VERSION to fall back to "development". This creates a
100% reproducible mismatch on every hook call, killing a healthy worker
and often failing to restart. Same pattern across #566, #665, #667,
#669, #689, #1124, #1145 (8+ releases).
2. Add process.ppid and PID-file PID to aggressiveStartupCleanup()
exclusions (#1426). Without this, a newly spawned daemon SIGKILLs
the hook process that spawned it and any already-running worker
the PID file points to.
3. Increase POST_SPAWN_WAIT from 5s to 15s (#1423). The 5s timeout was
sized for Linux (<1s startup) but macOS ARM64 cold starts take 6-8s
with Chroma enabled.
Fixes#1478
When a terminal reports cwd as '~' or '~/subpath' instead of the full
path, getProjectName() fell through to the 'unknown-project' fallback
because path.basename('~') returns '~' as-is.
Added expandTilde() helper that resolves leading ~ to os.homedir(),
called in both getProjectName() and getProjectContext() before path
operations and worktree detection.