Skip to content

feat(hooks): tune stop-hook cadence via env vars (MEMPAL_SAVE_INTERVAL / MEMPAL_STOP_HOOK_DISABLE)#985

Open
felipetruman wants to merge 2 commits into
MemPalace:developfrom
felipetruman:feat/hook-tuning-env-vars
Open

feat(hooks): tune stop-hook cadence via env vars (MEMPAL_SAVE_INTERVAL / MEMPAL_STOP_HOOK_DISABLE)#985
felipetruman wants to merge 2 commits into
MemPalace:developfrom
felipetruman:feat/hook-tuning-env-vars

Conversation

@felipetruman

Copy link
Copy Markdown
Contributor

Summary

The stop hook currently blocks every 15 human messages with the "AUTO-SAVE checkpoint" prompt, hard-coded via the module-level `SAVE_INTERVAL` constant in `mempalace/hooks_cli.py`. Two new env vars let users tune that without reinstalling or editing plugin files:

Variable Effect
`MEMPAL_SAVE_INTERVAL=N` Override the cadence (clamped to `max(1, N)`; invalid/empty falls back to 15 so a typo can't collapse the interval to 0)
`MEMPAL_STOP_HOOK_DISABLE=1` Pass-through (also accepts `true`/`yes`/`on`, case-insensitive, whitespace trimmed). State still tracked, no blocking.

Both are read on every hook invocation, so users can change behavior mid-session without touching the install.

Why

Two concrete cases I hit on my workstation today:

  1. Long Ollama-GPU embedding migration session — a session that does real work (editing the backend, reviewing a PR, adding tests) easily crosses 15 messages multiple times, and each block interrupts flow to repeat the same 3 MCP calls. A higher per-session interval (e.g. `MEMPAL_SAVE_INTERVAL=50`) keeps the auto-save intent while drastically cutting noise.
  2. Plugin installed but MCP tools not registered for the current project — the block message instructs the model to call `mempalace_diary_write` / `mempalace_add_drawer` / `mempalace_kg_add`, but if the MemPalace MCP server isn't exposed in the active Claude session those tools aren't callable. The model can't satisfy the block, the user sees a loop of error messages. `MEMPAL_STOP_HOOK_DISABLE=1` gives a clean opt-out.

Behavior preserved

  • Default cadence is still 15 (existing users see no change).
  • `SAVE_INTERVAL` constant is still exported from the module (set to `DEFAULT_SAVE_INTERVAL`), so any external code / tests that read it keep working.
  • The existing stop_hook_active infinite-loop guard and MEMPAL_DIR auto-ingest paths are untouched.

Test plan

Added `tests/test_hooks_cli_tuning.py` — 22 unit tests, all passing:

  • Default when unset.
  • Integer overrides with whitespace trim (`" 100 "` → `100`).
  • Empty / whitespace values fall back to default.
  • Invalid strings (`"abc"`, `"3.5"`, `"--"`) fall back to default rather than raising or silently zeroing.
  • Non-positive values (`"0"`, `"-1"`) clamped to 1.
  • Disable flag truthy variants (`1`, `true`, `TRUE`, `Yes`, `on`, `" true "`) all return True.
  • Disable flag falsy variants (`0`, `false`, `no`, `off`, `""`, `"maybe"`) all return False.
  • Backwards-compat constant export still equals 15.

```
$ pytest tests/test_hooks_cli_tuning.py -v
22 passed in 0.06s
```

Files

  • `mempalace/hooks_cli.py` — new `_get_save_interval()` / `_stop_hook_disabled()` helpers + wired into `hook_stop`.
  • `tests/test_hooks_cli_tuning.py` — new test file.

🤖 Generated with Claude Code

The stop hook currently blocks every 15 human messages with an
"AUTO-SAVE checkpoint" prompt, hard-coded via the module-level
``SAVE_INTERVAL`` constant.

Two new env vars let users tune this without reinstalling the plugin:

- ``MEMPAL_SAVE_INTERVAL=N`` overrides the cadence. Invalid / empty
  values fall back to 15 so a typo can never drop the interval to 0
  and block every single turn. Values <= 0 are clamped to 1.
- ``MEMPAL_STOP_HOOK_DISABLE=1`` (or true/yes/on) turns the block off
  entirely — the hook still records state but never asks the model to
  save. Useful when MemPalace MCP tools are not registered in the
  session (e.g. the plugin is installed but not configured into the
  current project's MCP list), so the block would nag without any
  way for the model to satisfy it.

Both read the env on every hook invocation, so users can change
behavior mid-session without touching the plugin install.

The public ``SAVE_INTERVAL`` constant is preserved (re-exports
``DEFAULT_SAVE_INTERVAL``) so any external code / tests that read it
keep working.

22 new unit tests in tests/test_hooks_cli_tuning.py cover:
  - default when unset
  - numeric override + whitespace trimming
  - invalid strings fall back to default (no zero-interval hazard)
  - non-positive values clamped to 1
  - truthy/falsy normalization for the disable flag
  - backwards-compatible constant export

All tests pass.
Copilot AI review requested due to automatic review settings April 17, 2026 22:49

Copilot AI left a comment

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Pull request overview

Adds runtime-configurable tuning knobs for the MemPalace stop hook so users can adjust (or disable) the auto-save blocking cadence via environment variables without reinstalling.

Changes:

  • Introduces MEMPAL_SAVE_INTERVAL parsing (with clamping/fallback) to override the default stop-hook blocking interval.
  • Introduces MEMPAL_STOP_HOOK_DISABLE parsing to allow a pass-through “no blocking” mode.
  • Adds a new unit-test module covering env-var parsing helpers and backwards-compat constant export.

Reviewed changes

Copilot reviewed 2 out of 2 changed files in this pull request and generated 3 comments.

File Description
mempalace/hooks_cli.py Adds env-var parsing helpers and wires them into hook_stop decision logic and logging.
tests/test_hooks_cli_tuning.py Adds unit tests for interval/disable env var parsing and compatibility constant behavior.

💡 Add Copilot custom instructions for smarter, more guided reviews. Learn how to get started.

Comment thread mempalace/hooks_cli.py Outdated
Comment on lines +25 to +26
Invalid / non-positive values silently fall back to the default so a
typo can never make the hook block on every single turn.

Copilot AI Apr 17, 2026

Copy link

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

The _get_save_interval docstring says “Invalid / non-positive values … fall back to the default”, but the implementation clamps non-positive values to 1 via max(1, value). Please update the docstring (or the logic) so the documented behavior matches what actually happens (especially since an interval of 1 effectively blocks every turn).

Suggested change
Invalid / non-positive values silently fall back to the default so a
typo can never make the hook block on every single turn.
Missing / non-integer values silently fall back to the default.
Parsed values less than 1 are clamped to 1, which causes the hook to
block on every turn.

Copilot uses AI. Check for mistakes.
Comment thread mempalace/hooks_cli.py Outdated
Comment on lines +260 to +264
# Fully disabled via env var — pass through.
if _stop_hook_disabled():
_output({})
return

Copilot AI Apr 17, 2026

Copy link

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

MEMPAL_STOP_HOOK_DISABLE currently returns early before counting messages and before updating/maintaining the per-session *_last_save state (and also skips logging). This contradicts the PR description (“state still tracked, no blocking”) and can cause an immediate block when the flag is turned off mid-session because since_last will include all messages since the last real save. Consider still updating the last-save point (and/or logging) when disabled so the hook remains non-blocking without losing cadence state.

Copilot uses AI. Check for mistakes.
Comment thread mempalace/hooks_cli.py
Comment on lines +244 to +249
"""Stop hook: block every N messages for auto-save.

Respects two env vars:
- ``MEMPAL_STOP_HOOK_DISABLE=1`` — pass through without blocking.
- ``MEMPAL_SAVE_INTERVAL=N`` — override the 15-message default.
"""

Copilot AI Apr 17, 2026

Copy link

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

The new env-var behaviors are only unit-tested at the helper level. Since hook_stop now depends on process environment at runtime, please add integration tests that exercise hook_stop with MEMPAL_SAVE_INTERVAL set (e.g., verify it blocks at the overridden interval) and with MEMPAL_STOP_HOOK_DISABLE set (verify pass-through behavior and whatever state-tracking semantics you intend). Also ensure existing hook_stop tests clear these env vars so they don’t become flaky when a developer has them set in their shell.

Copilot uses AI. Check for mistakes.
## MemPalace#1 docstring / implementation mismatch in _get_save_interval

Docstring said "invalid / non-positive values fall back to the default"
but the code clamps to 1 via max(1, value). Rewrote the docstring to
document the actual behavior: missing / non-integer values fall back to
default; parsed values < 1 clamp to 1 (effectively blocks every turn).
Added a pointer to MEMPAL_STOP_HOOK_DISABLE for users who wanted
"no block" rather than "block every turn".

## MemPalace#2 MEMPAL_STOP_HOOK_DISABLE must not drift cadence state

Disabled path used to return before updating *_last_save, so toggling
the flag off mid-session would make since_last cover every message
accumulated while disabled and trigger an immediate retroactive block.

Fixed by advancing the last-save watermark to the current exchange
count while disabled — state stays synced with activity, no block is
ever emitted. Re-enabling resumes the normal cadence from the current
position, not from whenever the hook was last active.

## MemPalace#3 integration tests for hook_stop env-var behavior

Unit tests only exercised _get_save_interval / _stop_hook_disabled
helpers. Added five integration tests in test_hooks_cli_tuning.py that
run hook_stop end-to-end:

- custom interval triggers block at override
- custom interval passes through below override
- MEMPAL_STOP_HOOK_DISABLE=1 passes through even far above interval
- disabled path still advances last-save watermark (state tracking)
- stop_hook_active short-circuit must not touch state

Also added an autouse fixture in test_hooks_cli.py that monkeypatches
MEMPAL_SAVE_INTERVAL and MEMPAL_STOP_HOOK_DISABLE off, so a developer
with those vars set in their shell can't break the interval assertions.

79 tests pass (46 in test_hooks_cli.py + 33 in test_hooks_cli_tuning.py).
@felipetruman

Copy link
Copy Markdown
Contributor Author

Thanks for the review. All 3 comments addressed in ba07cb6.

# File:Line Comment Resolution
#1 hooks_cli.py:25-26 Docstring said "fall back to default" but code clamps to 1 via max(1, value) Rewrote the docstring to document actual behavior: missing/non-integer → default, parsed < 1 → clamped to 1 (blocks every turn, intentional). Added pointer to MEMPAL_STOP_HOOK_DISABLE for users who wanted "no block" rather than "block every turn".
#2 hooks_cli.py:260 MEMPAL_STOP_HOOK_DISABLE bypassed state tracking — toggling the flag off mid-session would include every accumulated message in since_last and trigger an immediate block Fixed. Disabled path now advances the *_last_save watermark to the current exchange_count before returning {}. State stays synced with activity while disabled, so re-enabling resumes the normal cadence from the current position. No block is ever emitted while disabled.
#3 hooks_cli.py:249 Env-var behavior only unit-tested at helper level; existing hook_stop tests don't clear env vars Added 5 integration tests in test_hooks_cli_tuning.py that run hook_stop end-to-end: custom interval triggers block at the override; custom interval passes through below override; STOP_HOOK_DISABLE=1 passes through even far above interval; disabled path advances the watermark (proves #2 fix); stop_hook_active short-circuit does not touch state. Also added an autouse fixture in test_hooks_cli.py that clears MEMPAL_SAVE_INTERVAL / MEMPAL_STOP_HOOK_DISABLE so a developer's shell config can't make these tests flaky.

Testing

pytest tests/test_hooks_cli.py tests/test_hooks_cli_tuning.py79 passed (46 existing + 33 tuning, 5 of which are new integration tests).

Classification: #1 quality (docs), #2 critical (bug — disabled flag actually broke on toggle-off), #3 quality (test coverage). The state-tracking fix in #2 is the load-bearing one — the docstring and test improvements make the feature safe to recommend.

@igorls

igorls commented May 8, 2026

Copy link
Copy Markdown
Member

Hi, thanks for the contribution.

This PR has merge conflicts with develop, and the branch has not been updated in over 7 days, which puts it before our most recent release. The conflicts are likely against work that landed in that release.

Could you rebase onto develop so we can take another look?

If this change is no longer relevant, feel free to close the PR.

(This message is part of a periodic backlog pass, sent to all open PRs that match this state.)

@igorls igorls added the needs-rebase PR has merge conflicts with develop and needs rebase label May 8, 2026
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

area/hooks Claude Code hook scripts (Stop, PreCompact, SessionStart) enhancement New feature or request needs-rebase PR has merge conflicts with develop and needs rebase

Projects

None yet

Development

Successfully merging this pull request may close these issues.

3 participants