Commit Graph

2511 Commits

Author SHA1 Message Date
Alex Newman 263d8c0083 chore: bump version to 13.23.0
Co-Authored-By: Claude Fable 5.1 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_019NoBPqpo7YaR3HKipExjAT
v13.23.0
2026-09-01 13:27:31 -07:00
Alex Newman 04989feb4b feat(telemetry): report subscription usage-limit hits to PostHog (#3837)
Nothing told PostHog when a user ran out of Claude Code usage. The only
quota signal was abort_reason:quota on the observer rollup, which fires
when claude-mem's own guard stops early, not when the user's session is
blocked. The Stop hook never fires on a limit-hit turn, so the transcript
cannot carry it either.

The observer runs on the same account as the observed session, so the
SDK's rate_limit stream reporting a window as `rejected` is the moment
the user's own session ran out. ClaudeProvider now captures a
usage_limit_hit event there with limit_window, overage_status,
is_using_overage, and resets_in_minutes (closed enums, a boolean, and an
integer; never the provider's message text), plus the existing ide,
provider, observed_model, and observed_billing fields.

RateLimitStore.set returns true only for a fresh rejection, so a window
that stays rejected across many observer requests emits once, and emits
again after a reset or an allowed snapshot in between. resetsAt is
accepted as epoch seconds or milliseconds; Claude Code writes seconds in
transcripts while the SDK documents milliseconds.

Also widens isQuotaLimitedObserverOutput to the wordings Claude Code
actually writes ("You've hit your session limit", "You've reached your
… limit", "You're out of usage credits"). Those turns were classified as
prose and the batch dropped; they now pause the generator and preserve
the queued work like every other quota refusal. The detector also skips
XML like its two siblings.


Claude-Session: https://claude.ai/code/session_019NoBPqpo7YaR3HKipExjAT

Co-authored-by: Claude Fable 5.1 <noreply@anthropic.com>
2026-09-01 13:25:01 -07:00
Alex Newman 1dc3487665 docs: update changelog for v13.22.0
Co-Authored-By: Claude Fable 5.1 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_019NoBPqpo7YaR3HKipExjAT
2026-09-01 12:12:05 -07:00
Alex Newman 76088262c3 chore: bump version to 13.22.0
Co-Authored-By: Claude Fable 5.1 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_019NoBPqpo7YaR3HKipExjAT
v13.22.0
2026-09-01 12:11:29 -07:00
Alex Newman 28c84255fe feat(telemetry): report observed model, source, and billing tier to PostHog (#3836)
PostHog only knew the observer model (the model claude-mem uses to write
observations). It now also carries, per observed session:

- observed_model: the model the user's IDE session is running, read from the
  transcript's last assistant entry on every Stop hook
- observed_billing: closed enum (max | pro | team | enterprise | subscription |
  api_key | bedrock | vertex | foundry | unknown) detected in the hook process
  from Claude Code's env and ~/.claude.json oauthAccount.organizationType
- ide and provider are now actually carried on observer_turn_rollup (the docs
  already claimed this; the rollup was dropping them)

The Stop hook sends both fields to /api/sessions/summarize; the worker persists
them on sdk_sessions (observed_model, observed_billing; schema version 50) and
mirrors them onto the in-memory ActiveSession so every per-turn
session_compressed record carries them into the rollup (last-seen semantics,
'unknown' when never learned).

Privacy: only oauthAccount.organizationType, oauthAccount presence, and
customApiKeyResponses.approved are read from .claude.json, projected
immediately after parse; parse errors log only the error class name. Both new
keys are added to the scrub whitelist and documented in telemetry.mdx.

Co-Authored-By: Claude Fable 5.1 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_019NoBPqpo7YaR3HKipExjAT
2026-09-01 12:09:53 -07:00
Alex Newman 8f085b4f88 docs: update changelog for v13.21.2
Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01DD5YMaAiXXeY3zaitbGC6n
2026-08-31 14:37:53 -07:00
Alex Newman 2aeedb42ac chore: bump version to 13.21.2
Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01DD5YMaAiXXeY3zaitbGC6n
v13.21.2
2026-08-31 14:37:27 -07:00
Alex Newman a9aaeb9db0 fix(installer): pre-select CMEM Pro and Claude Code (#3825)
Both prompts opened with nothing checked, making the recommended path a
required chore before the install could continue.

CMEM Pro is now pre-selected on the provider prompt — it is the path the
funnel is built around, and selecting it no longer means "pay now": it
opens the offer page to read first (claude-mem-pro ea8a4d2).

The IDE prompt pre-checks everything detected, plus Claude Code, which
is what essentially everyone installing claude-mem is running.

Both remain multiselects with required: true, so a user who wants
something else just unchecks and picks.


Claude-Session: https://claude.ai/code/session_01DD5YMaAiXXeY3zaitbGC6n

Co-authored-by: Claude Opus 5 (1M context) <noreply@anthropic.com>
2026-08-31 14:35:42 -07:00
Alex Newman 8b2abd1878 docs: update changelog for v13.21.1
Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01DD5YMaAiXXeY3zaitbGC6n
2026-08-31 14:28:44 -07:00
Alex Newman e9c52a3537 chore: bump version to 13.21.1
Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01DD5YMaAiXXeY3zaitbGC6n
v13.21.1
2026-08-31 14:28:16 -07:00
Alex Newman 2948cb6e68 fix(promo): advertise the 30-day trial everywhere (#3824)
Every client-side promo link now carries `?trial=30` and every surface
says "30 days" instead of "7". PRO_TRIAL_DAYS is the single knob in
src/shared/pro-promo.ts, mirrored in the viewer's own copy because its
tsconfig pins rootDir and cannot import the shared module.

Also restores the trial link in the installer's Next Steps block. The
'installer' promo source was declared in ProPromoSource but had no
caller after the Next Steps trim, so the last screen of the funnel
never mentioned the offer. It now shows for non-Pro installs only.


Claude-Session: https://claude.ai/code/session_01DD5YMaAiXXeY3zaitbGC6n

Co-authored-by: Claude Opus 5 (1M context) <noreply@anthropic.com>
2026-08-31 14:26:02 -07:00
Alex Newman 55a322d379 docs: update changelog for v13.21.0
Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01DD5YMaAiXXeY3zaitbGC6n
2026-08-31 14:05:47 -07:00
Alex Newman fab75dc3b9 chore: bump version to 13.21.0
Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01DD5YMaAiXXeY3zaitbGC6n
v13.21.0
2026-08-31 14:05:20 -07:00
Alex Newman 04c6297065 Merge pull request #3823 from thedotmack/fix/local-provider-skips-login
fix(installer): let --provider claude skip the login requirement
2026-08-31 14:03:32 -07:00
Alex Newman 7ac207ebeb fix(installer): tighten the funnel copy and un-pin the trial length
Provider screen is now the two-line choice it should have been. Each
description rides in the LABEL, not clack's hint: multiselect renders a
hint only for the focused or selected row, so descriptions placed there
would appear one at a time instead of side by side. The nine-bullet
'Provider benefits' note above it is gone — the labels carry the value
proposition now.

The billing acknowledgement no longer runs in the terminal. It is a
term of the charge, so it belongs on the screen that takes the card,
where the user can see what they are agreeing to; asking first made
them consent twice to the same thing.

The checkout hand-off no longer waits for Return. Two waits in one flow
stalled the install outright: stdin had already been through the login
wait and a clack prompt, and the listener did not reliably get the
keypress, so the run stopped before printing 'Waiting for CMEM Pro
setup in the browser'. The login hand-off keeps its wait.

The trial length is no longer pinned to exactly 7 in the checkout URL.
That made the server unable to change its own offer without breaking
every published installer. The shape stays strict — both params, no
extras, pairing must match — but the number is the server's to choose.

Next Steps drops the WELCOME_HINT_ENABLED opt-out (for a hint the user
has not seen yet), the uninstall warning (uninstall trivia on the
install screen), and the A/B framing that dressed up 'just start
working' as a decision.

Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01DD5YMaAiXXeY3zaitbGC6n
2026-08-31 13:56:02 -07:00
Alex Newman b412bb2216 fix(installer): let --provider claude skip the login requirement
Login ran unconditionally before the provider choice, so a cmem.ai
outage failed installs that never needed an account. That is not
hypothetical: 13.20.0 shipped while /api/installer/oauth/start was
still undeployed, and every install died on 'Could not start OAuth
login' — including local-only ones.

--provider claude configures memory against the user's own Anthropic
plan and never contacts cmem.ai, so there is no account question left
for login to answer.

Keyed on the explicit flag, not on reachability. Falling back to a
local install whenever cmem.ai happened to be down would silently
change what the user gets; this only skips a step the user's own flag
already made moot. gemini and openrouter stay gated — openrouter is the
transport for the cmem gateway, so such an install may still be
reaching cmem.ai.

promptProvider now takes a nullable pairing and re-checks it before
enrolling in CMEM Pro rather than assuming it is present.

Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01DD5YMaAiXXeY3zaitbGC6n
2026-08-31 13:43:06 -07:00
Alex Newman 5a504da0fc docs: update changelog for v13.20.0
Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01DD5YMaAiXXeY3zaitbGC6n
2026-08-31 13:24:30 -07:00
Alex Newman e3ecf0f7ea Merge pull request #3821 from thedotmack/consolidate/observer-quota-oauth
fix(observer): consolidate #3807 + #3820 + #3819, and close the two defects where they meet
v13.20.0
2026-08-31 13:23:10 -07:00
Alex Newman 8545e52207 chore: bump version to 13.20.0
Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01DD5YMaAiXXeY3zaitbGC6n
2026-08-31 13:15:42 -07:00
Alex Newman 679a14e555 chore: rebuild worker bundle after merging main
The merge resolved plugin/scripts/*.cjs by regenerating them from the
merged source rather than hand-merging minified output.

Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01DD5YMaAiXXeY3zaitbGC6n
2026-08-31 13:14:21 -07:00
Alex Newman dac252674d Merge remote-tracking branch 'origin/main' into consolidate/observer-quota-oauth
# Conflicts:
#	plugin/scripts/mcp-server.cjs
#	plugin/scripts/server-service.cjs
#	plugin/scripts/transcript-watcher.cjs
#	plugin/scripts/worker-service.cjs
2026-08-31 13:11:39 -07:00
Alex Newman 99c61b21ad fix(installer): give the checkout hand-off the same return-to-open wait
The login step now waits for Return before opening the browser, but the
CMEM Pro checkout step still auto-opened, so the two browser hand-offs
behaved differently in one flow.

Worth the extra keystroke here: noteDeviceCode prints the pairing code
immediately above this, and that code has to be read before the browser
takes focus.

The two stdin listeners never overlap — waitForReturnToOpenBrowser
detaches its handler and restores raw mode before returning, and
waitForInstallerPairing attaches its own afterwards.

Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01DD5YMaAiXXeY3zaitbGC6n
2026-08-31 13:04:23 -07:00
Alex Newman b685ccfab9 fix(installer): keep the login step about logging in
Logging in is required of every install and happens before the provider
choice, so naming a paid plan there attaches an upsell to a mandatory
step and blurs the funnel.

Two places leaked CMEM Pro into the login phase:

- The poll loop prints whatever stage the server reports, so a
  server-side `awaiting_checkout` during login rendered "Waiting for
  CMEM Pro setup in the browser…". The checkout wording is now scoped
  to the enrollment phase; login gets neutral copy.

- The pre-login note explained provider mechanics before the user had
  chosen one. Replaced with a single line at the moment it is actually
  needed: "Continue setup in browser... (hit return to open
  automatically)".

That line now tells the truth. The browser previously opened on its
own, so `waitForReturnToOpenBrowser` holds until Return. The URL is
still printed first, so a headless or SSH session is never blocked —
open it by hand and the wait clears on Return. Raw-mode handling
mirrors waitForInstallerPairing: toggled only when this call turned it
on, and always restored.

Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01DD5YMaAiXXeY3zaitbGC6n
2026-08-31 13:01:58 -07:00
Alex Newman b16a2a8bf2 Merge pull request #3822 from thedotmack/changelog-v13.19.0
docs: update changelog for v13.19.0
2026-08-31 12:45:23 -07:00
Alex Newman 39107698d5 docs: update changelog for v13.19.0
Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
2026-08-31 12:45:10 -07:00
Alex Newman c4601b4770 Merge pull request #3820 from thedotmack/mem-error-restart-first
fix(observer): click-to-restart link in the outage warning, shown in red below context
v13.19.0
2026-08-31 12:44:30 -07:00
Alex Newman a5025b8a88 chore: bump version to 13.19.0
Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
2026-08-31 12:38:12 -07:00
Alex Newman 60310f86bf fix(quota): claim the cmem gateway re-probe instead of reading the clock
The gateway fallback said "allow one normal gateway request as a probe", but
nothing made it one. shouldUseCmemFallback is a subtraction and a less-than,
and ensureGeneratorRunning gates per session, so 28-69 live sessions are 28-69
independent callers.

It is worse than the in-memory case. The marker lives on disk
(CLAUDE_MEM_PRO_FALLBACK_AT in settings.json, loaded uncached), so every
process parses the same ISO string and computes the same expiry instant -- the
herd was handed a shared clock to synchronize on. Each member's failure then
does a read-modify-write of the user's whole settings.json to re-arm the
marker; the write is atomic but the sequence is not, so concurrent settings
edits can be clobbered.

The re-probe is now claimed through the breaker's existing machinery under a
DISTINCT key, 'cmem-gateway'. Distinct because tryAdmitQuotaProbe takes the
cooldown per call: this path's 15 minutes against the provider breaker's 30
would otherwise let two callers reach contradictory answers about whether the
same breaker is armed, and provider switches (which this branch makes routine)
make the collision reachable in both directions -- a fresh funded key born into
an armed breaker and withheld for a refusal it never earned.

getSelectedProvider stays read-only for diagnostics and status; only the new
selectProviderForGenerator claims. Every path out of ensureGeneratorRunning
that does not actually send releases the claim, including the one where a
generator is already running, so a claim cannot outlive the call that took it.

Also regenerates the plugin bundles rather than hand-merging them, which is
what the five .cjs conflicts in this consolidation always needed.

Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01AWmJDjFM6BNmGaLJ7Xbrsy
2026-08-31 12:18:30 -07:00
Alex Newman 12cbe1d8ce Merge branch 'fix/oauth-only-installer-pr' into consolidate/observer-quota-oauth
# 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/services/worker/http/routes/SessionRoutes.ts
2026-08-31 12:12:51 -07:00
Alex Newman e82ed621c3 fix(observer): persist the quota breaker, and stop telling capped users to restart
Two defects that only exist where #3807 and #3820 meet, found by having the
branches read each other rather than by either PR alone.

The breaker was a process-local Map, so every restart cleared it. That matters
because the restart is not incidental: worker-service.ts wires onRestart to
this.shutdown, so the process serving /api/admin/restart is the process holding
the Map. #3820 adds a click-to-restart link to the outage warning, which made
the button a one-click bypass of the breaker -- and the warning copy actively
recruited the user to press it. npx claude-mem restart, a reboot, a version
SIGKILL, a crash-respawn and this repo's own build-and-sync clear it too, so
suppressing the link alone would have closed one door of six.

The armed window now persists beside observer-health.json, for the reason that
file's docblock already gives. The probe claim deliberately does NOT persist:
it is single-process concurrency state, a restart kills every generator that
could hold one, and a claim restored from disk would be owned by a dead process
and wedge the provider shut until it went stale -- the opposite failure.
recordQuotaExhausted takes an optional armedAtMs so a revived window is not
restamped to now on every restart.

The warning no longer offers a restart when the allowance is spent. Nothing is
wedged, restarting cannot help, and doing it disarms the backoff protecting the
account. It also drops the "has failed 1 time in a row" framing there, which is
the blip wording the threshold of 3 exists to avoid, on the one condition
guaranteed not to self-heal.

Quota returned as assistant prose never throws, so it never reached the
generator catch and never armed the health ledger at all -- the warning was
structurally blind to that entire outage class. It now records the failure on
that path too.

Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01AWmJDjFM6BNmGaLJ7Xbrsy
2026-08-31 12:11:14 -07:00
Alex Newman 832d4a9c79 fix(restart-page): require successor readiness, not just a bound port
Third greptile P1 on #3820, this one against the previous fix.

A changed pid on /health proves the successor bound HTTP, nothing more.
ViewerRoutes' /health answers as soon as express listens, while
/api/readiness stays 503 until getInitializationComplete() — the successor
opens the database, bootstraps chroma and connects MCP after it starts
serving. The page was telling people they could close the tab while memory
processing was still unavailable.

Success now requires both signals, in order: a different pid on /health (so
we are talking to the successor and not the corpse), then /api/readiness ok.
This is the same signal the CLI restart path verifies. A 404 from readiness
means a worker too old to expose it and does not block, matching the existing
missing-pid fallback.

Deadline 30s -> 60s: readiness legitimately takes longer than binding a port,
and 30s was sized for the weaker check.

Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
2026-08-31 12:09:54 -07:00
Alex Newman ea23f57de1 Merge branch 'mem-error-restart-first' into consolidate/observer-quota-oauth
# Conflicts:
#	plugin/scripts/context-generator.cjs
#	plugin/scripts/worker-service.cjs
2026-08-31 12:04:30 -07:00
Alex Newman 9d5f4082c9 fix(restart-page): block framing and confirm the successor, not the dying worker
Both from greptile review on #3820, both reproduced against a live worker.

Clickjacking: requiring a real click stops a drive-by <img>, but not an
attacker who frames the page and harvests that click through an overlay — the
code comment claimed otherwise. The route now refuses to be framed at all:
Content-Security-Policy: frame-ancestors 'none' plus X-Frame-Options: DENY.

Health poll accepted the dying worker: the page treated any ok /health as
"restarted", but runShutdownSequence waits out a graceful deadline before it
exits and nothing closes the listener first, so the outgoing worker keeps
answering well past the first 500ms poll. Measured across a real restart:

  t=500ms   health OK, pid=6976  <- the worker we just asked to die
  t=1000ms+ no response
  (successor then spawns)

/health now reports pid, the page bakes in the pid of the worker that served
it, and success requires a DIFFERENT pid. A worker too old to report one falls
back to the previous "any healthy response" behavior rather than hanging.

Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
2026-08-31 11:53:35 -07:00
Alex Newman 0d6ae70d60 chore: rebuild plugin bundles across the observer and quota fixes
The rebase conflicted on the built bundle because both commits regenerated it.
Resolved from source rather than by picking a side, so the shipped bundle carries
the session-start briefing, the field condenser and the scoped quota probe
release together.

Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01DC1PFXGZJk7BQ2PNW2foz2
2026-08-31 11:40:24 -07:00
Alex Newman 52447e92b4 docs(plans): zero-arbitrary-limits audit and phased plan
Sweep of src/ found 126 named numeric constants and 11 numeric settings defaults,
eight of which discard captured content, and no notion of a model's input limit
anywhere in the codebase — every size ceiling is disconnected from what the model
can actually accept.

The plan derives one real number (top_provider.context_length from OpenRouter's
public catalogue, which covers the Anthropic models too) and deletes the rest.
Phase 0 records the verified API shape, including the trap that top-level
context_length reports the max across all providers and overstates
anthropic/claude-sonnet-4 by 5x.

Two audit unknowns resolved: CONTENT_BODY_MAX_BYTES is a real cross-repo wire
contract and stays; WINDSURF_CONTEXT_CHAR_LIMIT has no source in this repo and
needs one external check.

Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01DC1PFXGZJk7BQ2PNW2foz2
2026-08-31 11:40:24 -07:00
Alex Newman c26c291422 fix(observer): brief a recycled generation from the real session-start context
The recycle path never briefed its replacement. `loadSessionSoFar` looked up
prior observations by `session.memorySessionId`, but `ClaudeProvider.startSession`
blanks that id on every generator start (ClaudeProvider.ts:216-221) and the
lookup runs lazily inside the async generator body (:502), i.e. after the blanking.
So the guard `if (!session.memorySessionId) return []` was taken every time and
each fresh generation started blind. Instrumented live: 4 recycles, 4 empty seeds.

Use `generateContext` instead — the builder the SessionStart hook already uses.
It keys on project + cwd rather than the SDK session id, so the blanking cannot
reach it. This deletes the parallel `buildSessionSoFar` / `PriorObservation` /
`loadSessionSoFar` path rather than repairing it. Verified live at the default
400k budget: recycle at 404,297 chars, replacement briefed with 1,371 chars,
observations continuing forward across the boundary with no re-recording.

Also:

- The exhausted branch aborted without clearing `conversationHistory` and without
  gating restarts, so the next tool call spawned another generator that failed on
  the same budget check — one spawn and abort per captured tool call, with history
  growing 21k -> 59k chars. It now drops the conversation and withholds restarts
  for a cooldown, mirroring the quota breaker.
- `consecutiveContextOverflows` reset only on a valid XML parse, so two ordinary
  "nothing to record" replies could trip the exhausted branch. Overflow, quota and
  auth all return before that point, so reaching it proves the prompt fit — reset
  there instead.
- Oversized fields are condensed by a bounded model pass rather than head/tail cut
  with the middle deleted. One attempt, 30s timeout, falls back to the existing
  truncation on any failure. Live: 81,521 chars -> 184, preserving the FATAL line,
  the pool max, and a latency progression that only the discarded middle contained.

tests/gemini_provider.test.ts assumed the first request went out within a fixed
number of microtasks; the context lookup adds an await, so it now waits on the
condition instead of the timing.

Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01DC1PFXGZJk7BQ2PNW2foz2
2026-08-31 11:40:24 -07:00
Alex Newman 4745f6250b Merge origin/main (v13.18.1) into claude/unbounded-loops-audit-brd5sm
Conflicts were entirely mechanical — no source-level overlap. main changed no
files under src/ since the merge base.

- Version manifests (8 files): main bumped to 13.18.1 while this branch had
  already bumped to 13.19.0. Kept 13.19.0, the forward version.
- plugin/scripts/*.cjs: both sides had rebuilt the bundles, so they differed
  only by the embedded version string. Regenerated from the merged tree with
  `npm run build`; the output is byte-identical to this branch's, confirming
  the conflict carried no behavior.

#3817's observer_role rules land in plugin/modes/*.json and the mode-authoring
reference, which are runtime data rather than bundled code, so they merged
cleanly and survive the rebuild — verified present after the merge.

Typecheck clean.

Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01Sc5nL8WJRdBscNu9egvDQB
2026-08-31 11:33:06 -07:00
Alex Newman 45620bdaae Merge remote-tracking branch 'origin/main' into mem-error-restart-first
# Conflicts:
#	plugin/scripts/server-service.cjs
#	plugin/scripts/worker-service.cjs
2026-08-31 11:31:33 -07:00
Alex Newman 7a31d02bd4 fix(observer): click-to-restart link in the outage warning, shown in red below context
The observer-outage warning told the user to check settings.json, when a
restart clears nearly every outage (a wedged or SIGKILL'd provider
subprocess). It now leads with the restart, offered two ways:

  Click to restart:  http://localhost:<port>/restart
  Or in a terminal:  npx claude-mem restart

GET /restart is deliberately INERT — it serves a page whose button POSTs to
the existing /api/admin/restart. A GET that restarted would fire from any
page that can name the URL (<img src="http://localhost:PORT/restart">), and
requireLocalhost is no defense because the browser IS localhost. Same reason
the page does not POST on load: an <iframe> would run that script.

Restarting automatically was the other option and it is worse: the worker
restarting itself needs guards against its own automation (once per outage?
per flap? two failures racing?), and every guard is a bound on a loop that
only exists because the restart was automatic. A human pressing the button
is the bound.

Placement and color, both because the warning was getting lost:
- Appended BELOW the context, not prepended. The timeline runs long, so a
  warning at the top has already scrolled off by the time context finishes
  printing.
- Painted red on the human render only; the agent copy is a separate
  colors=false fetch where ANSI escapes are just noise. Every non-blank line
  is painted rather than the block wrapped once, so reflow cannot leave the
  tail unpainted.

Verified against a worker booted from source on an isolated port: 5x
GET /restart left the pid unchanged, POST /api/admin/restart took it from
11406 to 14700 and healthy again in 1s.

Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
2026-08-31 11:24:38 -07:00
Alex Newman 26d61c9c4b docs: update changelog for v13.18.1
Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_017LYx61kB4eAX5kHSz4qqzt
2026-08-31 11:23:14 -07:00
Alex Newman acb652cd12 chore: bump version to 13.18.1
Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_017LYx61kB4eAX5kHSz4qqzt
v13.18.1
2026-08-31 11:22:34 -07:00
Alex Newman 5a87186191 Merge origin/main into OAuth installer fix 2026-08-31 11:22:33 -07:00
Alex Newman f0412b70f0 fix(installer): replace signup links with OAuth pairing 2026-08-31 11:16:37 -07:00
Alex Newman d31efb21fd fix(quota): scope probe release to the generator that claimed it
`releaseQuotaProbe` cleared whatever probe was in flight for the provider,
regardless of which run had claimed it. Generators overlap freely, so an
earlier one could clear a later session's probe:

  1. Session A starts while the provider is healthy. `tryAdmitQuotaProbe`
     admits it without a breaker to claim against, so it owns no probe.
  2. The breaker arms, the cooldown elapses, and session B claims the single
     post-cooldown probe.
  3. Session A's long-running generator exits and unconditionally releases.
  4. Session C is admitted and sends a second request while B's probe is
     still in flight.

The same shape reopened the breaker when a stale claim was taken over, and
when a probe outlived the breaker it was claimed against.

Admission now returns a claim id, threaded through `startGeneratorWithProvider`
to both exit paths, and a release only clears a claim that matches. A run
admitted with no breaker carries a null claim and releases nothing.

Regression tests cover all three overlaps; each fails against the unscoped
release. Rebuilt plugin/scripts/worker-service.cjs so the shipped bundle
matches source.

Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01Sc5nL8WJRdBscNu9egvDQB
2026-08-31 11:12:03 -07:00
Alex Newman dc14018c13 feat(modes): tell observer sessions to stay silent and never contact other agents (#3817)
Appends two rules to observer_role in all four base modes:

- SILENT BY DESIGN: the observer runs invisibly in the background and the
  observed session must not learn it is being watched, because an agent that
  knows it is observed changes behavior in unpredictable ways — which corrupts
  the record the observer exists to create.
- NO CONTACT: never contact, message, ping, or notify another agent or session
  (including the observed one), never spawn subagents, never ask for input or
  attempt to influence work in progress.

observer_role is interpolated into both the init and continuation prompts, so
this reaches the observer on every turn. The 32 localized and --chill variants
override only placeholders and footers, so they inherit the new text with no
per-language edits.

Also adds the rule to the mode-creator authoring reference (example plus a
prompt-quality checklist item) so newly authored modes carry it.


Claude-Session: https://claude.ai/code/session_017LYx61kB4eAX5kHSz4qqzt

Co-authored-by: Claude Opus 5 <noreply@anthropic.com>
2026-08-31 11:06:17 -07:00
Claude d13686538a fix(observer): resume after a recycle, and admit one quota probe not N
Two P1 findings from Greptile's review of #3807, both reproduced with harnesses
and both real.

1. The final observation of a session was stranded after a recycle

Recycling resets the claimed batch to pending and aborts, but the documented
restart path needs a LATER ingest event. On the last tool call of a session no
later ingest arrives, so that work sat in the pending buffer and was never
recorded — the observer going quiet, which is worse than the bug this PR fixes
because it fails silently.

A recycle is the one abort that should resume on its own: unlike quota and auth,
nothing is waiting on the user, and the replacement generation fits by
construction. SessionRoutes now starts one after an `overflow:recycle` exit,
deferred a tick because `session.generatorPromise` is assigned after the promise
chain is built and resuming inline would be overwritten by that assignment.
`overflow:exhausted`, quota, and auth deliberately do not resume; the existing
recycle counter still bounds it to two attempts before the observer pauses.

2. An expired quota cooldown admitted every waiting session at once

`isQuotaCooldownActive` was a bare time check, so after the window elapsed every
concurrent session passed it and they all hit the provider together — on the
machine in #3800 that is 28-69 requests, turning "one probe per window" back
into the burst this breaker exists to prevent.

`tryAdmitQuotaProbe` now claims the single probe synchronously, so exactly one
caller wins and the rest are withheld until it resolves. Success clears the
breaker, a fresh refusal re-arms it, and `releaseQuotaProbe` runs on every
generator exit so a claim cannot outlive its request. A claim also goes stale
after 5 minutes, so a generator that dies without reaching any completion path
cannot wedge the provider shut — the inverse failure of the one being fixed.
`isQuotaCooldownActive` stays as the read-only form for logging.

The probe gate also moved below the Claude-CLI setup gate: claiming before those
early returns leaked the claim.

tests/worker/overflow-recycle-resume.test.ts drives the real SessionRoutes exit
path and asserts the replacement start, that exhausted/quota/auth do NOT resume,
and that the session is preserved rather than finalized. Eight new quota-cooldown
cases cover single admission under 28 concurrent callers, in-flight withholding,
stale reclaim, release, re-arm, and per-provider scoping.

Full suite: 2757 pass, 0 fail across 257 files. Bundles rebuilt.

Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01ERvWLxiY96C7pJrx7hPjh8
2026-08-31 04:19:15 +00:00
Claude eb9b211fa9 chore: bump version to 13.19.0
Minor rather than patch: adds the user-facing setting
CLAUDE_MEM_OBSERVER_MAX_CONVERSATION_CHARS and changes observer prompt content
(the <session_so_far> seed block), on top of the cost fixes.

Rebuilds the shipped plugin/scripts bundles so the released plugin actually
carries the observer changes, matching the previous bump commit's shape.

Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01ERvWLxiY96C7pJrx7hPjh8
2026-08-31 04:05:14 +00:00
Claude 8f60f8f98b refactor(observer): run the conversation in bounded generations seeded from memory
Replaces the mid-conversation compaction from the previous commit. Trimming the
transcript — pinning the framing message, keeping the newest turns, dropping the
middle behind an elision marker — was a second, ad-hoc summarizer bolted onto a
product whose entire premise is that observations ARE the compressed form of a
session. It also degraded continuity in a way the observer could not reason
about.

The conversation now runs in generations. When one reaches its character budget
it is retired and a fresh one starts, seeded with the session's own recorded
observations via SessionStore.getObservationsForSession — the memory claude-mem
has been writing all along. Nothing is trimmed and no gap is invented.

  - observer-recycle.ts owns only the "is this generation full?" decision.
  - recycle-conversation.ts performs a retirement: preserve the claimed batch,
    drop the conversation, force a fresh generation. Both triggers share it —
    the proactive budget check and the reactive "Prompt is too long" refusal
    (the safety net for windows narrower than the budget assumes).
  - buildSessionSoFar renders the prior observations into a <session_so_far>
    block, carried by both buildInitPrompt and buildContinuationPrompt. It is
    seeded on every generator start, not just after a recycle, so a generation
    resuming after a quota or auth pause also knows what it already captured
    instead of re-recording it.
  - The budget is checked BEFORE a send, so the request that would cross the
    ceiling is never paid for.
  - CLAUDE_MEM_OBSERVER_MAX_CONVERSATION_CHARS makes it operator-tunable; #3800
    noted there was no knob for this among the 102 settings.

Per-session cost is now linear in observations instead of quadratic, and a full
conversation is a routine generation boundary rather than a permanent wedge.

Full suite green: 2744 pass, 0 fail across 256 files.

Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01ERvWLxiY96C7pJrx7hPjh8
2026-08-31 00:41:18 +00:00
Claude 7a7ada5f65 fix(worker): bound observer conversation growth and stop two unbounded request loops
Three users were forced onto higher-tier plans by the observer burning their
subscription quota. Two independent unbounded loops are responsible.

1. Quadratic conversation growth, then a permanent wedge (#3800)

The observer keeps one long-lived conversation per session and appends every
captured tool call to it. Nothing trimmed it, and OpenAICompatibleProvider
re-sent the whole array on every observation, so total input tokens for a
session were O(N^2) in the number of tool calls. A single observation prompt is
field-truncated to ~32k chars, so ~25-30 observations crossed a 200k ceiling.

Past that the provider answers "Prompt is too long" as ordinary assistant text.
It was not classified, so it fell through to generic prose, the batch was
confirmed and dropped, and the next observation appended yet more — every later
tool call paid full freight for a request that could never succeed. A reporter
logged 2,264 of these in one day across 28 sessions.

Nothing stopped it: consecutiveInvalidOutputs was assigned 0 at all five sites
and never incremented, so no breaker keyed on it could trip, and the rejection
log line printed the counter it had just zeroed — which read as "breaker fine"
on every failure.

  - Add compactConversationHistory: pins the framing message, keeps the newest
    turns, drops the middle oldest-first, and announces the elision. Per-request
    size is now flat in session length instead of linear.
  - Add isContextOverflowObserverOutput and handle it: preserve the batch,
    recycle the conversation, force a fresh one, and hard-pause after two
    consecutive failures instead of retrying forever.
  - Stop appending rejections to history — a failure must not enlarge the next
    request.
  - Stop double-appending assistant replies (provider + ResponseProcessor both
    pushed the same text, inflating every later request by ~50%) (#3619).
  - Count overflows on their own counter so benign skip acknowledgements keep
    resetting without rebuilding the respawn debt that reset was added to stop.

2. One doomed request per observation after the allowance is spent (#3634)

Once a provider reported the inference allowance exhausted, the generator exited
and the very next captured tool call started a fresh one that earned the same
refusal — for the rest of the billing cycle. 11 capped users produced tens of
thousands of cap events in a day, ~100 per successful observation.

ensureGeneratorRunning already had this gate for a missing Claude CLI; add the
same shape for quota. The breaker arms on a quota exit (classified error or
quota prose), withholds requests for a cooldown, lets exactly one probe through
after it, and clears immediately on a successful store.

Full suite green: 2743 pass, 0 fail across 256 files.

Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01ERvWLxiY96C7pJrx7hPjh8
2026-08-30 21:26:11 +00:00
Alex Newman fa6a1e9ec1 docs: update changelog for v13.18.0
Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01M5yKHR68czpM9bFEf9kq7i
2026-08-29 16:41:03 -07:00