* chore: gitignore runtime state files
.claude/scheduled_tasks.lock is a PID+sessionId lock written by Claude
Code's cron scheduler every session. It got accidentally checked in during
the v12.0.0 bump and has been churning phantom diffs in every PR since.
Untrack it and ignore.
plugin/.cli-installed is a timestamp marker the claude-mem installer drops
to record when the plugin was installed. Never belonged in version control.
Ignore it.
Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
* chore: add trailing newline to .gitignore
---------
Co-authored-by: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
parseSummary runs on every agent response, not just summary turns. When the
turn is a normal observation, the LLM correctly emits <observation> and no
<summary> — but the fallthrough branch from #1345 treated this as prompt
misbehavior and logged "prompt conditioning may need strengthening" every
time. That assumption stopped holding after #1633 refactored the caller to
always invoke parseSummary with a coerceFromObservation flag.
Gate the whole observation-on-summary path on coerceFromObservation. On a
real summary turn, coercion still runs and logs the legitimate "coercion
failed" warning when the response has no usable content. On an observation
turn, parseSummary returns null silently, which is the correct behavior.
Co-authored-by: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
* feat: disable subagent summaries and label subagent observations
Detect Claude Code subagent hook context via `agent_id`/`agent_type` on
stdin, short-circuit the Stop-hook summary path when present, and thread
the subagent identity end-to-end onto observation rows (new `agent_type`
and `agent_id` columns, migration 010 at version 27). Main-session rows
remain NULL; content-hash dedup is unchanged.
Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
* fix: address PR #2073 review feedback
- Narrow summarize subagent guard to agentId only so --agent-started
main sessions still own their summary (agentType alone is main-session).
- Remove now-dead agentId/agentType spreads from the summarize POST body.
- Always overwrite pendingAgentId/pendingAgentType in SDK/Gemini/OpenRouter
agents (clears stale subagent identity on main-session messages after
a subagent message in the same batch).
- Add idx_observations_agent_id index in migration 010 + the mirror
migration in SessionStore + the runner.
- Replace console.log in migration010 with logger.debug.
- Update summarize test: agentType alone no longer short-circuits.
Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
* fix: address CodeRabbit + claude-review iteration 4 feedback
- SessionRoutes.handleSummarizeByClaudeId: narrow worker-side guard to
agentId only (matches hook-side). agentType alone = --agent main
session, which still owns its summary.
- ResponseProcessor: wrap storeObservations in try/finally so
pendingAgentId/Type clear even if storage throws. Prevents stale
subagent identity from leaking into the next batch on error.
- SessionStore.importObservation + bulk.importObservation: persist
agent_type/agent_id so backup/import round-trips preserve subagent
attribution.
Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
* polish: claude-review iteration 5 cleanup
- Use ?? not || for nullable subagent fields in PendingMessageStore
(prevents treating empty string as null).
- Simplify observation.ts body spread — include fields unconditionally;
JSON.stringify drops undefined anyway.
- Narrow any[] to Array<{ name: string }> in migration010 column checks.
- Add trailing newline to migrations.ts.
- Document in observations/store.ts why the dedup hash intentionally
excludes agent fields.
Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
* polish: claude-review iteration 7 feedback
- claude-code adapter: add 128-char safety cap on agent_id/agent_type
so a malformed Claude Code payload cannot balloon DB rows. Empty
strings now also treated as absent.
- migration010: state-aware debug log lists only columns actually
added; idempotent re-runs log "already present; ensured indexes".
- Add 3 adapter tests covering the length cap boundary and empty-string
rejection.
Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
* perf: skip subagent summary before worker bootstrap
Move the agentId short-circuit above ensureWorkerRunning() so a Stop
hook fired inside a subagent does not trigger worker startup just to
return early. Addresses CodeRabbit nit on summarize.ts:36-47.
Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
---------
Co-authored-by: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
* Initial plan
* fix: break infinite summary-retry loop (#1633)
Three-part fix:
1. Parser coercion: When LLM returns <observation> tags instead of <summary>,
coerce observation content into summary fields (root cause fix)
2. Stronger summary prompt: Add clearer tag requirements with warnings
3. Circuit breaker: Track consecutive summary failures per session,
skip further attempts after 3 failures to prevent unbounded prompt growth
Agent-Logs-Url: https://github.com/thedotmack/claude-mem/sessions/e345e8ec-bc97-4eaa-94bd-6e951fda8f77
Co-authored-by: thedotmack <683968+thedotmack@users.noreply.github.com>
* refactor: extract shared constants for summary mode marker and failure threshold
Addresses code review feedback: SUMMARY_MODE_MARKER and
MAX_CONSECUTIVE_SUMMARY_FAILURES are now defined once in sdk/prompts.ts
and imported by ResponseProcessor and SessionManager.
Agent-Logs-Url: https://github.com/thedotmack/claude-mem/sessions/e345e8ec-bc97-4eaa-94bd-6e951fda8f77
Co-authored-by: thedotmack <683968+thedotmack@users.noreply.github.com>
* fix: guard summary failure counter on summaryExpected (Greptile P1)
The circuit breaker counter previously incremented on any response
containing <observation> or <summary> tags — which matches virtually
every normal observation response. After 3 observations the breaker
would open and permanently block summarization, reproducing the
data-loss scenario #1633 was meant to prevent.
Gate the increment block on summaryExpected (already computed for
parseSummary coercion) so the counter only tracks actual summary
attempts.
Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
* test: cover circuit-breaker + apply review polish
- Use findLast / at(-1) for last-user-message lookup instead of
filter + index (O(1) common case).
- Drop redundant `|| 0` fallback — field is required and initialized.
- Add comment noting counter is ephemeral by design.
- Add ResponseProcessor tests covering:
* counter NOT incrementing on normal observation responses
(regression guard for the Greptile P1)
* counter incrementing when a summary was expected but missing
* counter resetting to 0 on successful summary storage
Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
* fix: iterate all observation blocks; don't count skip_summary as failure
Addresses CodeRabbit review on #2072:
- coerceObservationToSummary now iterates all <observation> blocks
with a global regex and returns the first block that has title,
narrative, or facts. Previously, an empty leading observation
would short-circuit and discard populated follow-ups.
- Circuit-breaker counter now treats explicit <skip_summary/> as
neutral — neither a failure nor a success — so a run that happens
to end on a skip doesn't punish the session or mask a prior bad
streak. Real failures (no summary, no skip) still increment.
- Tests added for both cases.
Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
* test: reference SUMMARY_MODE_MARKER constant instead of hardcoded string
Addresses CodeRabbit nitpick: tests should pull the marker from the
canonical source so they don't silently drift when the constant is
renamed or edited.
Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
* fix: also coerce observations when <summary> has empty sub-tags
When the LLM wraps an empty <summary></summary> around real observation
content, the #1360 empty-subtag guard rejects the summary and returns
null — which would lose the observation content and resurrect the
#1633 retry loop. Fall back to coerceObservationToSummary in that
branch too, mirroring the unmatched-<summary> path.
Adds a test covering the empty-summary-wraps-observation case and
a guard test for empty summary with no observation content.
Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
---------
Co-authored-by: copilot-swe-agent[bot] <198982749+Copilot@users.noreply.github.com>
Co-authored-by: thedotmack <683968+thedotmack@users.noreply.github.com>
Co-authored-by: Alex Newman <thedotmack@gmail.com>
Co-authored-by: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
Conductor workspace setup is no longer needed - plugins handle hook
registration directly via plugin/hooks/hooks.json. The shim was copying
a stale settings.local.json into every worktree, registering dead hook
paths (save-hook.js, new-hook.js, summary-hook.js) that no longer exist.
Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
Script now reads existing CHANGELOG.md, skips releases already documented,
only fetches bodies for new releases, and prepends them. Pass --full to
force complete regeneration.
Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
Addresses unresolved CodeRabbit finding on WorktreeAdoption.ts:296.
Previously, Chroma patch failures stranded rows permanently: adoptedSqliteIds
was built only from rows where merged_into_project IS NULL, so once SQL
committed, reruns couldn't rediscover them for retry.
The Chroma id set is now built from ALL observations whose project matches a
merged worktree — including rows already stamped to this parent. Combined
with the idempotent updateMergedIntoProject, transient Chroma failures
self-heal on the next adoption pass.
SQL writes remain idempotent (UPDATE still guards on merged_into_project IS
NULL), so adoptedObservations / adoptedSummaries continue to count only
newly-adopted rows. chromaUpdates now counts total Chroma writes per pass
(may exceed adoptedObservations when retrying).
Addresses six CodeRabbit/Greptile findings on PR #2052:
- Schema guard in adoptMergedWorktrees probes for merged_into_project
columns before preparing statements; returns early when absent so first
boot after upgrade (pre-migration) doesn't silently fail.
- Startup adoption now iterates distinct cwds from pending_messages and
dedupes via resolveMainRepoPath — the worker daemon runs with
cwd=plugin scripts dir, so process.cwd() fallback was a no-op.
- ObservationCompiler single-project queries (queryObservations /
querySummaries) OR merged_into_project into WHERE so injected context
surfaces adopted worktree rows, matching the Multi variants.
- SessionStore constructor now calls ensureMergedIntoProjectColumns so
bundled artifacts (context-generator.cjs) that embed SessionStore get
the merged_into_project column on DBs that only went through the
bundled migration chain.
- OBSERVER_SESSIONS_PROJECT constant is now derived from
basename(OBSERVER_SESSIONS_DIR) and used across PaginationHelper,
SessionStore, and timeline queries instead of hardcoded strings.
- Corrected misleading Chroma retry docstring in WorktreeAdoption to
match actual behavior (no auto-retry once SQL commits).
Observer sessions (internal SDK-driven worker queries) run under a
synthetic project name 'observer-sessions' to keep them out of
claude --resume. They were still surfacing in the viewer project
picker and unfiltered observation/summary/prompt feeds.
Filter them out at every UI-facing query:
- SessionStore.getAllProjects and getProjectCatalog
- timeline/queries.ts getAllProjects
- PaginationHelper observations/summaries/prompts when no project is selected
When a caller explicitly requests project='observer-sessions',
results are still returned (not a hard ban, just hidden by default).
Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
- Document --branch override in npx-cli help text
- Guard ContextBuilder against empty projects[] override; fall back to cwd-derived primary
- Ensure merged_into_project indexes are created even if ALTER ran in a prior partial migration
- Reject adopt --branch/--cwd flags with missing or flag-like values
- Use defined --color-border-primary token for merged badge border
Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
- Update allProjects test expectation to match [parent, composite] (matches JSDoc + callers in ContextBuilder/context handlers).
- Replace string-matched __DRY_RUN_ROLLBACK__ sentinel with dedicated DryRunRollback class to avoid swallowing unrelated errors.
- Add 5000ms timeout to spawnSync git calls in WorktreeAdoption and ProcessManager so worker startup can't hang on a stuck git process.
- Drop unreachable break after process.exit(0) in adopt case.
Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
Regenerated worker-service.cjs, context-generator.cjs, viewer.html, and
viewer-bundle.js to reflect all six implementation phases of the merged-
worktree adoption feature.
Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
ObservationCard renders a secondary "merged → <parent>" chip when
merged_into_project is set, next to the existing project label.
Both are meaningful: project is origin provenance, merged_into_project
is the current home.
Extends PaginationHelper's observations and summaries queries with
OR merged_into_project = ? so the single-project viewer fetch pulls
in adopted rows — the plan's Phase 3 covered multi-project context
injection; this is the single-project UI read path.
Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
Adds a manual escape hatch for the worktree adoption engine. Covers
squash-merges where git branch --merged HEAD returns nothing, and
lets users re-run adoption on demand.
Wired through worker-service.cjs (same pattern as generate/clean)
so the command runs under Bun with bun:sqlite, keeping npx-cli/
pure Node. --cwd flag passes the user's working directory through
the spawn so the engine resolves the correct parent repo.
Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
Invokes adoptMergedWorktrees() right after runOneTimeCwdRemap() and
before dbManager.initialize(), wrapped in try/catch so adoption
failures never block startup. Idempotent, so running every startup
is cheap — the SQL UPDATE only touches rows where merged_into_project
IS NULL.
Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
ObservationCompiler.queryObservationsMulti and querySummariesMulti
WHERE clause extended with OR merged_into_project IN (...), so a
parent-project read pulls in rows originally written under any
child worktree's composite name once merged.
SearchManager wraps the Chroma project filter in \$or so semantic
search behaves identically. ChromaSync baseMetadata now carries
merged_into_project on new embeddings; existing rows are patched
retroactively by the adoption engine.
Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
Detects merged worktrees via git (worktree list --porcelain +
branch --merged HEAD), then stamps merged_into_project on SQLite
observations/summaries and propagates the same metadata to Chroma
in lockstep. `project` stays immutable; adoption is a virtual
pointer. Idempotent via IS NULL guard on UPDATE and by idempotent
Chroma metadata writes. SQL is source of truth — Chroma failures
are logged but don't roll back SQL.
Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
Nullable pointer on observations and session_summaries that lets a
worktree's rows surface under the parent project's observation list
without data movement. Self-idempotent via PRAGMA table_info guard;
does not bump schema_versions.
Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
Worktrees are branches off main; the parent holds the architecture,
decisions, and long-tail history the worktree inherits. Scoping reads
to the worktree alone meant every new worktree started cold on any
question that required prior context.
Expand `allProjects` in a worktree to `[parent, composite]` so reads
pull both. Writes still go through `.primary` (the composite), so
sibling worktrees don't leak into each other.
Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
Ports scripts/cwd-remap.ts into ProcessManager.runOneTimeCwdRemap() and
invokes it in initializeBackground() alongside the existing chroma
migration. Uses pending_messages.cwd as the source of truth to rewrite
pre-worktree bare project names into the parent/worktree composite
format so search and context are consistent.
- Backs up the DB to .bak-cwd-remap-<ts> before any writes.
- Idempotent: marker file .cwd-remap-applied-v1 short-circuits reruns.
- No-ops on fresh installs (no DB, or no pending_messages table).
- On failure, logs and skips the marker so the next restart retries.
Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
Leftover artifacts from an abandoned context-injection feature. The
project-level CLAUDE.md stays; the directory-level ones were generated
timeline scaffolding that never panned out.
Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
Three sites didn't account for parent/worktree composite naming:
- PaginationHelper.stripProjectPath: marker used full composite, breaking
path sanitization for worktrees checked out outside a parent/leaf layout.
Now extracts the leaf segment.
- observations/store.ts: fallback imported getCurrentProjectName from
shared/paths.ts (a duplicate impl without worktree detection). Switched
to getProjectContext().primary so writes key into the same project as
reads.
- SearchManager.getRecentContext: fallback used basename(cwd) and lost
the parent prefix, making the MCP tool find nothing in worktrees.
Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
- Handle bare repo common-dir (strip trailing .git) instead of an
identical-branch ternary
- Surface unexpected git stderr while keeping "not a git repository"
silent
- Explicitly close the sqlite handle in both dry-run and apply paths
so WAL checkpoints complete
Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
When a caller (e.g. worker context-inject route) passes a `projects`
array without a matching cwd, the cwd-derived `context.primary` drifted
from the projects being queried — producing an empty-state header for
one project while querying another. Use the last entry of `projects` so
header and query target stay in sync.
The old worktree-remap.ts tried to reconstruct per-session cwd by regex-
matching absolute paths that incidentally leaked into observation free-text
(files_read, source_input_summary, metadata, user_prompt). That source is
derived and lossy: it only hit 1/3498 plain-project sessions in practice.
pending_messages.cwd is the structured, authoritative cwd captured from
every hook payload — 7,935 of 8,473 rows are populated. cwd-remap.ts uses
that column as the source of truth:
1. Pull every distinct cwd from pending_messages.cwd
2. For each cwd, classify with git:
- rev-parse --absolute-git-dir vs --git-common-dir → main vs worktree
- rev-parse --show-toplevel for the correct leaf (handles cwds that
are subdirs of the worktree root)
Parent project name = basename(dirname(common-dir)); composite is
parent/worktree for worktrees, basename(toplevel) for main repos.
3. For each session, take the EARLIEST pending_messages.cwd (not the
dominant one — claude-mem's own hooks run from nested .context/
claude-mem/ directories and would otherwise poison the count).
4. Apply UPDATEs in a single transaction across sdk_sessions,
observations, and session_summaries. Auto-backs-up the DB first.
Result on a real DB: 41 sessions remapped (vs 1 previously),
1,694 observations and 3,091 session_summaries updated to match.
43 cwds skipped (deleted worktrees / non-repos) are left untouched —
no inference when the data isn't there.
Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com>
Revert of #1820 behavior. Each worktree now gets its own bucket:
- In a worktree, primary = `parent/worktree` (e.g. `claude-mem/dar-es-salaam`)
- In a main repo, primary = basename (unchanged)
- allProjects is always `[primary]` — strict isolation at query time
Includes a one-off maintenance script (scripts/worktree-remap.ts) that
retroactively reattributes past sessions to their worktree using path
signals in observations and user prompts. Two-rule inference keeps the
remap high-confidence:
1. The worktree basename in the path matches the session's current
plain project name (pre-#1820 era; trusted).
2. Or all worktree path signals converge on a single (parent, worktree)
across the session.
Ambiguous sessions are skipped.
Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com>
Observations were 100% failing on Claude Code 2.1.109+ because the Agent
SDK emits ["--setting-sources", ""] when settingSources defaults to [].
The existing Bun-workaround filter stripped the empty string but left
the orphan --setting-sources flag, which then consumed --permission-mode
as its value, crashing the subprocess with:
Error processing --setting-sources:
Invalid setting source: --permission-mode.
Make the filter pair-aware: when an empty arg follows a --flag, drop
both so the SDK default (no setting sources) is preserved by omission.
Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
A prior Claude instance snuck in a `$CMEM` token branding header
during a context compression refactor (7e072106). Reverts back to
the original descriptive format: `# [project] recent context, datetime`
Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
The synthetic summary salvage feature created fake summaries from observation
data when the AI returned <observation> instead of <summary> tags. This was
overengineered — missing a summary is preferable to fabricating one from
observation fields that don't map cleanly to summary semantics.
Co-authored-by: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
* fix: reap stuck generators in reapStaleSessions (fixes#1652)
Sessions whose SDK subprocess hung would stay in the active sessions
map forever because `reapStaleSessions()` unconditionally skipped any
session with a non-null `generatorPromise`. The generator was blocked
on `for await (const msg of queryResult)` inside SDKAgent and could
never unblock itself — the idle-timeout only fires when the generator
is in `waitForMessage()`, and the orphan reaper skips processes whose
session is still in the map.
Add `MAX_GENERATOR_IDLE_MS` (5 min). When `reapStaleSessions()` sees
a session whose `generatorPromise` is set but `lastGeneratorActivity`
has not advanced in over 5 minutes, it now:
1. SIGKILLs the tracked subprocess to unblock the stuck `for await`
2. Calls `session.abortController.abort()` so the generator loop exits
3. Calls `deleteSession()` which waits up to 30 s for the generator to
finish, then cleans up supervisor-tracked children
Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
* fix: freeze time in stale-generator test and import constants from production source
- Export MAX_GENERATOR_IDLE_MS, MAX_SESSION_IDLE_MS, StaleGeneratorCandidate,
StaleGeneratorProcess, and detectStaleGenerator from SessionManager.ts so
tests no longer duplicate production constants or detection logic.
- Use setSystemTime() from bun:test to freeze Date.now() in the
"exactly at threshold" test, eliminating the flaky double-Date.now() race.
Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
---------
Co-authored-by: Claude Sonnet 4.6 <noreply@anthropic.com>
* fix: add circuit breaker to OpenClaw worker client (#1636)
When the claude-mem worker is unreachable, every plugin event (before_agent_start,
before_prompt_build, tool_result_persist, agent_end) triggered a new fetch that
failed and logged a warning, causing CPU-spinning and continuous log spam.
Add a CLOSED/OPEN/HALF_OPEN circuit breaker: after 3 consecutive network errors
the circuit opens, silently drops all worker calls for 30 s, then sends one probe.
Individual failures are only logged while the circuit is still CLOSED; once open
it logs once ("disabling requests for 30s") and goes quiet until recovery.
Generated by Claude Code
Vibe coded by Ousama Ben Younes
Co-Authored-By: Claude <noreply@anthropic.com>
* fix: limit HALF_OPEN to single probe and move circuitOnSuccess after response.ok check
- Add _halfOpenProbeInFlight flag so only one probe is allowed in HALF_OPEN state;
concurrent callers are silently dropped until the probe completes (success or failure)
- Move circuitOnSuccess() to after the response.ok check in workerPost, workerPostFireAndForget,
and workerGetText so non-2xx HTTP responses no longer close the circuit
- Clear _halfOpenProbeInFlight in both circuitOnSuccess and circuitOnFailure, and in circuitReset
- Add regression test covering HALF_OPEN one-probe behavior: non-2xx keeps circuit open,
2xx closes it
* chore: trigger CodeRabbit re-review
---------
Co-authored-by: Claude <noreply@anthropic.com>