214 Commits

Author SHA1 Message Date
Alex Newman
29af028403 fix(providers): remove client-side context truncation ("second system")
The OpenAICompatibleProvider.truncateHistory() sliding-window dropped
conversation messages based on a hardcoded 20-message cap and a 100k
"safety" token limit — a client-side context manager layered on top of
the provider's own context window. In practice it fired on message count
alone (dropping messages at ~12k tokens, nowhere near the token limit)
and silently corrupted conversation history, mislabeled as "runaway cost"
prevention.

Rip it out entirely. The full history is now sent to the provider, which
owns its own context window.

Removed:
- truncateHistory() + requireNonEmptyToTruncate from OpenAICompatibleProvider
- truncateHistoryForOpenRouter / truncateHistoryForGemini wrappers + constants
- CLAUDE_MEM_{GEMINI,OPENROUTER}_MAX_CONTEXT_MESSAGES / _MAX_TOKENS settings,
  their defaults, and SettingsRoutes validation
- truncation-specific tests; docs + openclaw installer references

Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
2026-06-30 20:52:54 -07:00
Alex Newman
5def052993 docs: add Hosted Server (Beta) page — MCP recall, paid-readiness, data deletion
Documents the cloud server's current state across the three merged features
(#3070/#3078/#3087): remote authenticated /v1/mcp recall, opt-in rate
limiting/quotas/usage metering, and audited data deletion. Includes the
explicit caveat that the UX/devex flow (dashboard, first-key bootstrap,
onboarding, billing UI) is still being built.

Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
2026-06-29 19:46:58 -07:00
Alex Newman
482b4f0a15 fix(server): 404 on cross-team/nonexistent project purge
ensureProjectAllowed only checks a key's optional project scope, so a
team-scoped key could call DELETE /v1/projects/:projectId/memory with any
projectId and get 200 { purged: true } with zero counts — misreporting an
unauthorized or nonexistent purge as success. Verify the project belongs to
the team (getByIdForTeam) before purging and 404 otherwise. The underlying
purge was already team-scoped, so no cross-team data was ever deleted; this
fixes the misleading success response. Addresses greptile P1 on PR #3089.

Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
2026-06-29 17:58:38 -07:00
Alex Newman
441c6377c9 Merge PR #3087: data deletion — forget a memory or purge a project
# Conflicts:
#	docs/api.md
#	src/server/routes/v1/ServerV1PostgresRoutes.ts
2026-06-29 17:46:53 -07:00
Alex Newman
82d6afb8ec Merge PR #3078: paid-readiness — usage metering, rate limiting/quotas, key issuance + connect
# Conflicts:
#	docs/api.md
#	src/server/routes/v1/ServerV1PostgresRoutes.ts
2026-06-29 17:45:39 -07:00
Alex Newman
d4a7e42aed Merge PR #3070: remote authenticated MCP recall endpoint (POST /v1/mcp)
# Conflicts:
#	src/server/routes/v1/ServerV1PostgresRoutes.ts
2026-06-29 17:44:32 -07:00
Alex Newman
aad0d87c55 Merge remote-tracking branch 'origin/main' into release/recovery-2026-06-24
# Conflicts:
#	plugin/scripts/context-generator.cjs
#	plugin/scripts/mcp-server.cjs
#	plugin/scripts/server-service.cjs
#	plugin/scripts/transcript-watcher.cjs
#	plugin/scripts/worker-service.cjs
#	src/cli/handlers/session-init.ts
#	src/servers/mcp-server.ts
#	src/services/sync/ChromaSync.ts
#	tests/hooks/server-client.test.ts
2026-06-29 16:01:54 -07:00
Alex Newman
8f65bb0860 chore: bump version to 13.9.0
Ships the claude-mem/sdk export (cmem-sdk) — in-process
capture→compress→semantic-search with no HTTP worker or Redis — plus the
server-beta→server runtime rename (back-compat aliases retained).

Includes CMEM-SDK reference docs (docs/public/sdk/cmem-sdk.mdx) and a
correction to sdk.mdx's stale parse-error behavior note.

Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
2026-06-29 15:28:24 -07:00
ChenglinWei97
0058a7a079 docs(api): document the data-deletion endpoints
Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
2026-06-29 15:22:48 -07:00
Alex Newman
108c5c65a1 Merge remote-tracking branch 'origin/main' into mutual-aardvark
# Conflicts:
#	plugin/scripts/context-generator.cjs
#	plugin/scripts/mcp-server.cjs
#	plugin/scripts/server-service.cjs
#	plugin/scripts/transcript-watcher.cjs
#	plugin/scripts/worker-service.cjs
#	plugin/ui/viewer-bundle.js
#	src/server/routes/v1/ServerV1PostgresRoutes.ts
#	src/server/runtime/ServerService.ts
#	src/server/runtime/create-server-service.ts
#	src/server/runtime/types.ts
#	tests/server/server-service.test.ts
2026-06-29 15:10:51 -07:00
Alex Newman
ad4bd6f7a6 fix(observer): drop invalid prose and pause on quota 2026-06-29 10:27:11 -07:00
ChenglinWei97
d75477d323 feat(server): monthly token cap on writes (CLAUDE_MEM_MONTHLY_TOKEN_CAP)
Tokens are the real cost driver ($/1M-token pricing), so add a per-team monthly
token quota. Gates writes only (ingestion = generation = token spend); reads stay
available so an over-budget team can still recall. Reuses requireMonthlyQuota
(kind:'tokens') against the token usage the generation worker now records. tsc clean.

Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
2026-06-28 18:26:15 -07:00
ChenglinWei97
9cb6f07845 feat(server): key issuance + connect onboarding (POST /v1/keys, GET /v1/connect)
- POST /v1/keys (write scope) mints a READ-ONLY, optionally-expiring API key for
  the team and returns the paste-ready `claude mcp add ... /v1/mcp` command. Raw
  key shown once. Write-scope gate means a read key can't escalate into more keys.
- GET /v1/connect (read scope) returns the same command with a <YOUR_API_KEY>
  placeholder. mcpUrl comes from CLAUDE_MEM_PUBLIC_URL or the request host.

The minted key lands in the same Postgres api_keys store readAuth checks, so it
authenticates immediately — the test asserts a freshly-minted key calls /v1/usage
and gets 200. Cold-start (first key via web session) still needs the better-auth
org → team mapping; documented in api.md.

Tests (postgres-gated): mint → 201 + command shape, minted key authenticates,
read key can't mint (403), GET /v1/connect placeholder. Verified live 11/11; tsc clean.

Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
2026-06-27 23:17:31 -07:00
ChenglinWei97
b277285420 feat(server): usage metering + per-key rate limiting + monthly quota
Paid-readiness primitives for Server Beta. All opt-in via env (default off =
behavior unchanged), wired by making readAuth/writeAuth middleware arrays so no
per-route edits are needed. All fail-open / fire-and-forget — a metering or
limiter hiccup never takes the API down.

- usage_events table + PostgresUsageRepository (record / total / summarize).
- meterRequests middleware: one 'request' event per authed call (CLAUDE_MEM_USAGE_METERING=1).
  Token/observation metering uses the same repo from the generation worker.
- rate_limit_counters table + PostgresRateLimitRepository: atomic fixed-window
  UPSERT, correct across instances. requireRateLimit → 429 + Retry-After
  (CLAUDE_MEM_RATE_LIMIT_PER_MIN).
- requireMonthlyQuota → 402 at the per-team monthly cap (CLAUDE_MEM_MONTHLY_REQUEST_CAP).
- GET /v1/usage: per-kind totals for the team this month.

Tests (postgres-gated): repo aggregation, rate-limit 429 + local-dev skip,
quota 402/pass, fire-and-forget metering, and GET /v1/usage end to end (proving
the array-middleware wiring). Verified live 7/7; tsc clean. Docs in api.md.

Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
2026-06-27 23:12:19 -07:00
Alex Newman
4557920565 Merge remote-tracking branch 'origin/main' into mutual-aardvark
# Conflicts:
#	package.json
#	plugin/scripts/context-generator.cjs
#	plugin/scripts/mcp-server.cjs
#	plugin/scripts/server-service.cjs
#	plugin/scripts/worker-service.cjs
#	plugin/ui/viewer-bundle.js
#	src/server/runtime/create-server-service.ts
#	tests/sdk/parse-summary.test.ts
#	tests/sdk/parser.test.ts
2026-06-27 21:20:51 -07:00
ChenglinWei97
5ee4b3d8a3 docs(api): document the /v1/mcp remote MCP recall endpoint
Adds the connect command (claude mcp add --transport http ... /v1/mcp), the
read-only tool list (search/context/recent), auth (memories:read, team-scoped),
and the stateless-transport note.

Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
2026-06-26 16:52:15 -07:00
Alex Newman
ebe6133089 feat(telemetry): carry observation volume on rollups so cache-value survives migration (#3017)
* feat(telemetry): carry observation volume on rollups so cache-value survives migration

The context-cache-value, per-user-savings, and observation-type-by-model
metrics were derivable only from the legacy per-occurrence events
(context_injected, session_compressed), which decay to zero as the fleet
upgrades to 13.7.0 and switches to the rollups. The rollups already received
the underlying records — they just didn't aggregate the observation fields.

- observer_turn_rollup: add observations_created (Σ per-turn observation count,
  distinct from the rollup's turn `count`) + summed obs_type_* buckets, so
  cost-per-observation (total_cost_usd / observations_created) and
  observation-type-by-top_model are derivable from the rollup alone.
- context_injected_rollup: add total_observations_injected (cache-reuse count)
  + total_tokens_saved_vs_naive (windowed savings sum).
- scrub.ts: whitelist the three new emitted keys (obs_type_* already allowed;
  deny-by-default whitelist would drop them otherwise).
- docs: correct the rollup field tables — the prior context_injected_rollup row
  documented fields the code never actually emitted.
- tests: assert both new aggregations (167 telemetry tests pass).

Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01LEZpnYz9z4TjKcG19qHFrJ

* build(telemetry): regenerate plugin bundles for rollup observation fields

worker-service.cjs and transcript-watcher.cjs rebuilt via `npm run build` to
bundle the new observation aggregation. Incidental, telemetry-unrelated churn
in the other service/UI bundles was left out to keep the diff meaningful.

Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01LEZpnYz9z4TjKcG19qHFrJ

---------

Co-authored-by: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
2026-06-21 13:31:35 -07:00
Alex Newman
580621c936 PostHog telemetry overhaul: per-session rollups, unified instrumentation, redacted error tracking (#3010)
* feat(telemetry): add unified instrument() layer fanning out to logger + telemetry

Introduces src/services/telemetry/instrument.ts as the single
instrumentation entry point: always writes the local log line, then
(consent-gated, swallow-all) routes scrubbed props to the telemetry
sink. Logger stays telemetry-free (no cycle). Migrates 3 exemplar
call sites (SessionRoutes, BaseRouteHandler, ResponseProcessor) from
duplicated log+capture pairs to single instrument() calls.

Phase 1 of the PostHog telemetry overhaul (plans/2026-06-19-posthog-telemetry-overhaul.md).

Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>

* feat(telemetry): emit per-session session_compressed rollups at session end

Replaces the 5-minute time-window session rollup with a per-session
accumulator keyed by sessionDbId. Flushes one observer_turn_rollup per
session on session_end (deleteSession/removeSessionImmediate),
worker_shutdown (drained in shutdownTelemetry before the PostHog client
closes), and a periodic safety_flush for over-cap sessions (window_seq
bump + re-arm). Adds rollup_reason/window_seq to the scrub whitelist.
context_injected stays a time-window rollup (hook-level, no sessionDbId).

Fixes a shutdown-drain ordering bug where worker_shutdown rollups were
dropped because isShutdown latched before the drain; adds a
shutdownTelemetry() integration test covering it.

Phase 2 of the PostHog telemetry overhaul.

Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>

* feat(telemetry): capture redacted error data via PostHog Error Tracking

Adds $exception capture with an allow-then-redact error scrubber
(error-scrub.ts): home dir, absolute paths, DB connection-string creds,
URL userinfo, emails, API tokens (sk-/phc-/ghp-/AWS AKIA/JWT), hex and
IPv4 are stripped; message capped 500 / stack ~2KB. captureException is
consent-gated, profile-less ($process_person_profile:false), and
fingerprint rate-limited (1/min/fingerprint, bounded LRU map). Manual
errors route via a no-cycle logger error sink (logger.setErrorSink).

Worker enables enableExceptionAutocapture, with errorBeforeSend fully
redacting autocaptured $exception_list payloads (deletes source
context_line/pre_context/post_context, redacts value + filename) so the
SDK's on-disk source-context capture can never exfiltrate code. A
sentinel marks manual captures so the limiter runs exactly once and
occurrence_count ships intact. Redaction inputs are hard-capped at 8KB
with bounded regexes to prevent ReDoS on the worker.

New kill-switch CLAUDE_MEM_TELEMETRY_ERRORS=0 disables error capture
independently of analytics.

Phase 3 of the PostHog telemetry overhaul.

Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>

* docs(telemetry): canonicalize rollup naming and document the new model

Fixes a stale session_compressed_rollup comment to the canonical live
event name (observer_turn_rollup). Rewrites docs/public/telemetry.mdx to
reflect the overhaul: per-session observer_turn_rollup with
rollup_reason/window_seq, the new redacted $exception error tracking
(what is kept vs redacted, rate-limiting, consent gate, one-way-door
note), the CLAUDE_MEM_TELEMETRY_ERRORS opt-out, the unified instrument()
logging model, and a note that PostHog session replay is N/A for a
headless Node worker. Reconciles "What is NEVER collected" honestly now
that redacted error messages are collected. No raw session_compressed or
context_injected events remain; only rollups are emitted.

Phase 5 of the PostHog telemetry overhaul.

Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>

* build(telemetry): regenerate plugin artifacts with telemetry overhaul

Rebuilds the tracked plugin/ bundle so the published plugin emits the
new per-session observer_turn_rollup (rollup_reason/window_seq), the
redacted $exception error tracking, and the unified instrument() layer.
The committed artifacts were stale (last built before Phases 1-5).

Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>

---------

Co-authored-by: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
2026-06-20 00:02:36 -07:00
Alex Newman
ccd907dc58 feat(telemetry): PostHog historical backfill — anonymized daily rollups + inferred install date (#2912)
* feat(telemetry): add PostHog historical backfill module (Phase 1)

One-time anonymized daily-rollup backfill: collectDailyRollups with pinned
aggregation semantics, sessions-first install-day inference, deterministic
v5 event uuids, 9-step gated transport (marker/consent/debug gates,
historicalMigration client, marker only on clean shutdown). Whitelist
additions in scrub.ts/common.ts, extended PostHog test mock, full (a)-(k)
test coverage. Per plans/2026-06-11-posthog-historical-backfill.md.

Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>

* feat(telemetry): wire historical backfill into worker startup + docs disclosure

Fire-and-forget runHistoricalBackfill call in initializeBackground after
the worker_started capture (non-blocking, marker-gated one-shot, retries
on next start if delivery fails). New "Historical backfill" section in
docs/public/telemetry.mdx documenting what is sent, the once-per-install
marker, identical consent gates, and upload-time geo caveat. Phases 3-4
of plans/2026-06-11-posthog-historical-backfill.md.

Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>

* docs(telemetry): document dual-layer error handling on backfill call site

runHistoricalBackfill never rejects by contract; the .catch is the
unhandled-rejection backstop for the fire-and-forget call. Addresses
Greptile review on #2912.

Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>

---------

Co-authored-by: Claude Fable 5 <noreply@anthropic.com>
2026-06-12 18:10:39 -07:00
Alex Newman
4ce51efd28 feat(telemetry): reliability signals — retrieval quality, compression trust, worker lifecycle, hook failures (Plan 14) (#2874)
* feat(telemetry): disclose 19 reliability-signal fields and 2 new events across all surfaces

Whitelist (scrub.ts), scrub tests, public docs (telemetry.mdx), and CLI
disclosure (COLLECTED_FIELDS/EVENT_NAMES) for the Plan 14 reliability
signals: search retrieval quality, compression trust, worker lifecycle,
and hook failure keys, plus the worker_stopped and hook_failed events.
Includes the plan document.

Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>

* feat(telemetry): retrieval-quality signals on search_performed

SearchManager.search() fills an optional telemetry envelope
(result_count, search_strategy, chroma_available, fallback_reason)
across all three search paths; handlers stash it on
res.locals.searchTelemetry and the existing finish-middleware spreads it
into the search_performed capture. Zero-result searches report
result_count: 0; Chroma fallback reasons are a closed enum, never the
error message. Response shapes unchanged.

Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>

* feat(telemetry): compression trust signals on session_compressed

fabrication_detected/fabricated_count flow through compressionProps (all
three emit paths); invalid-output respawns emit a respawn-gated
session_compressed with outcome invalid_output and the classifier value;
aborted generators emit outcome aborted with abort_reason normalized to
a closed enum in the .finally where all five abort flows converge (the
.catch path can never observe a non-null abortReason).

Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>

* feat(telemetry): worker lifecycle signals — worker_stopped, crash detection, memory metrics

Clean-shutdown sentinel written before telemetry flush and consumed at
startup; worker_started gains previous_shutdown (crash/clean/unknown)
and previous_uptime_seconds derived from the stale PID file; new
worker_stopped event (uptime_seconds, shutdown_reason stop/restart/
signal) emitted before shutdownTelemetry(); the CLI restart path tags
/api/admin/shutdown?reason=restart so restarts are distinguishable;
buildLifecycleProps adds integer process_rss_mb/heap_used_mb.

Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>

* feat(telemetry): threshold-gated hook_failed distress signal via CLI transport

recordWorkerUnreachable emits hook_failed exactly when the consecutive-
failure count reaches the fail-loud threshold; the generic blocking-error
branch emits error_mode blocking_error. Both emits are awaited before
the process.exit paths so the 2s-capped CLI POST survives; hook_type is
a closed enum registered at hookCommand entry. Exit codes unchanged.

Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>

* chore(build): regenerate plugin artifacts with Plan 14 telemetry signals

Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>

* fix(tests): make PostHog client regression test order-independent via global preload mock

The disableGeoip regression test mocked posthog-node per-file, but
telemetry.ts is imported transitively by many test files in the shared
bun process, so the mock registered too late and the test failed in
full-suite runs — CI on main has been red since v13.5.4. The mock now
registers in a bunfig [test].preload before any module loads, which
also guarantees test runs can never construct a real PostHog client and
flush fabricated events into production analytics (consent is
default-on and the suite outlives flushInterval). telemetry.ts gains a
test-only state reset so construction is observed deterministically
regardless of suite order.

Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>

* fix(telemetry): forward shutdown reason in Windows-managed IPC message

Review follow-up: the wrapper IPC path discarded the restart tag, so an
external Windows wrapper could only ever report shutdown_reason 'stop'.
No wrapper in this repo listens for the message, but the reason now
travels with it for any that does.

Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>

---------

Co-authored-by: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
2026-06-10 02:51:45 -07:00
Alex Newman
245c9b4e88 feat(telemetry): enable ingest-side GeoIP for worker events
posthog-node assumes server deployments and stamps $geoip_disable: true
on every event by default, which left ~98.5% of events with no location
data. The worker runs on the user's own machine, so the ingestion
request already carries their IP — passing disableGeoip: false lets
PostHog derive coarse location (country/region/city) at ingest. Raw IPs
are still never attached to events and are discarded on ingest.

Docs and the CLI consent screen now disclose the ingest-derived coarse
location. New regression test guards the client construction options.

Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
2026-06-10 00:21:19 -07:00
Alex Newman
384d328976 feat(telemetry): real token/cost/model data + install-state snapshot
Fixes four session_compressed data-quality bugs found via PostHog analysis:

- Claude tokens_output was an early-streaming placeholder (2-10 tokens) from
  the assistant message; the event is now stashed and fired from the SDK
  result message with finalized per-turn usage (empirically verified per-turn)
- cost_usd now real: Claude from cumulative total_cost_usd deltas between
  results, OpenRouter from usage.cost + cost_details.upstream_inference_cost
  (BYOK) with usage accounting requested from openrouter.ai only
- compression_ratio < 1 / 0.0 artifacts killed: both-or-null usage guards in
  Gemini/OpenRouter providers, input > 0 ratio guard, and a new endpoint_class
  property (openrouter | custom) to segment custom-gateway usage reporting
- model never silently absent: response.model stamped over the configured
  string, array-typed model config normalized, 'unknown' floor in both
  session_compressed emit sites, providers seed session.lastModelId

Also adds an install-state snapshot to worker_started (start + daily
heartbeat) as person properties: db_observation_count, db_session_count,
db_summary_count, db_project_count, db_size_mb, install_age_days,
obs_count_7d, obs_count_30d, days_since_last_obs — aggregate counts and
day-deltas only, never content. Epoch math normalizes legacy seconds-unit
rows. Fixes the ide lookup querying the legacy sessions table (no
platform_source column), which silently threw on every start.

Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
2026-06-09 23:07:58 -07:00
Alex Newman
f1e8481427 feat(telemetry): platform/toolchain capture for install-funnel diagnosis — os_version, WSL, install method, bun/uv/node/Claude Code versions
Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
2026-06-09 21:49:10 -07:00
Alex Newman
be43fd18b8 feat(telemetry): deep instrumentation — observation types, token economics, compression metrics, IDE on lifecycle events
context_injected now carries the full injection story computed alongside
rendering (ContextInjectStats): observation/session counts, timeline
depth, per-type buckets, tokens_injected, tokens_saved_vs_naive,
mode/provider/search_strategy. Capture moved from middleware into the
handler so stats ride the event; empty-state injections are not counted.

session_compressed gains hook (init|ingest|summarize, stamped at prompt
dispatch), compression_ms (dispatch → response), model id, ide (session
platform source), per-type buckets + dominant observation_type, and REAL
token usage: Claude from message usage (incl. cache), Gemini from
usageMetadata, OpenRouter from usage — never the 70/30 estimates;
providers leave lastUsage null when the API gives no split. provider is
now always set (currentProvider/agentName fallback chain).

worker_started moved to post-init so it can read the install's dominant
IDE from session history — person-property ide makes IDE-level
DAU/retention breakdowns non-null without reinstalling.

signal_rate_pct from the spec was NOT added: nothing in the codebase
measures injected-token relevance, and we don't fabricate metrics.

Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
2026-06-09 21:33:26 -07:00
Alex Newman
4bdea1e21c feat(telemetry): person profiles on lifecycle events to unlock retention/cohort analytics
PostHog cannot compute retention, stickiness, lifecycle, or cohort
insights on profile-less events — exactly the charts growth reporting
needs. Lifecycle events (install_*, uninstall_completed, worker_started;
~1-2/day/install) now build a person profile keyed to the anonymous
install UUID with $set restricted to whitelisted enums. High-volume
operational events stay $process_person_profile:false for cost.

Adds plans/2026-06-09-telemetry-metrics-spec.md mapping every event to
the growth/retention/activation/reliability metric it powers.

Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
2026-06-09 18:47:42 -07:00
Alex Newman
482a6c921d feat(telemetry): real product instrumentation — install funnel, durations, volumes, failure paths
Sequential-thinking pass over what the data needs to answer (growth,
core-loop health, failure surface, feature adoption) showed the v1
events were shells: session_compressed only fired on success with a
constant outcome, duration_ms was documented but never sent, installs
and context injection were untracked.

New events: install_completed/install_failed/uninstall_completed (npx
CLI via direct-POST transport with 2s timeout — no SDK in the CLI
bundle), context_injected. Enriched: worker_started gains trigger
(start|daily heartbeat for true DAU) + startup duration_ms;
session_compressed gains duration_ms/count/has_summary and an error
capture at the generator failure path; search_performed gains endpoint
(bounded route-name enum) + duration_ms.

Whitelist extended only with bounded enums/counters (endpoint, ide,
provider, runtime_mode, trigger, count, has_summary, is_update). Docs
and CLI consent text updated to match.

Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
2026-06-09 18:47:42 -07:00
Alex Newman
13670b25f5 feat(telemetry): default to opt-out — telemetry on for installs with no recorded decision
DO_NOT_TRACK, CLAUDE_MEM_TELEMETRY=0, telemetry.json enabled:false, and
'claude-mem telemetry disable' all still force it off. The install-ID
bootstrap no longer records enabled:false as a side effect (enabled is
now optional = no decision), so the default genuinely applies to
existing installs. Install prompt and docs updated to opt-out language.

Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
2026-06-09 18:47:42 -07:00
Alex Newman
b0171ed63d feat(telemetry): opt-in anonymous usage analytics via PostHog (#2863)
Opt-in PostHog product analytics living in the long-running worker. Consent precedence DO_NOT_TRACK > CLAUDE_MEM_TELEMETRY > telemetry.json > default OFF; whitelist-only scrubber; claude-mem telemetry CLI; install-flow consent prompt as the final step; async dependency installs with live spinner heartbeat; full privacy docs. Ships dark until the publishable key lands.
2026-06-09 17:16:49 -07:00
darion-yaphet
f3fa5601d5 Keep published docs aligned with release metadata (#2756)
README badges, system requirements, translated README copies, and public docs had drifted from package.json. Align them with version 13.4.0 and the Node >=20 engine requirement, and make the hook overview count explicit as Setup plus five lifecycle events.

Constraint: package.json is the source of truth for current package version and engine requirements

Rejected: Update only the root README | translated README files would still publish stale requirements

Confidence: high

Scope-risk: narrow

Directive: Keep README badges and requirements synchronized with package.json during version or engine bumps

Tested: rg found no remaining version-6.5.0, node >=18 badge, Node 18.0.0 requirement, or Hook System (5 events) references under README.md docs package.json

Not-tested: Full docs build
2026-06-09 15:04:51 -07:00
Alex Newman
e22824295b docs(sdk): cmem-sdk Phase 8 — tests, plain-Node example, docs page
Tests (`tests/sdk/`)
-------------------

Five focused integration tests covering the SDK's contract. Each uses
the project's existing skip-if-no-db pattern (gating on both
`CLAUDE_MEM_TEST_POSTGRES_URL` and `CLAUDE_MEM_SERVER_DATABASE_URL` so
the suite runs under both the CI harness and the SDK's own env), and
the live-Chroma tests also gate on `uvx --version` (same pattern as
`tests/integration/chroma-vector-sync.test.ts`).

- `create-cmem-client.test.ts`: schema bootstrap idempotency; second
  `createCmemClient` reuses the persisted `sdk-tenant.json`; per-test
  `CLAUDE_MEM_DATA_DIR` under `os.tmpdir()` keeps host state untouched.
- `capture.test.ts`: `capture` writes exactly one `agent_events` row +
  one `observation_generation_jobs` row (status `queued`); `captureBatch`
  does the same per event in a single tx. Plus a static "no-Redis"
  import-guard at the source level.
- `generate.test.ts`: `captureAndGenerate` with a stub `CmemProvider`
  returning minimal-valid agent XML — verifies the observation row,
  `observation_sources` link, and `completed` job state without
  calling a live LLM API.
- `search.test.ts`: empty-query → `listByProject`; Chroma error path
  via `ChromaMcpManager.callTool` monkey-patch (cleaner than killing
  chroma-mcp mid-run) → asserts `{ chroma: false, degraded: true,
  error: { message } }`; `context()` shape with degraded surfaced.
- `close.test.ts`: consumer-supplied pool stays open after
  `client.close()`; SDK-owned pool closes; subsequent calls throw
  "cmem-sdk: client is closed"; `close()` is idempotent.

Test count: 2065 → 2071 pass, 22 → 23 skip, 0 fail. The skips expand
to ~12 integration assertions when `CLAUDE_MEM_TEST_POSTGRES_URL` +
uvx are available.

Example (`examples/sdk-node/`)
------------------------------

Plain-Node (not Bun) script proving the headline requirement: capture
→ generate → search inline, with no worker or daemon running. ESM
module imports `claude-mem/sdk` from the local `file:../../` package.

- `index.mjs`: reads `CLAUDE_MEM_SERVER_DATABASE_URL` +
  `ANTHROPIC_API_KEY` from env, bails clearly if missing, runs
  `captureAndGenerate` then `search` then `context` and logs results,
  then `client.close()`.
- `package.json`: type `module`, single dep `claude-mem`.
- `README.md`: prereqs (Postgres URL, API key, uvx), run command,
  note that no worker is needed.

`node --check examples/sdk-node/index.mjs` → OK.

Docs (`docs/public/sdk.mdx` + nav)
----------------------------------

New Mintlify page "Using claude-mem in your app (SDK)" — 1341 words.
Covers:

- Headline: in-process capture/compress/search with no worker.
- Quick-start with minimal code sample.
- `createCmemClient` options table.
- Architecture diagram (mirror of the plan's top-of-file diagram,
  marking Chroma REQUIRED).
- Chroma section: install uv/uvx; explanation of the
  required-at-construction gate; runtime degradation semantics
  (`{ degraded: true }` + `logger.error`).
- Tenancy: default-team + default-project bootstrap via
  `sdk-tenant.json` vs. passing explicit `teamId`/`projectId`.
- Provider configuration: env-based (`ANTHROPIC_API_KEY` etc.) vs.
  options-based (`{ apiKey, model, provider }`) vs. user-supplied
  `CmemProvider` instance.
- Lifecycle: `close()` ownership rules.
- Error handling: Chroma init rejection, FTS fallback `{ degraded:
  true }`, "client is closed" after `close()`.

`docs/public/docs.json` — new top-level nav group "SDK & Embedding"
(icon `code`) with the single `sdk` page, inserted after
"Configuration & Development". JSON validity confirmed.

Verification:
- `npm run build`: green; check:sdk-bundle clean.
- `dist/sdk/index.js`: 214.07 KB (no SDK runtime change in this phase).
- `npm run typecheck`: 0 errors.
- `bun test`: 2071 pass / 0 fail / 23 skip across 180 files.

Plan §8 (plans/2026-05-25-cmem-sdk-and-server-rename.md).

Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
2026-05-29 15:22:27 -07:00
Alex Newman
166b956e88 Merge remote-tracking branch 'origin/main' into mutual-aardvark
# Conflicts:
#	package.json
#	plugin/scripts/context-generator.cjs
#	plugin/scripts/mcp-server.cjs
#	plugin/scripts/server-service.cjs
#	plugin/scripts/worker-service.cjs
#	plugin/ui/viewer-bundle.js
#	src/npx-cli/commands/install.ts
#	src/server/runtime/ServerService.ts
#	src/server/runtime/create-server-service.ts
#	src/services/worker-service.ts
#	tests/hooks/server-client.test.ts
2026-05-29 14:22:31 -07:00
Alex Newman
34c734a1a4 refactor(server): rename server-beta → server runtime (plan phase 1)
Foundational rename: the server runtime is now `server`, not `server-beta`.
Removes the literal-string regression in `runtime-selector.ts` where only
`CLAUDE_MEM_RUNTIME='server-beta'` was accepted (anything else silently
fell back to the worker runtime).

What changed (1a–1d per plans/2026-05-25-cmem-sdk-and-server-rename.md):

- 1a Regression fix: `selectRuntime()` accepts both `'server'` and
  `'server-beta'` (canonicalizing to `'server'`); new settings keys
  `CLAUDE_MEM_SERVER_{URL,API_KEY,PROJECT_ID}` read first, legacy
  `*_BETA_*` keys fall back via a `pickFirstNonEmpty` helper.
- 1b Code identifiers: ~80 `ServerBeta*`/`serverBeta*`/`SERVER_BETA_*`
  symbols (classes, types, functions, vars, non-persisted constants) →
  `Server*`/`server*`/`SERVER_*`.
- 1c File renames: 14 files moved via `git mv` (tracked as renames);
  build target `server-beta-service` → `server-service` (emits
  `plugin/scripts/server-service.cjs`); dispatch sites in
  `worker-service.ts`, `runtime.ts`, `ServerService.ts` keep a documented
  `existsSync` fallback to the legacy `.cjs` for installs running from a
  pre-rename plugin cache (plan §1c line 149).
- 1d Persisted-value back-compat (zero-risk path):
  - DB table name `server_beta_schema_migrations` preserved
  - Job/source enum strings (`server_beta_generate_event`, …) preserved
  - `lockedBy: 'server-beta-worker'` literal preserved
  - Installer writes new canonical settings keys + `'server'` runtime
    value going forward; reads dual-accept old + new.

Verification:
- Typecheck: 24/24 baseline errors unchanged (no new errors introduced).
- Tests: 1810 pass / 54 fail / 19 skip — failure count matches the
  pre-Phase-1 baseline; no new failures introduced.
- Build: `npm run build` succeeds; `plugin/scripts/server-service.cjs`
  emitted.

Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
2026-05-29 14:01:34 -07:00
Alex Newman
d13fc437fa feat(provider): configurable OpenAI-compatible base URL (DeepSeek/LM Studio/custom)
Make the OpenRouter provider's base URL configurable via CLAUDE_MEM_OPENROUTER_BASE_URL
(+ OPENROUTER_BASE_URL env), turning it into a generic OpenAI-compatible client. Shared
resolver appends /chat/completions to a base or uses a full URL verbatim; default
behavior unchanged when unset. Model id passes through verbatim. Applied to both worker
and server providers.

Closes #2382 #2590 #2622 #2393.

Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
2026-05-28 18:48:39 -07:00
Alex Newman
8bd19b5c5a chore: bump version to 13.1.0 2026-05-11 00:28:52 -07:00
Alex Newman
e7bbb2a9aa server-beta: Phases 4–13 — event pipeline, generation, MCP, compat, Docker, team audit, observability (#2383)
* feat(server-beta): Phase 4 — Postgres event-to-generation-job pipeline

Adds POST /v1/events, /v1/events/batch, GET /v1/jobs/:id, GET /v1/events/:id,
and POST /v1/memories on the server-beta runtime, backed by Postgres.

- Event row + outbox generation-job row insert in one withPostgresTransaction.
- BullMQ enqueue happens after commit; enqueue failure leaves the row queued
  for Phase 3 startup reconciliation.
- ?generate=false skips the outbox; ?wait=true returns queue status only,
  never observation IDs (provider generation is Phase 5).
- Batch pre-validates all event projectIds against api-key scope before any
  write; mixed-project batches reject 403 with zero side effects.
- /v1/memories is a direct insert alias — no generator, no outbox.
- Cross-tenant /v1/jobs/:id returns 404 to avoid leaking row existence.
- New PostgresAuthMiddleware reads api_keys by SHA-256 hash; populates
  req.authContext.teamId/projectId; legacy ServerV1Routes (SQLite, used by
  worker runtime) is left untouched.
- Tests: unit suite hardened with stubbed pool.query so route registration
  is safe; integration tests skip cleanly without CLAUDE_MEM_TEST_POSTGRES_URL.

Verification: 87 pass / 1 skip / 0 fail. No new typecheck errors. Required
greps for WorkerService and MemoryItemsRepository in src/server/routes/v1
and src/server/runtime return no hits.

Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>

* feat(server-beta): Phase 5 — provider observation generator

Adds independent provider generation under src/server/generation/ with no
worker coupling. Server beta can now generate observations end-to-end:
event -> outbox -> BullMQ -> provider -> parser -> persisted observation.

- ProviderObservationGenerator orchestrates: lock outbox (queued -> processing),
  reload agent_event from Postgres (BullMQ payload is advisory only), call
  provider, hand raw text to processGeneratedResponse, route errors via
  markGenerationFailed with retryable flag from ServerClassifiedProviderError.
- processGeneratedResponse parses with parseAgentXml, persists via
  PostgresObservationRepository with deterministic
  generation_key = generation:v1:{job_id}:{index}:{fingerprint},
  links via PostgresObservationSourcesRepository, advances outbox status,
  appends observation_generation_job_events, audits — all in one
  withPostgresTransaction. Idempotent on retry via UNIQUE constraints.
- Three provider adapters under src/server/generation/providers/:
  Claude, Gemini, OpenRouter. Self-contained — no imports from
  src/services/worker/*. Worker providers unchanged.
- Shared error classification + prompt builder under providers/shared/.
  Prompt builder strips <private> at the edge; fully-private batches
  emit <skip_summary /> without billing the provider.
- ActiveServerBetaGenerationWorkerManager wires BullMQ Worker via
  ServerJobQueue.start(...) with concurrency 1 + autorun:false +
  worker.on('error') per BullMQ docs.
- New GET /v1/events/:id/observations on ServerV1PostgresRoutes returns
  observations linked via observation_sources, team/project scoped.

Verification: 104 pass / 4 skip / 0 fail. No typecheck regressions.
Anti-pattern greps clean for services/worker imports under src/server,
WorkerRef/ActiveSession/SessionStore in src/server/generation.

Deferred: ModeManager loading uses a stable fallback observation type
list; summary and reindex queue lanes are not yet wired.

Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>

* feat(server-beta): Phase 6 — independent server session semantics

server_sessions is now the canonical Server beta session model. Sessions
are independent of legacy worker ActiveSession state.

- PostgresServerSessionRepository extended: findByExternalIdForScope,
  endSession (idempotent via COALESCE(ended_at, now())),
  markGenerationStarted/Completed/Failed, listUnprocessedEvents (filters
  agent_events with completed agent_event jobs).
- ServerSessionRuntimeRepository wraps the repo; every method requires
  explicit team_id + project_id and validates scope via assertProjectOwnership.
- SessionGenerationPolicy supports per-event (default), debounce
  (BullMQ delayed-job replace via getJob+remove+add), and end-of-session.
  Configured via CLAUDE_MEM_SERVER_SESSION_POLICY and
  CLAUDE_MEM_SERVER_SESSION_DEBOUNCE_MS env vars; per-team override hooks
  are exposed on ServerV1PostgresRoutesOptions for future settings layer.
- POST /v1/sessions/start (find-or-create on (project_id, external_session_id),
  GET /v1/sessions/:id (scoped 404), POST /v1/sessions/:id/end
  (transactional: end + create summary outbox via UNIQUE collapse +
  enqueue post-commit). Re-ending is fully idempotent.
- processSessionSummaryResponse persists summary as kind='summary'
  observation with the same idempotency model
  (generation_key + observation_sources UNIQUE).
- ProviderObservationGenerator dispatches on source_type:
  agent_event -> processGeneratedResponse, session_summary ->
  processSessionSummaryResponse; loadEvents handles session-summary
  by loading unprocessed events.
- ActiveServerBetaGenerationWorkerManager wires summary BullMQ lane
  alongside event lane (concurrency=1, autorun=false, error listener
  attached per BullMQ docs).

Verification: 110 pass / 6 skip / 0 fail. Net typecheck error count
unchanged at 24 (pre-existing, none in Phase 6 files). Anti-pattern
greps clean for ActiveSession/SessionStore in src/server/runtime,
no worker imports anywhere in src/server.

Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>

* feat(server-beta): Phase 7 — hook routing without worker dependency

Hooks can now talk directly to server-beta when CLAUDE_MEM_RUNTIME=server-beta
is selected, with a clean worker fallback when server-beta is unhealthy.

- src/services/hooks/server-beta-client.ts — typed HTTP client for
  /v1/sessions/start, /v1/events, /v1/sessions/:id/end. Throws
  ServerBetaClientError with kind classification (missing_api_key,
  transport, timeout, http_error, invalid_response) and isFallbackEligible
  helper. Zero imports from services/worker/.
- src/services/hooks/runtime-selector.ts — reads CLAUDE_MEM_RUNTIME from
  settings, returns worker or server-beta context, logs
  [server-beta-fallback] reason=<code> on every config-time fallback.
- src/services/hooks/server-beta-bootstrap.ts — Postgres-backed API key
  bootstrap. Find-or-creates local-hook-team + local-hook-project,
  generates cmem_<random> key (SHA-256 hashed), inserts into api_keys
  with scopes events:write/sessions:write/observations:read/jobs:read.
  Settings file written with chmod 0600. rotateServerBetaApiKey() wired
  to a new `claude-mem server keys rotate` command.
- src/cli/handlers/{observation,session-init,summarize}.ts — every hook
  handler tries server-beta first when configured, falls through to the
  existing worker path on transport/5xx/429/missing-key. One WARN line
  per fallback. Hook JSON output shape unchanged.
- src/shared/SettingsDefaultsManager.ts — three new keys with defaults:
  CLAUDE_MEM_SERVER_BETA_URL, CLAUDE_MEM_SERVER_BETA_API_KEY,
  CLAUDE_MEM_SERVER_BETA_PROJECT_ID.
- src/npx-cli/commands/install.ts — when installer selects server-beta
  runtime and CLAUDE_MEM_SERVER_DATABASE_URL is set, bootstraps a local
  API key automatically. Warns and continues if the DB URL is missing.

plugin/scripts/*.cjs bundles rebuilt via npm run build to pick up the
new hook handler code path. No plaintext keys in the bundle (verified).

Verification: 16 hook unit tests pass; 275 server/storage/services tests
pass with 7 pre-existing failures (verified independent of this change
via git stash --include-untracked). Build clean. No new typecheck
errors in Phase 7 files.

Anti-pattern guards verified:
- /api/sessions/observations only reached via explicit fallback path
- server-beta runtime never starts the worker process
- API keys live only in ~/.claude-mem/settings.json (chmod 0600), never
  in the bundle (grep confirmed)
- Worker fallback preserved, observable via single WARN line per call

Deferred: semantic context injection (UserPromptSubmit hook) stays
worker-only; server-beta does not yet expose /v1/context/semantic.

Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>

* feat(server-beta): Phase 8 — MCP backed by server-beta core

MCP tools now route through server-beta in server-beta mode while keeping
worker-mode search/timeline/get_observations tools fully working.

- src/servers/mcp-server.ts — five new observation_* tools registered:
  observation_add, observation_record_event, observation_search,
  observation_context, observation_generation_status. Three memory_*
  compatibility aliases delegate to the canonical handlers. Worker
  auto-start is gated when selectRuntime() === 'server-beta' so MCP
  in server-beta mode never spawns the worker.
- src/services/hooks/server-beta-client.ts — addObservation,
  searchObservations, contextObservations, getJobStatus added so MCP
  shares one transport with hooks (Phase 7).
- src/server/routes/v1/ServerV1PostgresRoutes.ts — POST /v1/search and
  POST /v1/context REST cores backed by PostgresObservationRepository
  full-text search (GIN tsvector from Phase 1).
- Existing memory_search/timeline/get_observations tools call
  callWorkerAPI unchanged in worker mode; worker tests unaffected.

Verification: 39 pass / 4 skip / 0 fail on targeted suite. Pre-existing
7 baseline failures verified independent (git stash). No new typecheck
errors. WorkerService grep clean across src/servers/mcp-server.ts and
src/server/.

Anti-pattern guards verified:
- No duplicate generation logic in MCP — observation_record_event hits
  /v1/events which owns event+outbox+enqueue inside one tx
- WorkerService not imported anywhere under MCP server-beta path
- No hardcoded worker URLs — all transport via Phase 7 ServerBetaClient
- memory_* aliases retained, single handler per pair

Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>

* feat(server-beta): Phase 9 — compatibility adapters without coupling

Legacy /api/sessions/observations and /api/sessions/summarize endpoints
keep working on server-beta runtime by translating to AgentEvent and
session-end calls — no worker code, no route duplication.

- src/server/services/IngestEventsService.ts — shared event-ingest path
  used by both /v1/events and the compat adapter. Owns transactional
  event row + outbox row + lifecycle log + post-commit BullMQ enqueue,
  honors Phase 6 SessionGenerationPolicy.
- src/server/services/EndSessionService.ts — shared session-end path
  used by both /v1/sessions/:id/end and the compat adapter. Idempotent
  ended_at + summary outbox + deterministic summary job id.
- src/server/compat/SessionsObservationsAdapter.ts — translates legacy
  POST /api/sessions/observations payload (Claude Code transcript shape)
  -> AgentEvent (source_adapter='claude-code-compat',
  event_type='tool_use') -> IngestEventsService.ingestOne. Resolves
  contentSessionId to server_sessions via find-or-create.
- src/server/compat/SessionsSummarizeAdapter.ts — translates legacy
  POST /api/sessions/summarize -> EndSessionService.end. Preserves the
  legacy agentId -> {status:'skipped', reason:'subagent_context'}
  behavior so existing clients see the same response shape.
- src/server/routes/v1/ServerV1PostgresRoutes.ts — refactored to
  delegate to the new shared services (-203 LoC net) so /v1 and
  /api compat both call the SAME canonical code path.
- src/server/runtime/ServerBetaService.ts — registers both compat
  adapters alongside ServerV1PostgresRoutes, sharing service instances.
- docs/server-beta-parity-map.md — full enumeration of legacy /api/*
  routes labeled native, adapter, or unsupported (with reasons).
  Viewer read-path adapters explicitly listed as unsupported pending
  a future viewer-rewrite phase.

Verification: 7 compat tests pass, 6 v1-routes tests still pass
(refactor preserved behavior), 4 session-routes tests pass. Pre-
existing 16 baseline failures verified independent via git stash.
Zero new typecheck errors.

Anti-pattern guards verified:
- No services/worker/http/routes or WorkerService imports under
  src/server/compat or src/server/runtime
- Compat adapters are thin translators with names ending in *Adapter
  and a top-of-file comment noting they are legacy compatibility
- /v1/* remains the canonical Server beta API; compat adapters
  call shared services rather than acting as a parallel API

Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>

* feat(server-beta): Phase 10 — Docker stack and deployable runtime

Server beta now ships as a Docker stack with no worker process anywhere
and a separate horizontal generation worker for scaling.

- src/server/runtime/create-server-beta-service.ts — validateServerBetaEnv()
  fails fast on missing CLAUDE_MEM_SERVER_DATABASE_URL, requires
  CLAUDE_MEM_QUEUE_ENGINE=bullmq in Docker, rejects
  CLAUDE_MEM_AUTH_MODE=local-dev and CLAUDE_MEM_ALLOW_LOCAL_DEV_BYPASS
  inside containers (detected via /.dockerenv or CLAUDE_MEM_DOCKER=1).
  Adds CLAUDE_MEM_GENERATION_DISABLED so the HTTP service can run
  generator-free.
- src/server/runtime/ServerBetaService.ts — runServerBetaGenerationWorker
  for the dedicated consumer process; runServerBetaApiKeyCli is a new
  Postgres-backed `server api-key` command (the legacy worker CLI wrote
  to SQLite and was invisible to the Postgres runtime); getQueueHealth
  shim feeds /api/health a consistent ObservationQueueHealth shape.
- src/npx-cli/commands/{runtime,server}.ts — `claude-mem server worker
  start` subcommand that boots only the BullMQ consumer.
- docker/claude-mem/{Dockerfile,entrypoint.sh} — entrypoint forces
  CLAUDE_MEM_DOCKER=1 + CLAUDE_MEM_RUNTIME=server-beta and exposes
  three modes: server (HTTP only, generation disabled), worker (BullMQ
  consumer), shell. Worker bundle is no longer the default CMD.
- docker-compose.yml — full stack: postgres + valkey + claude-mem-server
  (HTTP-only) + claude-mem-worker (generation consumer). Wires
  service-to-service env vars.
- scripts/e2e-server-beta-docker.sh + docker/e2e/server-beta-e2e.mjs —
  E2E now hits /v1/sessions/start, /v1/events?wait=true, /v1/jobs/:id;
  asserts no worker-service.cjs process anywhere in the stack;
  one-shot docker compose run --rm verifies local-dev auth is
  rejected with the expected stderr; restart-and-verify confirms
  Postgres durability and BullMQ retry idempotency.
- docs/server.md — full Phase 10 doc: stack diagram, env table,
  worker mode, auth-in-Docker policy.
- docs/api.md — event generation semantics (wait=true, generationJob).

Verification: full Docker E2E PASSED on live daemon
(phase1 + phase2 + restart-and-verify + revoked-key + no-worker-
process + local-dev-rejected). Unit tests 292 pass / 9 skip / 7 fail
(7 fails pre-existing baseline). Zero new typecheck errors.

Anti-pattern guards verified:
- entrypoint never execs worker-service.cjs; E2E greps prove no
  worker process anywhere in the stack
- validateServerBetaEnv refuses local-dev auth in Docker with explicit
  remediation message; ALLOW_LOCAL_DEV_BYPASS rejected the same way
- Docker requires CLAUDE_MEM_QUEUE_ENGINE=bullmq; in-process queue
  rejected at startup
- claude-mem worker / worker-service / WorkerService greps clean
  in docker/

Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>

* feat(server-beta): Phase 11 — team-aware generation with audit chain

Generation jobs now carry team_id/project_id/api_key_id/actor_id/
source_adapter from enqueue through execution; the outbox is reloaded
from Postgres before any side effect so BullMQ payload can never act
as auth authority.

- src/server/jobs/types.ts — ServerGenerationJobPayloadSchema (Zod
  discriminated union) requires team_id, project_id, generation_job_id,
  source_adapter, api_key_id, actor_id (nullable), source_type, source_id,
  plus event_id / server_session_id per kind. assertServerGenerationJobPayload
  is called at enqueue (outbox.ts) and again at execution boundary.
- src/server/services/{IngestEventsService,EndSessionService}.ts +
  SessionGenerationPolicy.ts — thread identity context (apiKeyId, actorId,
  sourceAdapter) into both event and summary BullMQ payloads.
- src/server/generation/ProviderObservationGenerator.ts —
  loadCanonicalOutbox loads the outbox row WITHOUT scope filter, then
  compares candidate.team_id/project_id to payload.team_id/project_id;
  mismatch -> ServerGenerationScopeViolationError (non-retryable),
  failed status, generation_job.scope_violation audit. isApiKeyRevoked
  checks api_keys (revoked_at, expires_at, row missing) before any
  provider call; revoked -> generation_job.revoked_key audit + non-
  retryable failure. generation_job.processing audit emitted on lock.
- src/server/generation/processGeneratedResponse.ts — generated
  observations carry team_id/project_id/server_session_id from the
  reloaded source row (not job payload). observation_sources.metadata
  records source_adapter, actor_id, api_key_id for traceability.
  observation.created audit per observation; generation_job.completed
  audit per terminal transition. All audit rows reference the same
  generation_job_id in details.
- src/server/routes/v1/ServerV1PostgresRoutes.ts — GET /v1/teams/:id/jobs
  and GET /v1/projects/:id/jobs with SQL-layer scoping (WHERE team_id=$1
  [AND project_id=$2] [AND status=$3]); cross-tenant returns 404 to
  avoid leaking row existence. Pagination via status/limit/offset.
  audit_log rows for event.received, event.batch_received, observation.read.
- src/server/compat/{SessionsObservationsAdapter,SessionsSummarizeAdapter}.ts —
  propagate apiKeyId and sourceAdapter='claude-code-compat'.

Verification: 162 pass / 10 skip / 0 fail. Pre-existing failures in
tests/services/queue and tests/services/worker confirmed independent
via git stash. Zero new typecheck errors in server-beta files.
Required greps:
  rg "team_id.*req\.body|project_id.*req\.body" src/server -> 0 matches
Audit chain integration test passes — generation_job.processing,
observation.created, and generation_job.completed audit rows all
share the same generation_job_id reference.

Anti-pattern guards verified:
- BullMQ payload never acts as auth authority — Postgres outbox
  reload with mismatch check happens before every side effect
- team_id / project_id never derived from request body for scope
  decisions; always req.authContext.teamId / projectId
- Application-layer team/project filtering forbidden — listJobsForScope
  pushes scope into the SQL WHERE clause
- Project-scoped key on cross-project /v1/teams/:id/jobs returns 404
- Revoked api keys cause non-retryable failure with audit before
  any provider call

Deferred: a redundant generation_job.queued audit_log row (already
covered by observation_generation_job_events lifecycle log per Phase 1
schema split). Compat adapters set actor_id=null but propagate
api_key_id which is the canonical reference downstream.

Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>

* feat(server-beta): Phase 12 — observability and operations

Operators can now inspect, retry, and cancel generation jobs from the
CLI; queue lane metrics flow into /api/health and /v1/info; every
request gets a stable request_id that flows through HTTP -> audit ->
outbox -> generator -> completion log.

- src/server/middleware/request-id.ts — honors safe inbound X-Request-Id,
  mints uuid v4 otherwise. Set on req.requestId and echoed via response
  header so external traces can correlate.
- src/server/jobs/ServerJobQueue.ts — QueueEvents wired with completed,
  failed, progress, stalled, error listeners; lifecycle counters
  exposed via observe() API. Logs emitted as
  [generation] job=<id> source_type=<...> duration=<ms> attempts=<N>
  reason=<message>. Stalled and error counters survive worker restart.
- src/server/jobs/types.ts — ServerGenerationJob payload schema
  extended with optional request_id; flows through from HTTP into
  every BullMQ job.
- src/server/queue/ObservationQueueEngine.ts — health snapshot now
  carries per-lane (event, summary) counts via
  ObservationQueueHealthLaneSnapshot.
- src/server/runtime/{ActiveServerBetaQueueManager,
  ActiveServerBetaGenerationWorkerManager,ServerBetaService}.ts —
  per-lane getJobCounts feed /api/health and /v1/info; stalled events
  audit through audit_log with action generation_job.stalled.
- src/server/routes/v1/ServerV1PostgresRoutes.ts —
  GET /v1/jobs (status/source_type/since/limit/offset, scope from
  api-key, payload stripped unless ?include=payload AND admin scope),
  POST /v1/jobs/:id/retry (idempotent; queued -> no-op; audit
  generation_job.retried_by_operator), POST /v1/jobs/:id/cancel
  (terminal -> no-op; audit generation_job.cancelled_by_operator;
  generator reload-before-side-effects already prevents double work).
- src/server/services/IngestEventsService.ts +
  SessionGenerationPolicy.ts + ProviderObservationGenerator.ts —
  request_id propagated end to end. Generator extracts request_id
  from BullMQ payload and includes it in lock/processing/completion
  logs and audit details.
- src/npx-cli/commands/server-jobs.ts +
  src/npx-cli/commands/server.ts — `claude-mem server jobs
  status|failed|retry|cancel`. status compares Postgres outbox counts
  to BullMQ queue counts and surfaces divergence. failed prints
  attempts + last_error message. --team and --project filters.

Verification: 350 pass / 12 skip / 7 fail (pre-existing baseline,
verified independent via git stash). 18 new tests added (request-id
middleware, server-jobs CLI seams, jobs list/retry/cancel routes
Postgres-gated). Zero new typecheck errors.

Anti-pattern guards verified:
- agent_events.payload only emitted in /v1/jobs response inside the
  admin-gated branch (?include=payload + admin scope) — returns 403
  otherwise
- jobs retry on a queued row is a no-op (no double BullMQ enqueue,
  no double UPDATE)
- Every operator action writes to audit_log with the
  *_by_operator action and request_id correlation in details
- Stalled events audit through generation_job.stalled

Sample correlated trace (one request_id end to end):
  HTTP middleware: req.requestId = 'req-abc'
  audit event.received: details.requestId = 'req-abc'
  BullMQ payload: { request_id: 'req-abc', generation_job_id: 'gj_x' }
  generator lock log: [generation] job locked { jobId, requestId }
  audit generation_job.processing: details.requestId = 'req-abc'
  completion log: [generation] job=evt_... duration=1230ms

Deferred: live /api/health round-trip integration test (needs
Redis); stalled event live integration test (needs Redis); storing
request_id on the observations row itself (spec did not require).

Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>

* docs(server-beta): add Phase 13 release readiness report

Captures the final verification gate: tests (1749 pass, 45 fail all
pre-existing baseline, zero regressions), required greps clean,
Docker E2E green end-to-end, all 7 exit criteria met, build clean,
typecheck unchanged from main. Documents deferred items.

Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>

* build(server-beta): rebuild server-beta-service bundle

Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>

* fix(server-beta): address Greptile review on PR #2383

- ProviderObservationGenerator.lockOutbox: skip duplicate worker run when
  another lock is active instead of returning the row, which previously let
  two BullMQ workers issue the (paid, rate-limited) external provider call
  before the persistence-layer terminal-status guard collapsed the duplicate.
  Reconciliation still recovers from a stale lock on startup or next retry.
- docker-compose.yml: require POSTGRES_USER/PASSWORD/DB env vars (no
  defaults). Stack refuses to start without explicit secrets. Added a header
  warning that the file must not be deployed unmodified.
- e2e-server-beta-docker.sh: export ephemeral test creds for the new
  required env vars so the Docker E2E driver still runs unattended.
- ServerBetaService api-key list: bound query with LIMIT/OFFSET (default 100,
  max 500) and add optional --team filter to prevent unintentional
  cross-tenant key metadata disclosure on shared admin hosts.
- SessionGenerationPolicy: fix dead `??` fallback for NaN parseInt result;
  use `||` so DEFAULT_DEBOUNCE_MS actually applies.
- ServerV1PostgresRoutes: `?wait=true` now actually waits — polls the outbox
  row until terminal status (timeout 30s, 100ms interval) on both
  /v1/events and /v1/events/batch. Returns `waitTimedOut: true` if the cap
  is hit so callers can re-poll the status endpoints.

Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>

* fix(server-beta): address CodeRabbit + Greptile second review on PR #2383

P1 fixes
- Operator retry endpoint was re-publishing the Postgres outbox metadata
  column as the BullMQ payload; the worker's
  assertServerGenerationJobPayload always rejected it, leaving the row
  stuck in queued until startup reconciliation. Persist the BullMQ payload
  on the outbox row at create-time inside IngestEventsService and
  EndSessionService, then re-enqueue that canonical payload on retry.

Major fixes
- prompt-builder: escape server_session_id when interpolating into the
  XML prompt; previously a session id containing `<`, `&`, or quotes
  could inject XML into the provider input.
- ServerJobQueue: route both worker.on('stalled') and the QueueEvents
  'stalled' subscriber through a single notifyStalled helper that
  dedupes by jobId for 30s, so counters.stalled increments once per
  stall. QueueEvents 'error' now routes through notifyQueueError so
  it increments counters.errored and runs onError listeners — keeping
  observability symmetric across both sources.
- ServerV1PostgresRoutes: convert PostgresObservationRepository from
  three dynamic imports to a single static import for consistency.
- mcp-server / ServerBetaClient: actually forward the
  observation_record_event tool's `generate` flag through to the
  /v1/events endpoint as `?generate=false` instead of voiding it.
- server-sessions.markGenerationFailed: guard jsonb_set against a null
  error payload so the failure path can't null out metadata before the
  generation_status='failed' write commits.

Minor fixes
- server-sessions.endSession: keep updated_at stable on repeated calls
  so the documented idempotency contract holds.
- SettingsDefaultsManager + ServerBetaService.getServerBetaPort: derive
  the server-beta default port from UID (37877 + uid%100), matching the
  worker port pattern, so two users on the same host don't collide.
  Docker stacks always pass CLAUDE_MEM_SERVER_PORT explicitly so the
  containerized deployment is unaffected.
- server-session-runtime test: close the pg.Pool in afterAll.
- server-beta-release-readiness.md: escape pipes inside table inline
  code, add `text` language tag to the fenced log block.

Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>

* fix(server-beta): address Greptile + CodeRabbit third review on PR #2383

P1 fixes
- SessionsObservationsAdapter.resolveServerSession: catch unique-violation
  (23505) on concurrent compat inserts and re-fetch instead of returning
  500. Two compat callers carrying the same contentSessionId can both
  observe `existing===null` and race on the (project_id,
  external_session_id) unique constraint; the second now resolves to the
  raced row instead of dropping the event.
- /v1/events/batch: pass `sourceAdapter: null` to ingestBatch so each
  event's BullMQ payload (and persisted outbox payload column) reflects
  its own event.sourceAdapter via buildEventBullmqPayload's fallback,
  rather than stamping the whole batch with the first event's adapter.

Minor
- server-session-runtime test afterEach: wrap DROP SCHEMA in try/finally
  so client.release() always runs even if the drop throws.

Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>

* fix(test): drop `pool as never` cast — pg.Pool already matches PostgresPool

Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>

* fix(server-beta): retry of completed job now 409s instead of duplicating

retryGenerationJob previously fell through to the reset+re-enqueue path
when called on a job in `completed` status. The observations index
dedupes on (generation_job_id, parsed_observation_index, content) but
LLM output is non-deterministic, so a second provider run almost always
produced a different content string and bypassed the index, persisting a
parallel set of observation rows attributed to the same generation job.

Match cancelGenerationJob's 409 guard for completed jobs. failed and
cancelled remain valid retry targets.

Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>

* build(server-beta): rebuild bundles after rebase onto main

Regenerates the three plugin bundles so they reflect the rebased source
state. Mechanical rebuild output only — no source changes.

Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>

* fix(server-beta): wrap resolveServerSession in try/catch for structured error response

Greptile P1 on PR #2383: resolveServerSession was called before the try/catch
in both compat adapters, so Postgres errors during session lookup (timeout,
pool exhaustion, etc.) escaped to Express's default error handler and returned
HTML/text 500s. Legacy clients calling response.json() would get a parse
failure instead of the documented { stored: false, reason: 'internal_error' }
(or { status: 'error', reason: 'internal_error' } for the summarize adapter)
shape.

Move the resolveServerSession call inside the existing try block in both
adapters so any failure flows through the structured catch handler.

Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>

* fix(server-beta): catch 23505 unique violation in POST /v1/sessions/start

Greptile P1 on PR #2383: concurrent requests with the same externalSessionId
can both pass the findByExternalIdForScope check, both call repo.create,
and the loser hits the (project_id, external_session_id) unique constraint.
The handler treated that as an unknown error and returned a 500.

Apply the same pattern resolveServerSession already uses: catch error.code
'23505' when externalSessionId is set, refetch the row inserted by the
winning request, and return 200 with that session.

Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>

---------

Co-authored-by: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
2026-05-11 00:26:11 -07:00
Alex Newman
36b0929fae Server-beta: Postgres storage + independent runtime + BullMQ queue (Phases 1–3) (#2351)
* Add server beta runtime foundation

* Address server beta review findings

* Resolve server beta review comments

* Tighten server beta review follow-ups

* Harden server beta auth and search

* Avoid unnecessary FTS rebuilds

* Block scoped keys from creating projects

* Release BullMQ claims best effort on close

* Address server beta review blockers

* Reset BullMQ claims best effort

* Add Postgres observation storage foundation

* feat(server-beta): add independent runtime service

Introduce src/server/runtime/ as a self-contained server-beta runtime
that owns its lifecycle, Postgres bootstrap, and HTTP boundary without
depending on WorkerService.

ServerBetaService wraps the existing Server class, exposes
/healthz and /v1/info with runtime="server-beta", and persists state
to dedicated paths (.server-beta.pid|.port|.runtime.json). The four
boundary managers (queue, generation worker, provider registry, event
broadcaster) are intentionally disabled in this phase and report their
status through /v1/info; later phases activate them.

Adds plans/2026-05-07-finish-bullmq-branch-ship-plan.md to track the
remaining work for this branch.

Phase 2 of plans/2026-05-07-server-beta-independent-bullmq-observation-runtime.md.

Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>

* feat(server-beta): route CLI lifecycle and bundle separate runtime

scripts/build-hooks.js now produces plugin/scripts/server-beta-service.cjs
as a separate Node CJS bundle, alongside the existing worker-service
bundle. The server-beta runtime is now installable independently.

src/npx-cli/commands/server.ts routes start|stop|restart|status to the
server-beta lifecycle instead of the legacy worker. The worker keeps its
own start|stop|restart|status under the worker namespace; the two
runtimes can be operated independently.

src/services/worker-service.ts adds a server-* command parser branch
that delegates to the sibling server-beta-service.cjs bundle so
direct worker-service invocations still route to the right runtime.

tests/npx-cli-server-namespace.test.ts updated to expect server-beta
lifecycle routing.

Includes rebuilt plugin/scripts/*.cjs bundles produced by
build-and-sync.

Phase 2 of plans/2026-05-07-server-beta-independent-bullmq-observation-runtime.md.

Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>

* feat(server-beta): add BullMQ job queue primitives

Introduce src/server/jobs/ as the queue-side primitives that Phase 3 of
the server-beta runtime needs to operate.

types.ts defines a discriminated union over the four job kinds (event,
event-batch, summary, reindex) and maps each to a per-kind BullMQ queue
name and deterministic-ID prefix.

job-id.ts builds deterministic, colon-free BullMQ jobIds from
(kind, team, project, source). The colon ban exists because BullMQ uses
':' as a Redis key separator internally; embedding ':' in jobIds
breaks scan and state lookups.

ServerJobQueue.ts is a thin wrapper over BullMQ Queue + Worker that
enforces autorun:false, default concurrency 1, and an attached error
listener — all per BullMQ docs requirements. Test seams accept queue
and worker factories so unit tests do not need Redis.

outbox.ts publishes through the Postgres ObservationGenerationJob
repository as canonical history. enqueueOutbox writes the row first,
then publishes to BullMQ; if BullMQ throws, the row is transitioned to
failed and a failed event is appended. reconcileOnStartup re-enqueues
queued + processing rows after a restart, replacing terminal BullMQ
jobs that may still be holding the deterministic ID slot. markCompleted
and markFailed wrap transitionStatus and append the matching event row.

Includes 20 unit tests covering deterministic ID stability, colon-free
output, queue lifecycle, error-listener attachment, double-start
refusal, idempotent enqueue, BullMQ failure rollback, startup
reconciliation, max-attempts skipping, and completion / failure /
retry transitions.

Phase 3 commit 1 of plans/2026-05-07-server-beta-independent-bullmq-observation-runtime.md.

Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>

* feat(server-beta): activate queue boundary in runtime service

Wire ActiveServerBetaQueueManager into the server-beta runtime graph.
The active manager owns one ServerJobQueue per generation kind (event,
event-batch, summary, reindex) and surfaces lane metadata through
boundary health.

Selection is opt-in and fail-fast: if CLAUDE_MEM_QUEUE_ENGINE is set to
bullmq the active manager is constructed (and any Redis/config error
throws — no silent fallback to SQLite, per Phase 3 anti-pattern guard).
For any other engine the disabled boundary remains so worker-era and
test setups stay compatible.

Widens ServerBetaBoundaryHealth.status to a discriminated union
('disabled' | 'active' | 'errored') with optional details. The disabled
adapter still emits status='disabled', which keeps the existing
server-beta-service test green.

ServerBetaService receives the manager through a new optional
queueManager field on CreateServerBetaServiceOptions so test graphs
and Phase 4 wiring can inject custom managers.

Adds tests/server/runtime/active-queue-manager.test.ts covering bullmq
guard, active health shape, per-kind queue access, close behavior, and
post-close errored health.

Phase 3 commit 2 of plans/2026-05-07-server-beta-independent-bullmq-observation-runtime.md.

Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>

* fix(server-beta): cap /v1/events/batch at 500 events

Prevents unbounded array DoS surface flagged in PR review.

Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>

---------

Co-authored-by: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
2026-05-08 01:20:07 -07:00
Alex Newman
a5bb6b346a docs: document LiteLLM gateway routing 2026-05-05 15:08:09 -07:00
Alex Newman
9902a15b21 Remove stale internal docs and reports (#2290) 2026-05-04 20:22:29 -07:00
Alex Newman
d384d3c595 fix: bug-batch — 17 issues + 4 foundations (chroma, opencode, parser, OAuth, paths, uptime, classification) (#2282)
* feat: foundations F1-F4 + simple bug fixes

Foundations (no consumer adoption yet):
- F1 spawnHidden wrapper at src/shared/spawn.ts
- F2 paths namespace with 18 accessors + invariant test (tests/shared/paths.test.ts)
- F3 getUptimeSeconds at src/shared/uptime.ts
- F4 ClassifiedProviderError at src/services/worker/provider-errors.ts + 6 tests

Issue fixes (file-isolated, parallel-safe):
- #2231: SECURITY.md at repo root for GitHub Security tab
- #2240: dedupe observationIds before Chroma sync (ResponseProcessor.ts)
- #2247: add task_complete to Codex session-end events
- #2243: rsync excludes scripts/package.json + scripts/node_modules

Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>

* fix: validate Claude executable with --version and detect desktop app

Extract findClaudeExecutable() into shared utility used by both
SDKAgent and KnowledgeAgent (deduplication). Every candidate is now
validated with --version (3s timeout). Desktop app executables in
AppData/Program Files get an actionable error message directing
users to install the CLI via npm.

Closes #2222

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

* fix: use Zod schemas in OpenCode plugin to fix _zod.def crash

OpenCode 1.14.x walks arg._zod.def at plugin registration, which
crashes on plain JSON Schema objects like {type: "string"}. Replace
with z.string().describe() so the Zod internals are present.

Closes #2226, #2225, #2154

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

* fix: neutralize chroma-mcp CPU storm at the root

Two surgical fixes to the chroma backfill path that together cause the
sustained 60–80% CPU + orphan accumulation pattern reported across

1. ChromaMcpManager.getSpawnEnv: cap embedding-thread fanout
   ONNX Runtime / OpenBLAS / MKL all default to cpu_count(), so a 12-core
   machine spins 12 threads burning embeddings concurrently. The user's
   getSpawnEnv only handled SSL certs — no thread limits at all. Inject
   OMP_NUM_THREADS / ONNX_NUM_THREADS / OPENBLAS_NUM_THREADS / MKL_NUM_THREADS
   defaults of 2 (only if user hasn't pinned them), and
   ANONYMIZED_TELEMETRY=false to stop background HTTP from the embedding
   subprocess. Closes the storm at the source.

2. ChromaSync.backfill{Observations,Summaries,Prompts}: per-batch watermark
   The bump was in a trailing finally block. SIGKILL / OOM / power loss
   mid-flight skips finally entirely, so the watermark stayed at 0 and the
   next worker boot re-embedded the entire history (16K obs in #2220's
   case), which then pegged CPU forever in combination with (1). Move the
   bump inside the loop so progress is durable per batch.
   Closes #2214.

Verification:
- 26/26 chroma tests pass (tests/services/sync, tests/integration/chroma-vector-sync)
- Bundle confirms thread caps and per-batch bumps are present
- Full suite: 1429 pass / 20 fail — pre-existing failures only, no
  regression vs v12.4.9 baseline (1429 pass / 27 fail)

Closes #2214.
Substantially de-amplifies #2220 (the structural Job-Object cleanup is
still tracked separately at #2216).

Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>

* fix: kill chroma-mcp process tree and limit backfill concurrency

Three fixes for orphan chroma-mcp processes and resource exhaustion:

1. killProcessTree() in ChromaMcpManager.stop() tears down the full
   uvx->uv->python->chroma-mcp spawn chain (pkill -P on POSIX,
   taskkill /T on Windows) before MCP client.close().

2. Register chroma process with pgid for supervisor shutdown cascade.

3. backfillAllProjects() now processes max 3 projects concurrently
   with a re-entrancy guard to prevent overlapping fire-and-forget runs.

Closes #2216, advances #2220, #2213

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

* build: regenerate plugin artifacts after cherry-picks

Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>

* feat: foundation consumers + Cursor/stdin/queue/docs fixes

F1 spawnHidden adoption (#2236):
- 8 spawn → spawnHidden conversions across worker-utils, ProcessManager,
  npx-cli (install/runtime), supervisor/process-registry

F3 getUptimeSeconds adoption (#2250):
- Server.ts:165 (THE BUG: returned ms)
- Server.ts:270, SessionRoutes.ts:326 (4th ms-bug consumer found),
  DataRoutes.ts:225 (refactor for consistency)

#2188 stdin '{}' fallback removal:
- Diagnostic logging to <DATA_DIR>/logs/runner-errors.log + CAPTURE_BROKEN
  marker; exit 0 to preserve Windows Terminal exit-code strategy

#2196 ANTHROPIC_BASE_URL docs:
- New docs/public/configuration/custom-anthropic-backends.mdx
- Note: issue may need separate auto-detect feature; docs document
  existing plumbing only

#2242 check-pending-queue endpoints:
- Point at /api/processing-status + /api/processing per DataRoutes.ts;
  honor CLAUDE_MEM_WORKER_PORT env

#2248 Cursor sessions never summarized:
- Pulled reporter wbingli's tested fix (commit 46eaba44)
- Bug A: cursor adapter now derives transcriptPath from cwd+sessionId
- Bug B: parser accepts both line.type and line.role
- Bug C: walk backward, prefer non-empty text, fallback to empty
- Tests: 10-case regression suite + tests/fixtures/cursor-session.jsonl

Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>

* feat: F2 paths namespace adoption (#2237 + #2238)

Replaced 24 hardcoded homedir() + '.claude-mem' sites across 18 source
files with paths.<accessor>() calls from src/shared/paths.ts.

Accessors used: dataDir, workerPid, settings, database, chroma,
combinedCerts, transcriptsConfig, transcriptsState, corpora,
supervisorRegistry, envFile, logsDir.

Sites converted (file:area):
- src/cli/claude-md-commands.ts (database)
- src/services/context/ContextConfigLoader.ts (settings)
- src/services/infrastructure/ProcessManager.ts (workerPid)
- src/services/infrastructure/WorktreeAdoption.ts (settings)
- src/services/integrations/CodexCliInstaller.ts (settings)
- src/services/sync/ChromaMcpManager.ts (chroma + combinedCerts)
- src/services/transcripts/config.ts (transcriptsConfig + transcriptsState)
- src/services/worker/ClaudeProvider.ts (envFile)
- src/services/worker/GeminiProvider.ts (envFile + 2 more)
- src/services/worker/http/routes/DataRoutes.ts (dataDir)
- src/services/worker/http/routes/SettingsRoutes.ts (settings + envFile)
- src/services/worker/knowledge/CorpusStore.ts (corpora)
- src/shared/EnvManager.ts (envFile)
- src/supervisor/index.ts (supervisorRegistry)
- src/supervisor/process-registry.ts (supervisorRegistry)
- src/supervisor/shutdown.ts (supervisorRegistry)
- src/utils/claude-md-utils.ts (database)
- src/utils/logger.ts (logsDir + settings, lazy to avoid cycle)

CLAUDE_MEM_DATA_DIR override now flows through 100% of the worker
runtime; no per-file env reads needed.

Verification:
- Grep guard: zero homedir+'.claude-mem' sites remain in src/
  (excluding paths.ts itself and SettingsDefaultsManager.ts)
- F2 invariant test: 3/3 pass (60 expects)
- Foundation tests: 19/19 pass

Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>

* feat: F4 provider classification + parser fence + OAuth keychain

F4 adoption (#2244 + #2254):
- Per-provider classifiers: classifyClaudeError, classifyGeminiError,
  classifyOpenRouterError. Each lives in the provider file.
- New retry helper at src/services/worker/retry.ts: withRetry() honors
  ClassifiedProviderError.kind; retriable=transient/rate_limit (with
  retryAfterMs); not retriable=unrecoverable/auth_invalid/quota_exhausted.
  maxRetries=2, perAttemptTimeout=30s, exponential backoff with jitter.
- GeminiProvider + OpenRouterProvider fetch calls wrapped with retry.
  Best-effort request-id capture (x-goog-request-id, x-request-id,
  x-openrouter-request-id) for dedup logging.
- Deleted unrecoverablePatterns allowlist at worker-service.ts:540 area;
  worker dispatches on err.kind instead.
- 28 new classifier tests at tests/worker/provider-classifiers.test.ts:
  429-no-Retry-After, 500-with-quota-exceeded, OverloadedError,
  per-provider auth_invalid signals.

#2233 Part A — parser fence handling:
- src/sdk/prompts.ts: removed 4 fence markers from XML example blocks.
  Model now sees plain XML, eliminating the failure-mode that drained
  quota via repeated retries.
- src/sdk/parser.ts: stripCodeFences() at top, called before
  parseAgentXml. Fence-tolerant regardless of model behavior.
- TODO comment references #2233 Part B (tool-use migration as separate
  scope).
- 4 fence-tolerance tests added to tests/sdk/parser.test.ts.

#2215 OAuth token keychain:
- New src/shared/oauth-token.ts (~360 LOC): readClaudeOAuthToken()
  reads from platform-native credential stores at worker spawn-time.
  - macOS: security find-generic-password -s "Claude Code-credentials"
  - Windows: PowerShell wrapper around CredRead (Win32 Advapi32.dll)
  - Linux: secret-tool lookup
  - Fallback: env CLAUDE_CODE_OAUTH_TOKEN with JWT exp claim or sidecar
    expiresAt validation; refuses stale-token injection.
- EnvManager.buildIsolatedEnvWithFreshOAuth() (async) replaces silent
  process.env copy. Empty injection on absent; marker write on expired.
- <DATA_DIR>/oauth-stale.marker surfaces "re-login via Claude Desktop"
  via existing SessionStart additionalContext mechanism (context.ts).
- ClaudeProvider.startSession + KnowledgeAgent.prime/executeQuery now
  await the async env builder.
- 17 oauth-token tests covering decodeJwtExpMs, marker round-trip,
  env-fallback expiry detection.

Verification:
- npx tsc --noEmit: only pre-existing bun-types error
- bun test (foundations + new): 70 pass, 0 new fails (8 fails are
  pre-existing parser.test.ts cases unrelated to fence work)

Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>

* feat: #2234 quota-aware wall-clock guard

New src/services/worker/RateLimitStore.ts (207 LOC) — vendor pattern from
meridian/rateLimitStore.ts (MIT, copied not depended).

API:
- class RateLimitStore: set/get/getAll/getMostRecentByWindow/size/clear,
  in-memory last-write-wins keyed by rateLimitType.
- globalRateLimitStore singleton.
- shouldAbortForQuota(authMethod, store, now?) → {abort, reason?, window?}
- isApiKeyAuth(authMethod): matches both verbose getAuthMethodDescription
  strings and concise "api_key".

Thresholds (auth-type gated):
- api_key: never aborts (user authorized per-call spend).
- cli/oauth/subscription:
  - five_hour utilization >= 0.95 OR resetsAt within 15min (with 0.85
    utilization floor to avoid false trip on freshly-reset windows)
  - seven_day_opus >= 0.93
  - seven_day_sonnet >= 0.92
  - seven_day >= 0.93
  - overage >= 0.95

ClaudeProvider integration (line 198, for-await loop):
- Detects message.type === 'system' && subtype === 'rate_limit'
- Records rate_limit_info via globalRateLimitStore.set
- Calls shouldAbortForQuota(authMethod, globalRateLimitStore)
- On abort: session.abortReason = 'quota:<window>', abortController.abort,
  break out of loop. Worker continues other sessions.

Health endpoint (Server.ts:174):
- New rateLimits field on /api/health from getMostRecentByWindow().
- Field shape: {five_hour?, seven_day?, seven_day_opus?, seven_day_sonnet?,
  overage?} each carrying utilization, status, resetsAt, observedAt.

Tests (tests/worker/rate-limit-store.test.ts):
- 22 cases covering store CRUD, isApiKeyAuth, abort decision matrix.
- api_key never aborts at any utilization.
- cli aborts at threshold breaches per window.
- Reset-grace buffer with utilization floor.

Verification:
- npx tsc --noEmit: only pre-existing bun error
- bun test tests/worker/rate-limit-store.test.ts: 22/22 pass
- bun test tests/claude-provider-resume.test.ts: 9/9 pass
- bun test tests/server/: 44/44 pass

Plugin artifacts regenerated.

Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>

* build: regenerate worker-service.cjs after final build-and-sync

Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>

* test: align test assertions with F4 classification + timeout

Two test fixes for branch-introduced regressions vs main:

1. tests/gemini_provider.test.ts "should throw on other errors":
   F4's classifyGeminiError replaced upstream Error message with
   ClassifiedProviderError. Test was pinned to pre-F4 string.
   Updated assertion to match new "Gemini bad request (status 400)".

2. tests/infrastructure/graceful-shutdown.test.ts:
   Test pokes real ~/.claude-mem/supervisor.json registry which on a
   developer machine contains live worker + chroma-mcp PIDs. SIGTERM →
   wait → SIGKILL cascade takes ~6s end-to-end. Bumped per-test timeout
   to 15000ms. Underlying shutdown code unchanged. Future cleanup
   should mock getSupervisor() here.

Result: branch failure count == main (77 pre-existing failures).
No new regressions from this branch's work.

Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>

* review: address 4 Greptile P1/P2 findings on PR #2282

P1 (real bug): clearStaleMarker silently broken in ESM
- src/shared/oauth-token.ts:14: add unlinkSync to top-level fs import
- src/shared/oauth-token.ts:342: drop inline require('fs'), call
  unlinkSync directly. ESM has no require, so the previous code threw
  ReferenceError swallowed by try/catch — making clearStaleMarker a
  permanent no-op. Stale oauth marker would persist indefinitely after
  Claude Desktop refreshed the token.

P2 (security): execSync shell-string interpolation
- src/shared/find-claude-executable.ts:39: execSync(`"${candidate}"
  --version`) → execFileSync(candidate, ['--version']). Path containing
  ", ;, & — reachable on Windows via crafted CLAUDE_CODE_PATH in
  settings.json — would otherwise produce a malformed/exploitable
  command.

P2 (security): PowerShell username injection
- src/shared/oauth-token.ts:119: userInfo().username escaped with PS
  single-quote convention (' → '') before interpolation into
  `'Claude Code-credentials:${user}'`. Defensive against future Windows
  versions or domain-joined machines that may permit ' in usernames.

P2 (style): Unreachable throw lastError post-loop
- src/services/worker/retry.ts:109: explained as the safety net for
  opts.maxRetries < 0 (pathological input where the loop never executes
  and lastError is undefined). Annotated with comment + descriptive
  fallback Error so the dead-looking code is now self-documenting.

Verification:
- npx tsc --noEmit: clean (only pre-existing bun-types error)
- bun test tests/shared/oauth-token.test.ts tests/worker/provider-classifiers.test.ts
  tests/worker/provider-errors.test.ts: 50 pass / 0 fail

Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>

* review: tighten SECURITY.md data-flow and audit dates

Fixes CodeRabbit comments #3178957249 (Data Storage section overstated
"no external transmission" — softened to call out Claude Agent SDK,
alternate provider, Chroma MCP, OAuth keychain, and registry fetches)
and #3178957250 (Next Scheduled Audit was earlier than Last Updated;
bumped Last Updated to 2026-05-03 and audit to 2026-09-16) on PR #2282.

Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>

* review: drop inline require('fs') in paths.ts

Fixes CodeRabbit outside-diff comment on src/shared/paths.ts:25-29 from
PR #2282 review. resolveDataDir() ran require('fs') inside an ESM module
(this file uses import.meta.url and .js imports), which can break in
strict ESM environments. readFileSync now imports at the top alongside
existsSync/mkdirSync.

Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>

* review: block CLAUDE_CODE_OAUTH_TOKEN from parent env (issue #2215)

Fixes CodeRabbit outside-diff comment on src/shared/EnvManager.ts:14-17
from PR #2282 review. The OAuth-token leak fix was bypassed because
buildIsolatedEnv() copied every parent env var that wasn't in
BLOCKED_ENV_VARS, but CLAUDE_CODE_OAUTH_TOKEN was not blocked. A stale
parent token therefore still reached isolatedEnv even when the fresh
keychain read returned expired/absent — defeating the fix documented
inline at lines 178-183.

Adds CLAUDE_CODE_OAUTH_TOKEN to BLOCKED_ENV_VARS and defensively deletes
it again at the top of buildIsolatedEnvWithFreshOAuth() so the
fresh-spawn-time read is the only path that can populate it.

Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>

* review: validate cursor sessionId against path traversal

Fixes CodeRabbit comment #3178957252 on PR #2282. The Cursor adapter
took sessionId straight from stdin and concatenated it into a
join(homedir(), '.cursor', 'projects', ..., sessionId, ...) path. A
crafted value containing path separators or '..' segments could escape
~/.cursor/projects, and the later transcript read would then probe
arbitrary local files.

deriveCursorTranscriptPath() now rejects any sessionId that doesn't
match /^[A-Za-z0-9_-]+$/ — Cursor's real session ids are UUID-style
identifiers, so the safe whitelist is non-disruptive.

Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>

* review: scope stripCodeFences() to full-wrapper payloads only

Fixes CodeRabbit comment #3178957253 on PR #2282. The previous regex
greedily removed the first opening and last closing triple-backticks
anywhere in the input, which could mangle valid content with internal
fenced examples or surrounding prose — and ran before XML parsing so
it created false negatives.

stripCodeFences() now only strips when the entire payload is a single
fenced block (start-to-end, with optional language tag and surrounding
whitespace), capturing the inner content. Adds a regression test that
feeds prose with internal triple-backtick markers around a real
<observation> block and asserts the inner ``` are preserved.

Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>

* review: honor abortSignal during retry backoff sleep

Fixes CodeRabbit comment #3178957263 on PR #2282. The retry helper used
an unconditional `setTimeout` Promise for backoff between attempts, so
an external abort that fired during the wait was delayed until the
timer completed.

The backoff now races setTimeout against opts.abortSignal: if the signal
flips, the timer is cleared and the Promise rejects with 'Aborted'
immediately. The abort listener is registered with { once: true } and
removed when the timer fires to avoid leaks.

Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>

* review: abort immediately on provider-side rejected status

Fixes CodeRabbit comment #3178957261 on PR #2282. shouldAbortForQuota()
only checked utilization thresholds and reset-grace heuristics; a
snapshot with status='rejected' (or overageStatus='rejected' on the
overage window) but no utilization number could still return
{ abort: false }, letting the worker keep consuming after the provider
had already declared the bucket exhausted.

Provider-side rejection is now checked before utilization. When either
rejection signal is present the guard returns abort=true with reason
"quota:<window> rejected by provider".

Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>

* review: only bump Chroma watermark on confirmed batch writes

Fixes CodeRabbit comments #3178957259 (watermark advances on swallowed
batch failures) and #3178957260 (backfillInProgress can stick true if
init throws) on PR #2282.

addDocuments() previously logged and swallowed per-batch failures with a
void return type, so all three backfill loops (observations, summaries,
prompts) bumped the watermark unconditionally after the call —
turning a transient Chroma failure into permanently-skipped records.
addDocuments() now returns the count of documents that actually landed
(including delete+add reconcile retries), and each loop only advances
the watermark when the batch wrote successfully. Failed batches log a
debug message and continue so the loop still gets through the rest.

backfillAllProjects() now constructs SessionStore and ChromaSync inside
a try block so a constructor throw can't leave the static
backfillInProgress guard stuck true and silently skip every later
backfill. The finally always clears the guard and best-effort closes
each resource.

Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>

* review: fall back to pid kill when process group is gone

Fixes CodeRabbit outside-diff comment on src/supervisor/shutdown.ts:118-134
from PR #2282 review. signalProcess() returned silently when a pgid was
present and process.kill(-pgid, signal) threw ESRCH, never attempting
the per-pid signal. With the new chroma registration path that records a
pgid alongside the pid, an already-collapsed group could turn shutdown
into a no-op even though the root pid was still alive.

The POSIX branch now tries -pgid first when present, and on ESRCH falls
through to process.kill(pid, signal). Non-ESRCH errors still propagate.

Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>

* review: settings path, uptime clamp, fetch timeouts

Fixes three smaller CodeRabbit issues on PR #2282:

- SettingsRoutes (outside-diff #2282 review on lines 65-79): the
  parse-error response told users to delete ~/.claude-mem/settings.json
  even when paths.settings() resolved elsewhere. Now uses the resolved
  settingsPath variable in the message.

- uptime.ts (#3178957264 / lines 2-3): getUptimeSeconds() could return
  a negative value if startedAtMs was in the future or the system clock
  moved backward. Clamps with Math.max(0, ...) so health endpoints
  never see negative seconds.

- check-pending-queue.ts (#3178957248 / lines 27-45): checkWorkerHealth,
  getProcessingStatus and triggerProcessing all called fetch with no
  timeout, so the script could block forever if the worker accepted the
  TCP connection but never responded. Wraps each fetch with an
  AbortController + 10s timeout that throws a clear timeout message.

Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>

* review: walk descendants recursively when killing chroma-mcp tree

Fixes CodeRabbit comment #3178957258 on PR #2282. The POSIX teardown in
ChromaMcpManager.killProcessTree() relied on `pkill -P <pid>`, which
only signals direct children. Under uv, chroma-mcp spawns python as a
grandchild — when uv exits and python re-parents to init, pkill -P
never reaches it and the descendant survives the "tree kill".

killProcessTree() now collects the full descendant set via a recursive
`pgrep -P` walk before each signal phase. The walk returns leaves first
so signals propagate bottom-up (SIGTERM children before their parents,
then again for SIGKILL after the 500ms grace window so any layer that
re-parented during teardown still gets cleaned up). pgrep failures
(no children, missing binary) return [] so this stays best-effort and
falls back to the existing per-pid signal.

Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>

* review: tolerate malformed JSONL lines in transcript-parser

Fixes Greptile P1 comment 3178964456 on PR #2282.

extractLastMessageFromJsonl previously called JSON.parse(rawLine) with no
guard. A truncated/malformed JSONL line — common when a transcript was
crashed mid-write or partially flushed — would throw SyntaxError, crash
the summarization pipeline for that session, and silently lose all
prior valid messages.

Fix: wrap JSON.parse in try/catch and skip bad lines. The empty-line
guard only catches truly empty strings, not malformed fragments.

Regression tests added for two cases:
- Mixed valid + truncated lines: returns last valid match.
- All lines malformed: returns empty string (no throw).

Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>

* review: classify FK constraint failures BEFORE provider classifier

Fixes Greptile P1 comment 3178979583 on PR #2282.

The F4 #2244 work introduced a regression: reclassifyAtDispatch always
returns a non-null ClassifiedProviderError for known agent types
(Claude/Gemini/OpenRouter), so the isFkConstraintFailure branch was dead
code. Per-provider classifiers don't recognize "FOREIGN KEY constraint
failed", so SQLite FK failures fell through to the default 'transient'
kind and would retry indefinitely — restart loop on corrupted session
DB state.

Old unrecoverablePatterns explicitly listed FK constraint as
unrecoverable; restoring that semantic by checking FK FIRST and only
deferring to the classifier when not an FK error.

Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>

* review: validate CLAUDE_MEM_WORKER_PORT in check-pending-queue

Parse the env var, range-check (1-65535), and fall back to 37777 with a
console.warn on invalid input instead of letting a malformed value flow
into the URL builder unchecked (CodeRabbit Minor on PR #2282).

Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>

* review: SIGKILL union of pre-TERM and post-wait descendant sets

When the chroma-mcp root exits during the SIGTERM grace window, its
descendants get re-parented to init and drop out of the post-wait
pgrep -P scan. Without including the pre-TERM snapshot, those
re-parented PIDs would never receive SIGKILL even though they were
definitely children before SIGTERM and may still be alive (CodeRabbit
Major on PR #2282).

Compute Array.from(new Set([...descendantsBeforeTerm, ...descendantsBeforeKill]))
and SIGKILL the union. The two sets typically overlap, so dedupe is
required.

Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>

* review: enforce addDocuments return-count in direct sync paths

syncObservation/syncSummary/syncUserPrompt now capture the written count
from addDocuments() and only bump the watermark when every requested
document landed in Chroma. addDocuments() tolerates per-batch failures
(returns the actual written count), so the previous unconditional bump
was silently marking unsynced rows as synced on transient errors —
preventing the next backfill from retrying them (CodeRabbit Major on PR
#2282).

A partial write now logs a warn with the (requested, written) pair and
preserves retryability on the next pass.

Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>

* review: guard backfill watermark against non-contiguous failures

The backfill watermark is a single monotonic id, so it cannot represent
sparse success: "synced through 200, gap at 201–250, then 251 onward"
would, on restart, skip 201–250 forever because the watermark sat at
either 200 or 251 — both lose data (CodeRabbit Major on PR #2282).

Add a per-loop hadGap flag to backfillObservations / backfillSummaries /
backfillPrompts. Once any batch under-writes, every subsequent batch
must also skip the bump, regardless of whether it itself succeeded.
Also tighten the failure check from `writtenInBatch <= 0` to
`writtenInBatch < batch.length` so partial-batch writes are caught.

The watermark stays at the last contiguously-synced position; the next
backfill pass retries from there, eventually closing the gap.

Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>

* review: clear oauth-stale marker when token is absent

When an OAuth token disappears entirely (user logs out, keychain
cleared), buildIsolatedEnvWithFreshOAuth's absent branch was leaving any
prior stale-marker file in place. The session-start hook would then keep
surfacing an "expired token, re-login" warning even though the token is
no longer expired — it's gone, and re-login was already done elsewhere
or not applicable (CodeRabbit Minor on PR #2282).

Call clearStaleMarker() in the absent branch the same way the present
branch already does. Add a regression test exercising the full
buildIsolatedEnvWithFreshOAuth path: pre-write a marker, force absent
via spoofed unsupported platform, assert the marker is gone after.

Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>

* review: skip unknown message.content shapes instead of throwing

extractLastMessageFromJsonl already tolerates malformed JSONL lines
(JSON.parse failure -> continue), but a valid JSON line whose
message.content is an unexpected type (null, number, plain object) was
still throwing — contradicting the new tolerance and crashing the entire
summary pipeline on a single weird line (CodeRabbit Major + Greptile P1
on PR #2282).

Replace the `throw new Error(...)` with `continue` so a single bad
content shape skips that line instead of failing the whole transcript
read. Forward compat: future content schemas land harmlessly.

Add regression tests covering null, number, and plain-object content;
each must not throw and must fall back to the most recent valid line.

Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>

* review: guard null/primitive entries in message.content array

Fixes CodeRabbit comment 3179004190 on PR #2282.

The Array.isArray branch previously did `c.type === 'text'` directly,
which throws if `c` is null or a primitive — possible in malformed logs.
Tightened the filter with a type guard: requires c to be a non-null
object with type === 'text' and a string text field. Same defensive
class as the malformed-line and unknown-content-shape tolerances.

Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>

---------

Co-authored-by: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
2026-05-03 22:27:07 -07:00
Alex Newman
9e2973059a UX redesign: installer + provider rename + /learn-codebase + welcome card + SessionStart hint (#2255)
* feat(ux): claude-mem UX improvements with installer enhancements

Squashed PR #2156 commits for clean rebase onto main:
- feat(installer): add provider selection, model prompt, worker auto-start
- refactor: rename *Agent provider classes to *Provider
- feat: add /learn-codebase skill and viewer welcome card
- feat(worker): inject welcome hint when project has zero observations
- fix(pr-2156): address greptile review comments
- fix(pr-2156): address coderabbit review comments
- fix(pr-2156): persist CLAUDE_MEM_PROVIDER for non-claude in non-TTY mode
- fix(pr-2156): file-backed settings reads in installer + env-first SKILL doc

Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>

* build: rebuild plugin artifacts after rebase onto v12.4.7

Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>

* refactor(skills): strip claude-mem internals from learn-codebase

The learn-codebase skill, install next-step copy, WelcomeCard, and
welcome-hint previously walked the primary agent through worker endpoints
and synthetic observation payloads. The PostToolUse hook already captures
every Read/Edit the agent makes — the agent should have no awareness that
the memory layer exists. Collapse the skill to one instruction ("read every
source file in full") and rephrase touchpoints to describe only what the
user observes (Claude reading files), not what happens behind the scenes.

Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>

* fix(sync): preflight version mismatch + settings-aware port resolution

Two related fixes for build-and-sync's worker restart step:

1. Read CLAUDE_MEM_WORKER_PORT from ~/.claude-mem/settings.json the same
   way the worker does, instead of computing the default port from the
   uid alone. Previously, users with a custom port saw a misleading
   "Worker not running" message because the restart POST hit the wrong
   port and got ECONNREFUSED.

2. Add a preflight check that aborts the sync when the running worker's
   reported version does not match the version we are about to build.
   Claude Code's plugin loader pins the worker to a specific cache
   version per session, so syncing into a newer cache directory has no
   effect until the user runs `claude plugin update thedotmack/claude-mem`
   to bump the pin. The preflight surfaces this explicitly with the exact
   command to run; --force bypasses it for intentional cases.

Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>

* docs(learn-codebase): note sed for partial reads of large files

Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>

* refactor: strip comments codebase-wide

Removed prose comments from all tracked source. Preserved directives
(@ts-ignore, eslint-disable, biome-ignore, prettier-ignore, triple-slash
references, webpack magic, shebangs). Deleted two tests that asserted
on comment text rather than runtime behavior.

Net: 401 files, -14,587 / +389 lines, -10.4% bytes.

Verified: typecheck passes, build passes, test count unchanged from
baseline (22 pre-existing fails, all unrelated).

Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>

* refactor(installer): move runtime setup into npx, eliminate hook dead air

Smart-install ran 3 times during a fresh install — the worst run was silent,
fired by Claude Code's Setup hook after `claude plugin install`, producing
~30s of dead air that looked like the plugin was hung.

This change makes `npx claude-mem install` the single place heavy work
happens, with a visible spinner. Hooks become runtime-only.

- New `src/npx-cli/install/setup-runtime.ts` module: ensureBun, ensureUv,
  installPluginDependencies, read/writeInstallMarker, isInstallCurrent.
  Marker schema preserved exactly ({version, bun, uv, installedAt}) so
  ContextBuilder and BranchManager readers keep working.
- `npx claude-mem install`: ungated copy/register/enable for every IDE,
  inserts a "Setting up runtime" task with honest "first install can take
  ~30s" spinner. The claude-code shell-out to `claude plugin install` is
  removed — npx already populated everything Claude reads.
- New `npx claude-mem repair` command for post-`claude plugin update`
  recovery, force-reinstalls runtime.
- Setup hook now runs `plugin/scripts/version-check.js` (29ms wall) instead
  of smart-install. Mismatch prints "run: npx claude-mem repair" on stderr.
  Always exits 0 (non-blocking, per CLAUDE.md exit-code strategy).
- SessionStart loses the smart-install entry; 2 hooks remain (worker start,
  context fetch).

Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>

* chore(installer): delete smart-install sources, retarget tests

- Delete scripts/smart-install.js + plugin/scripts/smart-install.js (both
  are source files kept in sync manually; both must go).
- Delete tests/smart-install.test.ts (covered surface is gone).
- tests/plugin-scripts-line-endings: drop smart-install.js entry.
- tests/infrastructure/plugin-distribution: retarget two assertions at
  version-check.js (the new Setup hook script).
- New tests/setup-runtime.test.ts: 9 tests covering marker read/write,
  isInstallCurrent semantics. Marker schema invariant verified.

Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>

* docs(installer): describe npx-driven setup + version-check Setup hook

Sweep public docs and architecture notes to reflect the new flow:
npx installer does Bun/uv setup with a visible spinner; Setup hook runs
sub-100ms version-check.js; users hit `npx claude-mem repair` after a
`claude plugin update`.

- docs/architecture-overview.md: hook lifecycle table + npx flow paragraph
- docs/public/configuration.mdx: tree + hook config example
- docs/public/development.mdx: build output line
- docs/public/hooks-architecture.mdx: full rewrite of pre-hook section,
  timing table, performance table
- docs/public/architecture/{overview,hooks,worker-service}.mdx: tree
  comments, JSON config example, Bun requirement section

docs/reports/* untouched (historical incident reports).

Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>

* fix(install): mergeSettings writes via USER_SETTINGS_PATH

Greptile P1 (#2156): `settingsFilePath()` only resolved
`process.env.CLAUDE_MEM_DATA_DIR`, while `getSetting()` reads via
`USER_SETTINGS_PATH` which `resolveDataDir()` populates from BOTH the env
var AND a `CLAUDE_MEM_DATA_DIR` entry persisted in
`~/.claude-mem/settings.json`. Result: a user with the data dir saved in
settings.json but not exported in their shell would have provider/model
settings silently written to `~/.claude-mem/settings.json` while
`getSetting()` read from `/custom/path/settings.json` — read/write split.

Drop `settingsFilePath()` and the now-unused `homedir` import; reuse the
already-imported `USER_SETTINGS_PATH` constant.

Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>

* fix(cli): parse --provider, --model, --no-auto-start install flags

Greptile P1 (#2156): InstallOptions has fields `provider`, `model`,
`noAutoStart`, but the install case in the npx-cli switch only parsed
`--ide`. The other three flags were silently dropped — `npx claude-mem
install --provider gemini` was a no-op.

Extract a `parseInstallOptions(argv)` helper, share it between the bare
`npx claude-mem` and `npx claude-mem install` paths, and validate
`--provider` against the allowed set. Update help text accordingly.

Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>

* fix(install): pipe runtime-setup output, always show IDE multiselect

Two issues caught in a docker test of the installer:

1. The bun.sh installer, uv installer, and `bun install` were using
   stdio: 'inherit', dumping their stdout/stderr through clack's spinner
   region — visible as raw "downloading uv 0.11.8…" / "Checked 58
   installs across 38 packages…" text streaming under the spinner. Switch
   to stdio: 'pipe' and surface captured stderr only on failure (via a
   shared describeExecError() helper that includes stdout when stderr is
   empty). Spinner stays clean on the happy path.

2. promptForIDESelection() silently picked claude-code when no IDEs were
   detected, never showing the user the multiselect. On a fresh machine
   with no IDEs present yet (e.g. our docker test container), the user
   never got to choose. Now: always show the full IDE list when
   interactive; mark detected ones with [detected] hints and pre-select
   them; show a warn line if zero are detected explaining they should pick
   what they plan to use. Non-TTY callers still get the silent
   claude-code default at the call site (unchanged).

Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>

* fix(install): skip marketplace work for claude-code-only, offer to install Claude Code

Two related UX fixes from a docker test:

**Delay between "Saved Claude model=…" and "Plugin files copied OK"**

After dropping the needsManualInstall gate, every install was unconditionally
running `copyPluginToMarketplace` (which copied the entire root node_modules
tree — thousands of files, dozens of seconds) and `runNpmInstallInMarketplace`
(npm install --production) even when only claude-code was selected. Neither
is needed for claude-code: that path uses the plugin cache dir + the
installed_plugins.json + enabledPlugins flag, all of which we already write.

- Drop `node_modules` from `copyPluginToMarketplace`'s allowed-entries list;
  the dependency-install task populates it on the destination side anyway.
- Re-introduce `needsMarketplace = selectedIDEs.some(id => id !== 'claude-code')`
  scoped *only* to `copyPluginToMarketplace`, `runNpmInstallInMarketplace`,
  and the pre-install `shutdownWorkerAndWait` (also pointless for claude-code-
  only flows since we're not overwriting the worker's running cache dir
  source). All other tasks (cache copy, register, enable, runtime setup) stay
  unconditional.

**Claude Code missing → silent install of an IDE that isn't there**

When the user picked claude-code on a machine without it (e.g. a fresh
container), the install completed but `claude` was unavailable and the only
hint was a generic warn line. Replace with an explicit pre-flight prompt:

  Claude Code is not installed. Claude-mem works best in Claude Code, but
  also works with the IDEs below.
  ? Install Claude Code now?
    ◆ Yes — install Claude Code (recommended)
    ◯ No — pick another IDE below
    ◯ Cancel installation

If the user picks "Yes", run `curl -fsSL https://claude.ai/install.sh | bash`
(or the PowerShell equivalent on Windows), then re-detect IDEs and proceed
with claude-code pre-selected. If the install fails or the user picks "No",
the multiselect still appears with claude-code visible (just unmarked
[detected]), so they can opt in or pick another IDE.

Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>

* fix(install): detect Claude Code via `claude` CLI, not ~/.claude dir

The directory `~/.claude` can exist (e.g. mounted in Docker, or created
by tooling) without Claude Code actually being installed. Detect the
`claude` command in PATH instead so the installer correctly offers to
install Claude Code when missing.

* docs(learn-codebase): add reviewer note explaining the cost tradeoff

The skill intentionally reads every file in full to build a cognitive
cache that pays off across the rest of the project. Add a brief note
so reviewers (human or bot) understand the tradeoff before flagging
the unbounded read as a cost issue.

* fix: address Greptile P1 feedback on welcome hint and learn-codebase

- SearchRoutes: skip welcome hint when caller passes ?full=true so
  explicit full-context requests aren't intercepted by the hint.
- learn-codebase: replace `sed` instruction with the Read tool's
  offset/limit parameters, since Bash is gated in Claude Code by
  default.

* feat(install): ASCII-animated logo splash on interactive install

Plays a ~1s bloom animation of the claude-mem sunburst logomark when
the installer starts in an interactive terminal — geometrically rendered
via 12 ray curves around a center disc, in the brand orange. The
wordmark and tagline type on alongside the final frame.

Auto-skipped on non-TTY, in CI, when NO_COLOR or CLAUDE_MEM_NO_BANNER
is set, or when the terminal is too narrow.

Inspired by ghostty +boo.

* feat(banner): replace rotation frames with angular-sector bloom generator

Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>

* feat(banner): replace rotation frames with angular-sector bloom generator

Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>

* feat(banner): three-act choreography renderer with radial gradient and diff redraw

Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>

* feat(banner): update preview script to support small/medium/hero tier selection

Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>

* fix(docker): add COLORTERM=truecolor to test-installer sandbox

Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>

* feat(install): auto-apply PATH for Claude Code with spinner UX

The Claude Code install.sh prints a Setup notes block telling users to
manually edit "your shell config file" to add ~/.local/bin to PATH —
which left fresh installs unable to launch claude from the command line.

After a successful install, detect ~/.local/bin/claude on disk and, if
the dir is missing from PATH, append the right export line to .zshrc /
.bash_profile / .bashrc / fish config (idempotent, marked with a
comment). Also updates process.env.PATH for the current install run.

Wraps the curl|bash install in a clack spinner (interactive only) so the
~4 minute native-build download doesn't look frozen — output is captured
silently and dumped on failure for debuggability. Non-interactive mode
keeps inherited stdio for CI logs.

Verified end-to-end in the test-installer docker sandbox: spinner
animates, .bashrc gets the export, fresh login shell resolves claude.

Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>

* feat(banner): video-frame ASCII renderer with three-act choreography

Generator switched from a single Jimp-rendered logo to pre-extracted
video frames concatenated with \x01 separators and gzip-deflated, ported
from ghostty's boo wire format. Renderer rewritten around three acts
(ignite → stagger bloom → text reveal + breathe) with adaptive sizing,
radial gradient, and diff-based redraw.

Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>

* feat(onboarding): unify install / SessionStart / viewer around one first-success moment

Three surfaces now point at the same north-star moment — open the viewer, do
anything in Claude Code, watch an observation appear within seconds — with the
same verbatim timing and privacy lines, and a single canonical "how it works"
explainer instead of three diverging copies.

- Canonical explainer at src/services/worker/onboarding-explainer.md served via
  GET /api/onboarding/explainer; mirrored into plugin/skills/how-it-works/SKILL.md
- SessionStart welcome hint rewritten as third-person status (no imperatives
  Claude tries to execute), pinned with a default-value regression test
- Post-install Next Steps reframed as "two paths": passive default + optional
  /learn-codebase front-load; drops /mem-search and /knowledge-agent from this
  surface; adds verbatim timing + privacy lines and /how-it-works link
- /api/stats response gains firstObservationAt for the viewer stat row
- Viewer WelcomeCard branches on observationCount === 0: empty state shows live
  worker-connection dot + "waiting for activity"; has-data state shows
  observations · projects · since [date] and two example prompts. v2 dismiss key
- jimp added to package.json to fix pre-existing banner-frame build break

Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>

* fix(banner): play unconditionally; only honor CLAUDE_MEM_NO_BANNER

The 128-col / TTY / CI / NO_COLOR gates silently swallowed the banner in
narrower terminals, CI logs, and any non-TTY pipe — including Docker runs
where -it should preserve the experience but column width was the wrong
gate. Remove the implicit gates; keep the explicit opt-out only.

If a frame wraps in a narrow terminal, that's better than the banner
not playing at all.

Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>

* revert(banner): restore 15:33 gating logic per user request

Reverts eb6fc157. Restores isBannerEnabled to the state at commit
8e448015 (2026-04-30 15:33): TTY check, !CI, !NO_COLOR, !CLAUDE_MEM_NO_BANNER,
and cols >= BANNER.width.

Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>

* feat(install): wrap remaining slow steps with spinners

Each IDE installer (Cursor, Gemini CLI, OpenCode, Windsurf, OpenClaw,
Codex CLI, MCP integrations) now runs inside a clack task spinner with
per-step progress messages instead of silent dynamic-import + cpSync.
Pre-overwrite worker shutdown (up to 10s) and the post-install health
probe (up to 3s) also get spinners.

Internal console.log/error/warn from each IDE installer is buffered
during the spinner; if the install fails, captured output is replayed
afterward via log.warn so users can see what broke.

Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>

* fix(review): observation count + IDE pre-selection regressions

WelcomeCard's "no observations yet" empty state was triggered when a
project filter narrowed the feed to zero rows, even with thousands of
observations elsewhere. Source the count from global stats.database
to match firstObservationAt's scope.

Restore initialValues: [] in the IDE multiselect — pre-selecting every
detected IDE was the exact regression #2106 was filed for.

Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>

* fix(install): trichotomy worker state + cache fallback for script path

ensureWorkerStarted now returns 'ready' | 'warming' | 'dead' instead of
boolean. The spawned-but-still-warming case (common in Docker cold
starts and slow first-time inits) was being misreported as 'did not
start', which contradicted the next-steps panel saying 'still starting
up'. Install task message and Next Steps headline now agree on the
actual state.

Also fixes the actual root cause of 'Worker did not start' on
claude-code-only installs: the worker script path was hardcoded to the
marketplace dir, which is left empty when no non-claude-code IDE is
selected. Now falls back to pluginCacheDirectory(version) when the
marketplace copy isn't present.

Verified end-to-end in docker/claude-mem with --ide claude-code,
--ide cursor, and a fresh container — install task and headline
agree on 'Worker ready at http://localhost:<port>' in all cases.

Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>

* docs: align CLAUDE.md and public docs with current code

Sweep across CLAUDE.md and 10 high-traffic docs/public/ MDX files to
remove point-in-time references and align with the actual current
shape of the codebase. Highlights:

- Hardcoded port 37777 → per-user formula (37700 + uid % 100) on the
  front-door pages (introduction, installation, configuration,
  architecture/overview, architecture/worker-service, troubleshooting,
  hooks-architecture, platform-integration).
- Default model 'sonnet' → 'claude-haiku-4-5-20251001' (matches
  SettingsDefaultsManager).
- Node 18 → 20 (matches package.json engines).
- Lifecycle hook count corrected (5 events).
- Removed the nonexistent 'Smart Install' component and pre-built
  directory tree referencing files that no longer exist
  (context-hook.ts, save-hook.ts, cleanup-hook.ts, etc.); replaced
  with the real worker dispatcher shape.
- Removed CLAUDE.md '#2101' issue tag (kept the design rationale).
- Replaced obsolete hooks.json example with a description of the real
  bun-runner.js / worker-service.cjs hook event shape.

Lower-traffic doc pages still hardcode 37777 — left for a separate
global pass.

Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>

* chore(scripts): land strip-comments around real parsers (postcss, remark, parse5)

Each language gets a real parser to locate comments, then we splice ranges
out of the original source. The library never serializes — that's how
remark-stringify produced 243 reformat-noise diffs in the first attempt
versus the 21 real strip targets here.

  JS/TS/JSX  -> ts.createSourceFile + getLeadingCommentRanges
  CSS/SCSS   -> postcss.parse + walkComments + node.source offsets
  MD/MDX     -> remark-parse (+ remark-mdx) + AST html / mdx-expression nodes
  HTML       -> parse5 with sourceCodeLocationInfo
  shell/py   -> kept hand-rolled hash stripper (no library worth the dep)

Preserves: shebangs, @ts-* directives, eslint-disable, biome-ignore,
prettier-ignore, triple-slash refs, webpack magic, /*! license keep,
@strip-comments-keep file marker. JS/TS handler runs a parse-roundtrip
check and refuses to write if syntax errors increased (catches the
worker-utils.ts breakage class from the 2026-04-29 attempt).

npm scripts:
  strip-comments         (apply)
  strip-comments:check   (CI-style, exits non-zero if changes needed)
  strip-comments:dry-run (list, no writes)

Verified --check on this repo: 21 changes, -4.0% bytes, no parse-error
regressions, no reformat-suspect false positives.

Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>

* refactor: strip comments codebase-wide via parser-backed tool

21 files changed, -17,550 bytes (-4.0%) of narrative comments removed
across .ts / .tsx / .js / .mjs and the .gitignore. JS/TS comments stripped
via ts.createSourceFile + getLeadingCommentRanges — same canonical lexer,
same behavior as the 2026-04-29 strip, no reformat noise.

Preexisting baseline (unchanged):
  typecheck: 16 errors at HEAD, 16 errors after strip (line numbers shift,
             no new error classes — verified via diff of sorted error lists)
  build:     fails at HEAD with CrushHooksInstaller.js unresolved import
             (preexisting, unrelated to this strip)

Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>

* fix(install): drop Crush integration references after extract

The Crush integration was extracted to its own branch on May 1, but the
import at install.ts:280 (and the case block + ide-detection entry +
McpIntegrations config + npx-cli help text) still referenced the now-
removed CrushHooksInstaller.js, breaking the build.

Removes:
- case 'crush' block in install.ts
- crush entry in ide-detection.ts
- CRUSH_CONFIG and registration in McpIntegrations.ts
- 'crush' from the IDE Identifiers help line in index.ts

Rebuilds worker-service.cjs to match.

Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>

* chore(banner): mark generated banner-frames.ts with @strip-comments-keep

Without this, every build/strip cycle ping-pongs five lines of doc
comments in and out of the auto-generated output. The keep-marker tells
strip-comments.ts to skip the file entirely.

Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>

* fix(build): drop banner-frame regen from build script

generate-banner-frames.mjs requires PNG frames in /tmp/cmem-banner-frames
that only exist after the maintainer runs ffmpeg locally on the source
video. CI has neither the video nor the frames, so the build broke on
Windows. The output (src/npx-cli/banner-frames.ts) is committed, so the
regen is a one-shot dev step — not a build step. Run the script directly
when the video changes.

Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>

* fix(worker): unstick the spinner — kill claim-self-lock, wake on fail, auto-broadcast

Three surgical changes that cure the stuck-spinner bug at the source.

Phase 1.1 (L9): claimNextMessage no longer self-excludes its own worker_pid.
A single UPDATE-RETURNING grabs the oldest pending row by id. Removes the
LiveWorkerPidsProvider plumbing that was never injected — Supervisor enforces
single-worker via PID file, so the multi-worker SQL was defending against a
configuration the project does not support.

Phase 1.2 (L19): SessionManager.markMessageFailed wraps PendingMessageStore.markFailed
and emits 'message' on the per-session EventEmitter. The iterator's waitForMessage
now wakes immediately on re-pend instead of parking for 3 minutes. ResponseProcessor
and SessionRoutes routed through the new wrapper.

Phase 1.3 (L24): PendingMessageStore takes an optional onMutate callback fired
from every mutator (enqueue, claimNextMessage, confirmProcessed, markFailed,
transitionMessagesTo, clearFailedOlderThan). SessionManager wires it; WorkerService
passes broadcastProcessingStatus. Ten manual broadcast calls deleted across
SessionCleanupHelper, SessionEventBroadcaster, SessionRoutes, DataRoutes, and
worker-service. Caller discipline becomes structurally impossible to forget.

Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>

* refactor(worker): delete dead code — legacy routes, processPendingQueues, decorative guards

Pure deletions. Phase 2 of kill-the-asshole-gates.

- Legacy /sessions/:sessionDbId/* routes (handleSessionInit, handleObservations,
  handleSummarize, handleSessionStatus, handleSessionDelete, handleSessionComplete)
  bypassed all five ingest gates and were a parallel write path. Folded the
  initializeSession + broadcastNewPrompt + syncUserPrompt + ensureGeneratorRunning
  + broadcastSessionStarted work into the canonical /api/sessions/init handler so
  the hook makes one round trip instead of two.
- processPendingQueues (~104 lines, zero callers) — replaced in Phase 6 by a
  one-statement startup sweep.
- spawnInProgress Map and crashRecoveryScheduled Set — decorative dedupe over
  generatorPromise and stillExists checks that already provide the real safety.
- STALE_GENERATOR_THRESHOLD_MS — pre-empted live generators and raced with the
  finally block; the 3min idle timeout already kills zombies.
- MAX_SESSION_WALL_CLOCK_MS — ran a SELECT on every observation to enforce 24h.
  Runaway-spend protection lives in the API key, not in claude-mem.
- Missing-id 400 in shared.ts ingestObservation — Zod already enforces min(1)
  on contentSessionId and toolName at the route schema.
- SessionCompletionHandler import + completionHandler field on SessionRoutes
  (orphaned after handler deletions).

Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>

* refactor(worker): SQL-backed getTotalQueueDepth — single source of truth

Was: iterate this.sessions.values() and sum getPendingCount per session.
Now: SELECT COUNT(*) FROM pending_messages WHERE status IN ('pending','processing').

The in-memory sessions Map drifted from the DB rows whenever a generator exited
without confirm/fail, leading to false-positive isProcessing in the UI. Phase 1.3's
auto-broadcast fires on every mutation, but it broadcast a stale Map count.
Reading from the DB makes the UI's spinner state match what the queue actually holds.

Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>

* refactor(worker): typed abortReason replaces wasAborted boolean

Was: a boolean wasAborted that lumped every abort together. The finally block
branched on !wasAborted, so any abort skipped restart — including idle aborts
with pending work, which is exactly the case where we DO want to restart.

Now: ActiveSession.abortReason is a typed enum 'idle' | 'shutdown' | 'overflow'
| 'restart-guard'. The finally block consumes the reason and only skips restart
for 'shutdown' and 'restart-guard'. Idle and overflow aborts fall through, so
if pending work exists they trigger restart correctly.

Dropped 'stale' and 'wall-clock' from the union — Phase 2 deleted those paths.
Natural-completion abort (post-success) intentionally has no reason; it's not
gating restart logic.

Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>

* refactor(worker): unify the two generator-exit finally blocks

Was: worker-service.ts:startSessionProcessor and SessionRoutes:ensureGeneratorRunning
each had their own ~70-line finally block with divergent restart-guard handling.
The worker-service path called terminateSession on RestartGuard trip and orphaned
pending rows (the L16 bug); the SessionRoutes path drained them. Two places to
update when rules changed.

Now: handleGeneratorExit in src/services/worker/session/GeneratorExitHandler.ts
owns the contract:
  1. Always kill the SDK subprocess if alive.
  2. Always drain processingMessageIds via sessionManager.markMessageFailed
     (which wakes the iterator — Phase 1.2).
  3. shutdown / restart-guard reasons: drain pending rows via
     transitionMessagesTo('failed'), finalize, remove from Map. Fixes L16.
  4. pendingCount=0: finalize normally and remove from Map.
  5. pendingCount>0: backoff respawn via per-session respawnTimer (no global Set;
     Phase 2.4 deleted that). RestartGuard trip drains to 'abandoned'.

Both finally blocks are now ~10-line wrappers that translate local state into the
canonical abortReason and delegate. Restored completionHandler injection into
SessionRoutes (was dropped in Phase 2 cleanup; needed by the unified helper for
finalizeSession).

Behavior change: SessionRoutes' previous "keep idle session in memory" was
deliberately replaced by the plan's "remove from Map on natural completion" —
next observation reinitializes via getMessageIterator → initializeSession.

Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>

* feat(worker): startup orphan sweep — reset 'processing' rows at boot

When the worker dies (crash, kill, restart), any pending_messages rows it left
in 'processing' state are by definition orphans (the only worker is dead).
Single SQL UPDATE at boot resets them to 'pending' so the iterator can claim
them again. Replaces the deleted processPendingQueues function (Phase 2.2).

Runs in initializeBackground after dbManager.initialize() and before the
initializationComplete middleware releases blocked HTTP requests, so no
in-flight request can race the sweep. NOT on a periodic timer — after boot,
every 'processing' row has a live consumer and a periodic sweep would race.

Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>

* refactor(worker): simplify enqueue catch, replace memorySessionId throw with re-pend

7.1: queueObservation's catch was logging two ERROR-level messages and rethrowing.
The rethrow is correct (FK violations / disk full / schema drift should crash
loudly), but the verbose ERROR logging pretended the error was recoverable.
Reduced to one INFO line + rethrow.

7.2: ResponseProcessor's memorySessionId guard was throwing if the SDK hadn't
included session_id on the first user-yield, terminal-failing the entire batch.
Now warns and re-pends in-flight messages via sessionManager.markMessageFailed
(which wakes the iterator — Phase 1.2). The next iteration tries again with
memorySessionId hopefully captured.

Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>

* fix(sync): mirror builds to installed-version cache for hot reload

When package.json bumps past Claude Code's installed pin, sync-marketplace
wrote new code to cache/<buildVersion>/ but the worker loaded from
cache/<installedVersion>/, so worker:restart reloaded the same old code.

Replace the exit-on-mismatch preflight with a mirror step: when versions
differ, also rsync plugin/ into cache/<installedVersion>/ so worker:restart
hot-reloads new code without a Claude Code session restart. The
build-version cache still gets written for the eventual
`claude plugin update`.

Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>

* chore: delete dead barrel files and orphan utilities

- src/sdk/index.ts (re-exports parser+prompts; nothing imported the barrel)
- src/services/Context.ts (re-exports ./context/index.js; no importers)
- src/services/integrations/index.ts (no importers)
- src/services/worker/Search.ts (3-line barrel of ./search/index.js)
- src/services/infrastructure/index.ts: drop CleanupV12_4_3 re-export
- src/utils/error-messages.ts (getWorkerRestartInstructions never imported)
- src/types/transcript.ts (170 LoC of types, zero importers)
- src/npx-cli/_preview.ts (banner dev preview, no script wires it)

Build + tests still pass; observations still flowing.

Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>

* chore(parser): drop unused detectLanguage

Only the user-grammar-aware variant detectLanguageWithUserGrammars()
is actually called.

Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>

* chore(types): drop unused SdkSessionRecord + ObservationWithContext

Both interfaces in src/types/database.ts had zero importers anywhere
in src or tests.

Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>

* chore(npx-cli): drop unused getDetectedIDEs + claudeMemDataDirectory

getDetectedIDEs has no callers — install.ts uses detectInstalledIDEs
directly. claudeMemDataDirectory has no callers either.

Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>

* chore(ProcessManager): drop dead orphan-reaper + signal-handler helpers

Each had zero callers in src/ or tests/:
  - cleanupOrphanedProcesses + enumerateOrphanedProcesses
  - ORPHAN_PROCESS_PATTERNS + ORPHAN_MAX_AGE_MINUTES
  - forceKillProcess
  - waitForProcessesExit
  - createSignalHandler
  - resetWorkerRuntimePathCache

The orphan reaper was retired in PATHFINDER Plan 02 ("OS process groups
replace hand-rolled reapers", commit 94d592f2) — these were the leftover
pieces. shutdown.ts uses the supervisor's own kill-pgid path instead.

parseElapsedTime kept (covered by tests/infrastructure/process-manager.test.ts).

Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>

* chore(scripts): delete 11 unreferenced DX/forensic scripts

None of these are referenced by package.json npm scripts or docs/.
All last touched on Apr 29 only as part of the comment-stripping
pass — the feature code itself is older and orphaned:

  analyze-transformations-smart.js
  debug-transcript-structure.ts
  dump-transcript-readable.ts
  endless-mode-token-calculator.js
  extract-prompts-to-yaml.cjs
  extract-rich-context-examples.ts
  find-silent-failures.sh
  fix-all-timestamps.ts
  format-transcript-context.ts
  test-transcript-parser.ts
  transcript-to-markdown.ts

These are standalone tools — runtime behavior unchanged.

Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>

* chore(scripts): delete unused extraction/ and types/ subdirs

- scripts/extraction/{extract-all-xml.py, filter-actual-xml.py, README.md}
  point at ~/Scripts/claude-mem/ — the user's pre-relocation path that no
  longer exists. Zero references in package.json, src/, or tests/.
- scripts/types/export.ts duplicates ObservationRecord etc. and has no
  importers (CodexCliInstaller imports transcripts/types, not this).

Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>

* chore(BranchManager): drop dead getInstalledPluginPath

OpenCodeInstaller has its own (used) getInstalledPluginPath; the
BranchManager copy never had any external callers.

Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>

* chore(ChromaSyncState): unexport DocKind (used internally only)

Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>

* test(gemini): drop stale earliestPendingTimestamp / processingMessageIds

Both fields were removed from ActiveSession in earlier queue-engine
cleanup. Tests had been silently keeping them because the mock sessions
use 'as any' to bypass strict typing, so the dead fields rode along
without complaint.

Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>

* chore: drop 3 unused module-level constants

- src/npx-cli/banner.ts: CURSOR_HOME, CLEAR_DOWN (banner uses
  CLEAR_SCREEN which combines clear-down + cursor-home into a single
  CSI sequence; the standalone constants were leftovers).
- src/services/worker/BranchManager.ts: DEFAULT_SHELL_TIMEOUT_MS
  (BranchManager only uses GIT_COMMAND_TIMEOUT_MS / NPM_INSTALL_TIMEOUT_MS).

Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>

* chore(opencode-plugin): drop dead workerPost helper

Only the fire-and-forget variant (workerPostFireAndForget) is actually
called. workerPost was the await-result version with no remaining caller.

Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>

* chore: drop 8 truly-unused interface fields

Verified each by grepping for `.field`, `"field"`, `'field'`, and
`field:` patterns across src/ + tests/ + plugin/scripts. Where the
only remaining usage was the assignment site, removed the assignments too.

- GitHubStarsData: watchers_count, forks_count (only stargazers_count read)
- TableColumnInfo: dflt_value (PRAGMA returns it but no caller reads it)
- IndexInfo: seq (PRAGMA returns it but no caller reads it)
- ObservationRecord: source_files (legacy field, no readers)
- HookResult.hookSpecificOutput: permissionDecisionReason
- WatchTarget: rescanIntervalMs (set in config, never read)
- ShutdownResult: confirmedStopped (write-only — assigned but no
  reader; updated all 3 return sites to drop it)
- ModePrompts: language_instruction (multilingual support never wired)

Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>

* chore(npx-cli): reuse InstallOptions type instead of inline duplicate

parseInstallOptions had its return type written out inline as an
anonymous duplicate of InstallOptions. Use the canonical type
(import type — zero bundle cost).

Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>

* chore(integrations): drop unused Platform type alias

The detectPlatform() function that returned this type was deleted earlier
in the branch (along with getScriptExtension that consumed it). The type
itself outlived its consumer; only string literals "Platform:" survive in
console.log diagnostics, which don't reference the alias.

Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>

* fix(worker): broadcast processing_status when summarize is queued

broadcastSummarizeQueued was an empty no-op even though
handleSummarizeByClaudeId calls it after enqueueing. The PendingMessageStore
onMutate callback already fires broadcastProcessingStatus on enqueue, but
calling it explicitly from broadcastSummarizeQueued ensures the spinner
ticks on the moment a summary is requested even if the onMutate chain has
any timing race.

Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>

* fix(worker): keep spinner on while summary generates

ClaudeProvider's SDK can pull multiple synthetic prompts (e.g.
observation + summarize) before producing responses. Each pull pushed
an ID to session.processingMessageIds. When the SDK's first
observation response came back, ResponseProcessor.confirmProcessed
deleted ALL pending message rows — including the still-in-flight
summary — so getTotalQueueDepth dropped to 0 and the spinner turned
off, even though the summary took another ~22s to actually generate.

Tag each in-flight message with its type ({id, type}) so the response
processor can pop only the FIFO message of the matching type
(observation vs summarize). The summary row stays in 'processing'
until its own response arrives, keeping the spinner lit through the
entire summary window.

Also updates Gemini/OpenRouter providers and GeneratorExitHandler for
the new shape.

Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>

* fix(worker): clear summary from queue on any SDK response

Switch ResponseProcessor from type-aware FIFO matching to strict FIFO
popping (each SDK response → 1 in-flight message consumed). This way
the summary always clears when the SDK responds, even when the
response is unparseable or the summary doesn't actually generate
content — preventing stuck spinner / queue-depth-stuck-at-1.

Spinner behavior is preserved: messages enqueued after the summary
keep the queue depth elevated, and only when the SDK has responded
to every prompt does the queue drain to zero.

Also: when the consumed message is a 'summarize' and parsing fails,
treat it as best-effort and confirmProcessed (no retry) — summaries
that can't be parsed shouldn't keep retrying.

Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>

* feat(viewer): redesign welcome card and remove source filters

The first-start welcome card now explains the three feed card types
(observation/summary/prompt) with color-coded badges, points users at
the gear icon for settings and the project dropdown for filtering, and
plugs /mem-search for recall — replacing the old two-line "ask:" prompts.

Source filter tabs (Claude/Codex/etc.) are removed from the header.
Filtering by AI provider was nonsense from a user POV; the project
dropdown is the only header filter now. Source tracking is also
stripped from useSSE, usePagination, App state, and CSS.

Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>

* fix(viewer): keep welcome card in feed column, swap rows for 3 squares

Two visible problems in the previous design: the card stretched
edge-to-edge while feed cards sit in a centered 650px column, and
the body was a stack of long horizontal rows that scanned line-by-line.

Both fixed: Feed now accepts a pinnedTop slot so the welcome card
renders inside the same .feed-content column as observation cards.
Body is now a 3-column grid of square feature blocks — Live feed,
Tune it, Recall it — each with a custom inline SVG illustration
(stacked cards with color-coded stripes, gear+sliders, magnifier
over cards). Old text-row sections (welcome-card-types,
welcome-card-tips, welcome-card-section, welcome-card-tip-icon)
are removed. Squares stack to one column under 600px.

Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>

* feat(viewer): convert welcome card to glassy modal with stylized logo

Card now opens as a centered modal with a frosted/glass backdrop
(blur + saturate) so it doubles as a proper help dialog when reopened
from the header's question-mark button. Removed the observation count,
project count, and "since" date — those don't make sense for a
first-launch surface and felt out of place in a help context.

Header art swapped from the small webp logomark to the new
high-resolution sun/sunburst PNG (claude-mem-logo-stylized.png),
shipped as a checked-in asset in src/ui and plugin/ui.

Bigger throughout: 28px h2, 16px tagline, 88px illustrations,
26px feature padding, 1:1 aspect-ratio squares. Backdrop click and
Esc both close. Mobile collapses the grid to one column and drops
the aspect-ratio constraint.

Reverted the unused pinnedTop slot on Feed.tsx since the welcome
card is now a true overlay rather than an in-feed pinned card.

Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>

* fix(viewer): make welcome modal actually glassy

Previous version had a 55%-opacity black backdrop that almost fully
blocked the underlying UI — the "glass" was just a dark plate.

Now the backdrop is fully transparent (no darkening at all), the
panel itself drops to 55% bg-card opacity with its existing
backdrop-filter blur(28px) saturate(170%), and the feature squares
drop to 35% bg-tertiary so they layer as glass-on-glass over the
already-blurred panel. The header and feed below now read clearly
through the modal's frosted blur.

Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>

* fix(viewer): bulletproof square features via padding-bottom + clamp() fluid type

Squares were rendering taller than wide because aspect-ratio is treated
as a minimum — content can push the box past 1:1. Switched to the
classic padding-bottom: 100% trick: percentage padding resolves against
the parent's width, so the box is ALWAYS W × W regardless of content.
Inner content sits in an absolutely-positioned flex column that can't
push the shell taller.

Whole modal is now desktop-first and fluid via clamp() — no media-query
stair-steps for type, padding, gaps, border-radius, illustration size,
or modal width. Single mobile breakpoint at <600px collapses the grid
to one column and reverts the padding-bottom trick so each feature can
grow to natural content height.

Tightened the three feature descriptions so they fit comfortably inside
the square at the desktop size.

Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>

* style(viewer): 15% black overlay + heavier modal shadow for elevation

Backdrop goes from transparent to rgba(0,0,0,0.15) — just enough
darkening to push the modal visually forward without burying the
underlying UI. Modal shadow stacked: 40px/120px ambient + 16px/48px
contact, both deeper, plus the existing inset 1px highlight.

Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>

* fix(build): clear pending_messages queue on build-and-sync

Rewrites scripts/clear-failed-queue.ts to talk directly to SQLite via
bun:sqlite — the previous HTTP endpoints (/api/pending-queue/*) were
removed during the queue engine rewrite, so the script was orphaned.
Wires `npm run queue:clear` into `build-and-sync` so each rebuild
starts with a clean queue.

Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>

* refactor(worker): collapse parser to binary valid/invalid + clearPendingForSession model

- Parser: { valid: true, observations, summary } | { valid: false } — drops kind/skipped enum dispatch
- ResponseProcessor: two branches only (parseable → store + clearPendingForSession; else → no-op)
- Drop processingMessageIds + per-message claim/confirm/markFailed lifecycle across 3 providers
- PendingMessageStore: 226 → 140 lines; remove markFailed/transitionMessagesTo/confirmProcessed/clearFailedOlderThan/getAllPending/peekPendingTypes... wait keep peekPendingTypes
- Schema migration v31+v32: drop retry_count, failed_at_epoch, completed_at_epoch, worker_pid columns
- SessionQueueProcessor: delete two 1s recovery sleeps (let iterator end on error)
- Server.ts/SettingsRoutes.ts: replace four magic-number setTimeout exit-flush patterns with flushResponseThen helper
- GeneratorExitHandler: 183 → 117 lines (drain in-flight loop gone)

Net: -181 lines. No more silent data loss via maxRetries=3.

Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>

* fix(pr-2255): address review comments batch 1

- install.ts: needsMarketplace true when claude-code selected (P1, was no-op)
- install.ts: throw on invalid --model so CLI exits non-zero
- install.ts: skip worker health checks + adapt next-step copy when --no-auto-start
- install.ts: repair regenerates plugin cache when missing
- index.ts: readFlag rejects missing/flag-shaped values
- index.ts: route flag-first invocations (e.g. `--provider claude`) to install
- banner.ts: fail-open if frame payload decode throws
- SearchRoutes.ts: 5s TTL cache for settings reads on hot hook path (P2)
- detect-error-handling-antipatterns.ts: trailing-brace strip whitespace-tolerant
- investigate-timestamps.ts: compute Dec 2025 epochs at runtime (was Dec 2024)
- regenerate-claude-md.ts: include workingDir in fallback walker so root is covered
- sync-marketplace.cjs: parseWorkerPort validates 1..65535 before http.request
- sync-to-marketplace.sh: resolve SOURCE_DIR from script location, not cwd
- Dockerfile.test-installer: bash --login sources .bashrc via .bash_profile
- docs/configuration.mdx: drop nonexistent .worker.port file refs, use settings.json
- docs/architecture-overview.md: dynamic port + queue model after parser collapse
- docs/architecture/worker-service.mdx: dynamic port example + drop port-file claim
- docs/platform-integration.mdx: WORKER_BASE_URL pattern, drop hardcoded 37777
- install/public/install.sh: Node 20 floor (was 18) to match docs

Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>

* fix(pr-2255): reset claimed messages to pending on early-return paths

ResponseProcessor returns early in two cases:
- parser invalid (unparseable response)
- memorySessionId not yet captured

Both paths previously left the just-claimed message in `status='processing'`,
which counts toward `getPendingCount`. The generator-exit handler then sees
`pendingCount > 0` and respawns the generator, looping until the restart
guard trips and `clearPendingForSession` deletes the message — silent data
loss.

Calling `resetProcessingToPending` on these paths lets the next generator
pass re-claim the message and try again, instead of burning the restart
budget on no-op respawns.

Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>

* fix(pr-2255): swebench fallback row + troubleshooting port path

- evals/swebench/run-batch.py: append fallback prediction row when
  orchestrator future raises, preserving "never drop an instance" guarantee
- docs/troubleshooting.mdx: drop nonexistent .worker.port / worker.port file
  references; use settings.json + /api/health for port discovery

Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>

* fix(pr-2255): memoize per-project observation count for welcome-hint hot path

handleContextInject runs on every PostToolUse hook (after every Read/Edit).
The welcome-hint block ran a COUNT(*) on observations for every call once
CLAUDE_MEM_WELCOME_HINT_ENABLED was true. Observation counts are
monotonically increasing — once a project has any observations it always
will — so cache the positive result in a Set and skip the COUNT(*) on
subsequent requests.

Combined with the 5s settings TTL added earlier, the steady-state cost on
the hook hot path drops to a Set lookup.

Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>

* fix(pr-2255): use clearProcessingForSession on AI-success path

clearPendingForSession deletes ALL rows for the session. On the success
path of processAgentResponse, that's wrong: messages that arrived as
'pending' during the (1-5s) AI response latency get deleted along with
the 'processing' row we just consumed. In a hook burst (three quick
PostToolUse hooks), B and C land while A is in flight; A's success then
nukes B and C — silent data loss.

Add a status-scoped clearProcessingForSession to PendingMessageStore +
SessionManager, and use it in ResponseProcessor's success path. The
unconditional clearPendingForSession remains correct in
GeneratorExitHandler for hard-stop / restart-guard-trip paths.

Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>

* Revert "fix(pr-2255): use clearProcessingForSession on AI-success path"

This reverts commit a08995299c.

---------

Co-authored-by: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
2026-05-02 16:05:56 -07:00
Alex Newman
d13662d5d8 Cynical deletion: close 27 issues by removing defenders + tolerators (#2141)
* fix: mirror migration 28 in SessionStore so pending_messages.tool_use_id and worker_pid columns are created (#2139)

SessionStore's inline migration list jumped from v27 to v29, skipping
rebuildPendingMessagesForSelfHealingClaim. The worker uses SessionStore
directly via worker/DatabaseManager.ts and bypasses the canonical
MigrationRunner, so fresh installs ended up at "max v29" with neither
column present — every queue claim and observation insert failed.

Adds addPendingMessagesToolUseIdAndWorkerPidColumns following the existing
mirror precedent (addObservationSubagentColumns / addObservationsUniqueContentHashIndex).
Uses ALTER TABLE + column-existence guards so already-broken DBs at v29
self-heal on next worker boot.

Verified on fresh DB and on a synthetic v29-without-v28 broken DB:
both columns and indexes (idx_pending_messages_worker_pid,
ux_pending_session_tool) appear after one boot.

Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>

* fix: wrap v28 mirror dedup+index creation in transaction

Addresses Greptile P2 review on PR #2140: matches the existing pattern in
addObservationsUniqueContentHashIndex (v29 mirror at SessionStore.ts:1127)
and runner.ts rebuildPendingMessagesForSelfHealingClaim. A crash between
the dedup DELETE and the schema_versions INSERT no longer leaves the DB
in a half-applied state.

Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>

* docs(plan): cynical-deletion plan for 29 open issues

9-phase plan applying delete-first lens to triaged issue corpus.
Headlines: kill defenders (orphan cleanup, EncodedCommand spawn,
restart-port-steal) and tolerators (silent JSON drops, drifted SSE
filters). Each phase closes a named subset of issues.

Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>

* fix: delete process-management theater (Phase 1: DEL-1 + DEL-2)

Delete aggressiveStartupCleanup, the PowerShell -EncodedCommand
spawn branch, and the restart-with-port-steal sequence. Replace
daemon spawning with a single uniform child_process.spawn path
using arg-array form, keeping setsid on Unix when available.

The defenders (orphan cleanup, duplicate-worker probes, port
stealing) bred more bugs than they fixed. PID file with start-time
token already provides correct OS-trust ownership; restart now
requests httpShutdown, waits 5s for the port to free, then exits 1
if it didn't (user resolves). Net -247 lines.

Closes #2090, #2095 (already fixed at session-init.ts:78), #2107,
#2111, #2114, #2117, #2123, #2097, #2135.

Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>

* fix: observer-sessions trust boundary via CLAUDE_MEM_INTERNAL env (Phase 2: DEL-9)

Replace the cwd === OBSERVER_SESSIONS_DIR discriminator (which every
consumer must repeat and inevitably drifts) with a single env-var
trust boundary set once at spawn time in buildIsolatedEnv.

- buildIsolatedEnv now sets CLAUDE_MEM_INTERNAL=1, covering all three
  spawn sites (SDKAgent, KnowledgeAgent.prime, KnowledgeAgent.executeQuery)
- shouldTrackProject checks the env var first (cwd check stays as
  belt-and-braces fallback)
- New shared shouldEmitProjectRow predicate — SSE broadcaster and
  pagination filter share the same predicate so they can never drift
  apart (#2118)
- ObservationBroadcaster filters observer rows from SSE stream
- PaginationHelper hardcoded 'observer-sessions' replaced with
  OBSERVER_SESSIONS_PROJECT const
- project-filter basename match pass — *observer-sessions* now matches
  basename, not just full path (globToRegex's [^/]* can't cross /)
  (#2126 item 1)
- New `claude-mem cleanup [--dry-run]` subcommand wires CleanupV12_4_3
  through to the worker for #2126 item 5

Closes #2118, #2126.

Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>

* fix: strip proxy env vars before spawning worker (Phase 4: CON-1)

User's HTTP_PROXY/HTTPS_PROXY config was bleeding into internal AI
calls when claude-mem spawns the claude subprocess, causing
connection failures. Strip unconditionally — no passthrough knob,
which rejects #2099's whitelist proposal.

Closes #2115, #2099.

Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>

* fix: fail-fast on silent drops in stdin/file-context/memory-save (Phase 5: FF-1)

Three independent fail-fast fixes:

#2089 — stdin-reader silent drop. Non-empty stdin that fails JSON.parse
now rejects with a clear error instead of resolving undefined. Empty
stdin still resolves undefined.

#2094 — PreToolUse:Read truncation Edit deadlock. file-context handler
no longer returns a fake truncated Read result via updatedInput.
Removes userOffset/userLimit/truncated machinery; injects the timeline
via additionalContext only and lets the real Read pass through. Read
state and Claude's expectation now stay consistent, eliminating the
infinite Edit retry loop.

#2116 — /api/memory/save metadata drop + project bug. Schema accepts
metadata as a documented JSON column (migration 30 adds observations.
metadata TEXT, mirrored in SessionStore). Schema also tightened to
.strict() so unknown top-level fields fail fast instead of being
silently dropped. Project resolution now consults metadata.project as
a fallback before defaultProject.

Closes #2089, #2094, #2116.

Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>

* fix: small deletions — Zod externalize / Gemini fallback / session timeout / installCLI alias (Phase 6)

DEL-4 (#2113): Externalize zod from mcp-server.cjs and context-generator.cjs
hook bundles so OpenCode's runtime resolves a single Zod copy. Worker
keeps Zod bundled (it's a daemon subprocess, not in OpenCode's hook
bundle). Added zod to plugin/package.json so externalized requires
resolve at runtime.

DEL-5 (#2087): Delete the never-wired GeminiAgent → Claude fallback.
fallbackAgent was always null in production. On 429 the agent now
throws cleanly (message stays pending for retry). Removed
setFallbackAgent, FallbackAgent interface, and the 429 fallback
branch from both GeminiAgent and OpenRouterAgent. Updated docs
that claimed automatic Claude fallback.

DEL-6 (#2127, #2098): Raise MAX_SESSION_WALL_CLOCK_MS from 4h to
24h. The timeout is a real guard against runaway-cost loops (per
issue #1590), but 4h kills legitimate long Claude Code days. 24h
preserves the guard while never hitting in normal use. No knob —
a session approaching this age is a bug worth investigating, not
a value worth tuning.

DEL-8 (#2054): Delete installCLI() alias function. Saves 4 keystrokes
at the cost of cross-platform shell-config mutation surface — not
worth it. Canonical entry is npx claude-mem (and bunx). Uninstall
now strips legacy alias/function lines from ~/.bashrc, ~/.zshrc,
and the PowerShell profile.

Closes #2087, #2098, #2113, #2127, #2054.

Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>

* fix: de-hardcode worker port + multi-account commit (Phase 3: CON-2 + DEL-7)

Replace hardcoded 37777 fallbacks with SettingsDefaultsManager.get(
'CLAUDE_MEM_WORKER_PORT') in npx-cli (runtime/install/uninstall),
opencode-plugin, OpenClaw installer, SearchRoutes example URLs.
Timeline-report SKILL.md now resolves WORKER_PORT from settings.json
at the top and uses ${WORKER_PORT} in all curl invocations.
Remaining 37777 literals are doc comments + viewer build-time form-
field placeholder (which is replaced by /api/settings on mount).

hooks.json: add cygpath POSIX→Windows path translation between _R
resolution and node invocation. No-op on macOS/Linux. Closes the
Windows + Git Bash MODULE_NOT_FOUND in #2109.

CLAUDE.md gains a Multi-account section documenting CLAUDE_MEM_DATA_DIR
+ optional CLAUDE_MEM_WORKER_PORT — every existing path/port code
path now honors them.

Closes #2103, #2109, #2101.

Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>

* fix: install/uninstall improvements (Phase 7: #2106)

5 fixes for the install/uninstall flow:

Item 1 — multiselect default. install.ts no longer pre-selects every
detected IDE; user explicitly opts in.

Item 3 — shutdown-before-overwrite. New
src/services/install/shutdown-helper.ts shared by install and
uninstall: POSTs /api/admin/shutdown then polls /api/health until
the worker stops responding. install calls it before
copyPluginToMarketplace so reinstall over a running worker doesn't
conflict; uninstall calls it before deletion.

Item 4 — uninstall path coverage. Removes ~/.npm/_npx/*/node_modules/
claude-mem, ~/.cache/claude-cli-nodejs/*/mcp-logs-plugin-claude-mem-*,
~/.claude/plugins/data/claude-mem-thedotmack/. Best-effort: per-path
try/catch so a single permission failure doesn't abort uninstall.
chroma-mcp shutdown is implicit via the worker's GracefulShutdown
cascade in item 3's helper.

Item 5 — install summary documents "Close all Claude Code sessions
before uninstalling, or ~/.claude-mem will be recreated by active
hooks."

Item 6 — real-port query. After install, fetches /api/health on the
configured port with 3s timeout. Reports actually-bound port if the
response carries it; falls back to requested port. No retry loop.

Closes #2106 (items 1, 3, 4, 5, 6). Items 2, 7 closed separately
as already-fixed and insufficient-detail.

Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>

* fix: pin chroma-mcp to 0.2.6 (Phase 8: DEL-3 lite)

Replace unpinned 'chroma-mcp' arg with chroma-mcp==0.2.6 in both
local and remote modes. Pinning makes installs deterministic across
machines and across time, eliminating the dependency-drift class
of bugs.

Verified 0.2.6 in a clean uv cache: starts cleanly, no httpcore/
httpx ImportError, no --with flags needed. The --with flags removed
in a0dd516c are not required at this pin (transitive deps resolve
correctly when the top-level version is fixed).

#2102's three protections (transport cleanup on failure, stale onclose
handler guard, 10s reconnect backoff) confirmed intact.

Closes #2046, #2085, #2102.

Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>

* test: update stale assertions for per-UID port + migration 30 (Phase 9)

SettingsDefaultsManager.CLAUDE_MEM_WORKER_PORT default is per-UID
(37700 + uid%100), not literal '37777'. Three assertions in
settings-defaults-manager.test.ts now compute the expected value
the same way the source does.

migration-runner.test.ts: drop expect(versions).toContain(19)
(version 19 was a noop never recorded — pre-existing bug at parent),
add expect(versions).toContain(30) for the new observations.metadata
column added in Phase 5.

Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>

* fix: address Greptile P1/P2 review comments on PR #2141

P1: spawnDaemon return value was unchecked in worker-service.ts restart
case, so a failed spawn silently exited 0 with a misleading "Worker
restart spawned" log. Now error and exit 1 when restartPid is undefined.

P2: shutdown-helper.ts health-poll catch treated AbortError (timeout)
the same as connection-refused, so a slow worker could be reported
confirmedStopped while still holding file locks. Now distinguish:
AbortError continues polling; other errors return confirmedStopped.

Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>

* build: rebuild plugin artifacts after merging main

Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>

* fix: address CodeRabbit review comments on PR #2141

- hooks.json: quote $HOME in cache lookup so paths with spaces work
- timeline-report SKILL.md: fall back when process.getuid is unavailable (Windows)
- opencode-plugin: validate CLAUDE_MEM_WORKER_PORT before using
- uninstall.ts: only strip alias lines, not function declarations (multi-line bodies left intact)
- MemoryRoutes: trim whitespace-only project before precedence resolution
- SessionStore migration 21: preserve metadata column if observations already has it
- stdin-reader test: restore full property descriptor to avoid cross-test pollution

Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>

---------

Co-authored-by: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
2026-04-25 21:23:24 -07:00
Alex Newman
9d695f53ed chore: remove auto-generated per-directory CLAUDE.md files
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>
2026-04-16 17:51:24 -07:00
Alex Newman
c648d5d8d2 feat: Knowledge Agents — queryable corpora from claude-mem (#1653)
* feat: add knowledge agent types, store, builder, and renderer

Phase 1 of Knowledge Agents feature. Introduces corpus compilation
pipeline that filters observations from the database into portable
corpus files stored at ~/.claude-mem/corpora/.

Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>

* feat: add corpus CRUD HTTP endpoints and wire into worker service

Phase 2 of Knowledge Agents. Adds CorpusRoutes with 5 endpoints
(build, list, get, delete, rebuild) and registers them during
worker background initialization alongside SearchRoutes.

Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>

* feat: add KnowledgeAgent with V1 SDK prime/query/reprime

Phase 3 of Knowledge Agents. Uses Agent SDK V1 query() with
resume and disallowedTools for Q&A-only knowledge sessions.
Auto-reprimes on session expiry. Adds prime, query, and reprime
HTTP endpoints to CorpusRoutes.

Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>

* feat: add MCP tools and skill for knowledge agents

Phase 4 of Knowledge Agents. Adds build_corpus, list_corpora,
prime_corpus, and query_corpus MCP tools delegating to worker
HTTP endpoints. Includes /knowledge-agent skill with workflow docs.

Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>

* fix: handle SDK process exit in KnowledgeAgent, add e2e test

The Agent SDK may throw after yielding all messages when the
Claude process exits with a non-zero code. Now tolerates this
if session_id/answer were already captured. Adds comprehensive
e2e test script (31 assertions) orchestrated via tmux-cli.

Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>

* fix: use settings model ID instead of hardcoded model in KnowledgeAgent

Reads CLAUDE_MEM_MODEL from user settings via getModelId(), matching
the existing SDKAgent pattern. No more hardcoded model assumptions.

Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>

* feat: improve knowledge agents developer experience

Add public documentation page, rebuild/reprime MCP tools, and actionable
error messages. DX review scored knowledge agents 4/10 — core engineering
works (31/31 e2e) but the feature was invisible. This addresses
discoverability (docs, cross-links), API completeness (missing MCP tools),
and error quality (fix/example fields in error responses).

Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>

* docs: add quick start guide to knowledge agents page

Covers the three main use cases upfront: creating an agent, asking a
single question, and starting a fresh conversation with reprime. Includes
keeping-it-current section for rebuild + reprime workflow.

Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>

* fix: address code review issues — path traversal, session safety, prompt injection

- Block path traversal in CorpusStore with alphanumeric name validation and resolved path check
- Harden system prompt against instruction injection from untrusted corpus content
- Validate question field as non-empty string in query endpoint
- Only persist session_id after successful prime (not null on failure)
- Persist refreshed session_id after query execution
- Only auto-reprime on session resume errors, not all query failures
- Add fenced code block language tags to SKILL.md

Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>

* fix: address remaining code review issues — e2e robustness, MCP validation, docs

- Harden e2e curl wrappers with connect-timeout, fallback to HTTP 000 on transport failure
- Use curl_post wrapper consistently for all long-running POST calls
- Add runtime name validation to all corpus MCP tool handlers
- Fix docs: soften hallucination guarantee to probabilistic claim
- Fix architecture diagram: add missing rebuild_corpus and reprime_corpus tools

Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>

* fix: enforce string[] type in safeParseJsonArray for corpus data integrity

Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>

* fix: add blank line before fenced code blocks in SKILL.md maintenance section

Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>

---------

Co-authored-by: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
2026-04-08 17:30:20 -07:00
Alex Newman
c21e49d9fa fix: address PR review comments and add file read gate docs
Fix indentation bugs flagged in PR review (SettingsDefaultsManager,
MigrationRunner), add current date/time to file read gate timeline
so the model can judge observation recency, and add documentation
for the file read gate feature.

Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
2026-04-07 13:09:46 -07:00
Alex Newman
842d614adb Merge branch 'pr-1550' into integration/validation-batch 2026-04-06 14:18:28 -07:00
Alex Newman
4509da1409 Merge branch 'pr-1588' into integration/validation-batch 2026-04-06 14:18:28 -07:00
Alex Newman
811c94da36 fix: correct content hash description, update merged PR references, fix ChromaSync desc 2026-04-04 15:20:30 -07:00
Alessandro Costa
af6bfda2d8 fix: address CodeRabbit review on PR #1574
- architecture-overview: add 'text' language to all fenced code blocks (MD040)
- architecture-overview: split 'Stop' lifecycle into 'Summary' + 'SessionEnd'
  to match canonical 5-hook naming
- production-guide: reference PR numbers for proposed settings not yet upstream

Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
2026-04-04 15:19:52 -07:00
Alessandro Costa
bf8b7dbd9f docs: add architecture overview and production guide
Architecture overview covers the 4-layer system design, hook lifecycle,
data flow, and key patterns (CLAIM-CONFIRM, circuit-breaker, graceful
degradation, deduplication, dual session IDs).

Production guide provides recommended settings, health monitoring
metrics and thresholds, quick health check commands, multi-machine
sync setup, growth expectations, common issues with solutions, and
log analysis tips.

Based on 23 days of production usage with 3,400+ observations
across two physical servers and 8 projects.

Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
2026-04-04 15:19:52 -07:00
Alex Newman
76a880a3d6 feat: update install CLI, ESM compat, and Gemini CLI docs
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>
2026-04-03 12:38:45 -07:00