Commit Graph
5 Commits
Author SHA1 Message Date
mvalentsev d09392c11a feat(hooks): add a budget-safe SessionEnd save hook for clean exits (#1341)
Short sessions that exit cleanly below SAVE_INTERVAL and without a PreCompact
were never saved. Add a SessionEnd hook that takes one final flush.

Claude Code budgets SessionEnd hooks at 1.5s and a plugin-provided timeout
cannot raise it, and a cold mempalace start exceeds that, so the wrapper
backgrounds the work and returns immediately; the detached child completes the
transcript ingest, project mine, and diary checkpoint after the session exits.

The handler validates transcript_path through _validate_transcript_path before
any ingest or diary write, so a traversal or wrong-suffix path is rejected while
the independent project mine still runs.

Adds hook_session_end, both shell wrappers, the plugin hooks.json entry, the
session-end CLI choice, and focused tests.

(cherry picked from commit 10e1450e04fc7cec72984ab7442d3b4fca1490e8)
2026-06-19 13:00:14 +00:00
Igor Lins e Silva 2cab6f5a5b style: reformat tree with ruff 0.15.9
One-time mechanical reformat so `ruff format --check .` passes under the
newly pinned ruff. Layout only (assert-message parenthesization etc.),
no behavior change. 29 files: 28 under tests/ + 1 tools helper, no core
mempalace/ modules. Produced by `ruff format .`.
2026-05-17 23:20:01 -03:00
mvalentsev c736a57078 test(hooks): force umask 022 in test subprocess (#1440)
Cycle 2 follow-up: harden the permission tests so they actively
falsify the "ambient umask masking a regression" hypothesis raised
during the adversarial review pass.

- `_run_hook` now sets `preexec_fn=lambda: os.umask(0o022)` on the
  bash subprocess. The child starts with a permissive 022 umask so a
  bare file would land at mode 0644. The hook's own `umask 077` in
  the parse subshell remains the only thing protecting
  `last_python_err.log` and `last_input.log`, so `test_dump_is_not_world_readable`
  and `test_python_stderr_log_is_not_world_readable_on_failure` now
  empirically prove that protection rather than passively benefitting
  from a CI runner's restrictive ambient umask.
- Docstring precision: the helper's primary diagnostic surface is
  `hook.log` (a file under `STATE_DIR`), not the subprocess's stderr.
  Subprocess stderr is captured here only to surface unexpected
  interpreter failures (e.g., a bash syntax error introduced by a
  future edit), and the docstring now says so.

No production-code changes. 26/26 tests still pass.
2026-05-15 22:51:28 +05:00
mvalentsev 2bdb1e8846 fix(hooks): apply post-review hardenings (#1440)
Follow-up on c271e7b after the bot review and an adversarial pass.

- Replace `echo "$INPUT"` with `printf '%s' "$INPUT"` in both hooks
  so payloads with leading `-n`/`-e`/`-E` or backslashes are not
  eaten by echo flag parsing.
- Wrap the `last_python_err.log` and `last_input.log` writes in a
  `( umask 077; ... )` subshell so the files are created at mode
  0600 atomically. The existing `chmod 600` calls stay as
  belt-and-suspenders.
- Refactor the `_run_hook` test helper: internal assert on
  returncode with stderr surfaced in the failure message; new
  `extra_env=` kwarg subsumes the ad-hoc `subprocess.run` in the
  UTF-8 locale test.
- Promote the world-readable, bounded-overwrite, UTF-8, unicode and
  literal-unknown tests to a shared `_BOTH_HOOKS` parametrize with
  readable ids so the symmetry across both hooks is enforced.
- `test_sed_extraction_present` strips line comments before counting
  `sed -n '` occurrences so a regression that swapped sed back to
  mapfile while keeping the prose cannot false-pass.
- Rewrite the precompact hook header docblock: the previous
  "ALWAYS blocks" wording was stale (the hook returns `{}` and
  does not emit a `decision: block` payload to the Stop-hook
  protocol; the "always run" guarantee comes from the synchronous
  mine call).
- Apply ruff 0.4.x format to the new test file (matches CI pin
  `pip install "ruff>=0.4.0,<0.5"`).

Tests: 1749 passed (was 21 cases in c271e7b's test file; now 26 after
parametrize promotions, +5 cases).
2026-05-15 22:51:27 +05:00
mvalentsev e241aa38ba fix(hooks): replace mapfile with sed for bash 3.2 compat (#1440)
The legacy hook scripts used `mapfile -t` (introduced in #1231) to read
sanitized values from the inline Python JSON parser. On stock macOS
/bin/bash is GNU 3.2.57 (Apple GPLv3 freeze) and `mapfile` only landed
in bash 4.0, so the hook silently failed: every parsed value fell back
to its default, the log wrote "Session unknown: 0 exchanges", and zero
drawers were saved.

This commit replaces the array read with `sed -n 'Np'` line extraction,
which is POSIX and runs on /bin/bash 3.2 unmodified. It also adds a
defense-in-depth fail-loud guard: Python prints a `__MEMPAL_PARSE_OK__`
sentinel and the hook surfaces the raw input plus Python traceback to
0600-mode sibling files when the sentinel is missing, so the next silent
failure has a debuggable trail instead of an unsolved zero-drawer day.
2026-05-15 22:51:27 +05:00