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)
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 .`.
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.
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).
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.