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 .`.
A direct ``hook["type"]`` subscript raised ``KeyError`` before the
assert evaluated when the ``"type"`` key was missing, so the
diagnostic message naming the event and the unexpected value was
never shown. ``hook.get("type")`` lets the assert fail gracefully
with the intended feedback.
A duplicate hook entry inside `entries[0]["hooks"]` (or a second
entry under `Stop`/`PreCompact`) silently passes the per-hook
bounds check and would double-fire the hook. Pin `len(entries) == 1`
and `len(sub_hooks) == 1` so cardinality drift fails loudly with a
diagnostic message naming the event and the unexpected count.
The `.claude-plugin/hooks/hooks.json` Stop and PreCompact entries
shipped without an explicit `timeout`, so a hung `mempalace hook
run` (reported on Windows under CLI/plugin version drift) blocks
the Claude Code UI for the full 600s `"type": "command"` default.
Set explicit bounds: Stop = 30s (fire-and-forget save path with
synchronous chromadb diary write, detached mine via _spawn_mine),
PreCompact = 90s (60s inner _mine_sync subprocess timeout in
mempalace/hooks_cli.py plus a 30s slack so legitimate mines for
users with MEMPAL_DIR are not truncated mid-flight).
Schema test in tests/test_claude_plugin_hook_config.py pins both
per-event (floor, ceiling) bounds and catches future events added
to the plugin config without a registered bounds entry.