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#659bensig merged 3 commits intoMemPalace:developfrom
Conversation
There was a problem hiding this comment.
Pull request overview
This PR fixes stop-hook diary entries being written to a single agent wing by introducing an optional wing parameter for diary read/write and deriving a per-project wing from the Claude Code transcript path.
Changes:
- Added optional
wingparameter totool_diary_write()andtool_diary_read(), and exposed it via the MCP tool schemas. - Added transcript-path parsing in the stop hook to append a
wing=<project>hint to the block reason. - Relaxed the stop-hook test assertion to account for the new reason suffix.
Reviewed changes
Copilot reviewed 3 out of 3 changed files in this pull request and generated 5 comments.
| File | Description |
|---|---|
mempalace/mcp_server.py |
Adds wing support to diary tools and updates MCP tool schemas accordingly. |
mempalace/hooks_cli.py |
Derives a per-project wing from transcript paths and includes it in stop-hook block messaging. |
tests/test_hooks_cli.py |
Updates assertion to tolerate the new stop-hook reason suffix. |
💡 Add Copilot custom instructions for smarter, more guided reviews. Learn how to get started.
| try: | ||
| agent_name = sanitize_name(agent_name, "agent_name") | ||
| entry = sanitize_content(entry) | ||
| except ValueError as e: | ||
| return {"success": False, "error": str(e)} | ||
|
|
||
| wing = f"wing_{agent_name.lower().replace(' ', '_')}" | ||
| if wing: | ||
| wing = wing.lower().replace(" ", "_") | ||
| else: | ||
| wing = f"wing_{agent_name.lower().replace(' ', '_')}" | ||
| room = "diary" |
There was a problem hiding this comment.
wing is currently only normalized via lower().replace(" ", "_") and is not passed through sanitize_name like other wing/room inputs (e.g. tool_add_drawer). This allows slashes, .., very long strings, etc., which can break IDs/metadata and bypass the repo’s path-traversal protections. Please validate/sanitize wing (and ideally the final computed wing string) using sanitize_name, and treat whitespace-only wing as “not provided”.
There was a problem hiding this comment.
Fixed — replaced wing.lower().replace(" ", "_") with sanitize_name(wing) for proper path-traversal/length/character validation.
| def tool_diary_read(agent_name: str, last_n: int = 10, wing: str = ""): | ||
| """ | ||
| Read an agent's recent diary entries. Returns the last N entries | ||
| in chronological order — the agent's personal journal. | ||
| """ | ||
| wing = f"wing_{agent_name.lower().replace(' ', '_')}" | ||
| if wing: | ||
| wing = wing.lower().replace(" ", "_") | ||
| else: | ||
| wing = f"wing_{agent_name.lower().replace(' ', '_')}" | ||
| col = _get_collection() |
There was a problem hiding this comment.
tool_diary_read now accepts a user-provided wing but does not validate it (or agent_name) and doesn’t handle ValueError like tool_diary_write. For consistency and to avoid invalid metadata queries / unexpected behavior, sanitize agent_name and wing with the shared sanitizers and return a structured {success: False, error: ...} on validation failure.
There was a problem hiding this comment.
Fixed — added sanitize_name() for both agent_name and wing in tool_diary_read(), plus try/except ValueError matching tool_diary_write()'s pattern.
| Read an agent's recent diary entries. Returns the last N entries | ||
| in chronological order — the agent's personal journal. |
There was a problem hiding this comment.
The tool_diary_read docstring still describes reading an agent’s personal diary only, but the function signature now supports overriding the target wing. Please update the docstring to mention the wing parameter and clarify how it changes the data source (agent-default vs explicit wing).
| Read an agent's recent diary entries. Returns the last N entries | |
| in chronological order — the agent's personal journal. | |
| Read recent diary entries for an agent. | |
| Returns the last N entries from the diary room in chronological order. | |
| If `wing` is omitted or empty, entries are read from the agent's default | |
| personal wing derived from `agent_name`. If `wing` is provided, entries | |
| are read from that explicit wing instead. |
There was a problem hiding this comment.
Fixed — updated docstring to document wing parameter and how it changes the data source.
| ~/.claude/projects/-home-<user>-Projects-<project>/session.jsonl | ||
| We extract <project> as the wing name. Falls back to "sessions". | ||
| """ | ||
| match = re.search(r"-Projects-([^/]+?)(?:/|$)", transcript_path) |
There was a problem hiding this comment.
_wing_from_transcript_path only treats / as a path separator. On Windows (CI runs windows-latest), transcript paths are typically backslash-separated, so this will always fall back to "sessions" and won’t actually fix per-project diary wings. Consider supporting both separators (or use Path(...).parts / normalize separators) when extracting the project segment.
| match = re.search(r"-Projects-([^/]+?)(?:/|$)", transcript_path) | |
| normalized_path = transcript_path.replace("\\", "/") | |
| match = re.search(r"-Projects-([^/]+?)(?:/|$)", normalized_path) |
There was a problem hiding this comment.
Fixed — added path separator normalization (replace("\\\\", "/")) before regex match. Added dedicated tests for Windows backslash paths.
| assert result["reason"].startswith(STOP_BLOCK_REASON) | ||
|
|
||
|
|
There was a problem hiding this comment.
The test now only checks reason.startswith(STOP_BLOCK_REASON), which can pass even if the new wing=... suffix is missing or malformed. Add assertions for the expected suffix (e.g. contains wing=sessions for non-Claude paths) and a new case covering a Claude Code transcript path so _wing_from_transcript_path() behavior is exercised.
| assert result["reason"].startswith(STOP_BLOCK_REASON) | |
| assert result["reason"].startswith(STOP_BLOCK_REASON) | |
| assert "wing=sessions" in result["reason"] | |
| def test_stop_hook_blocks_at_interval_claude_code_path(tmp_path): | |
| transcript = tmp_path / ".claude" / "projects" / "example" / "t.jsonl" | |
| transcript.parent.mkdir(parents=True, exist_ok=True) | |
| _write_transcript( | |
| transcript, | |
| [{"message": {"role": "user", "content": f"msg {i}"}} for i in range(SAVE_INTERVAL)], | |
| ) | |
| result = _capture_hook_output( | |
| hook_stop, | |
| {"session_id": "test", "stop_hook_active": False, "transcript_path": str(transcript)}, | |
| state_dir=tmp_path, | |
| ) | |
| assert result["decision"] == "block" | |
| assert result["reason"].startswith(STOP_BLOCK_REASON) | |
| assert "wing=projects" in result["reason"] |
There was a problem hiding this comment.
Fixed — tightened assertion to check for wing=sessions suffix. Added 5 new tests: wing extraction from Claude Code transcript path, project extraction, fallback to sessions, Windows paths, and lowercasing.
Addresses Copilot review feedback on MemPalace#659. Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
7c311ee to
36deb35
Compare
Addresses Copilot review feedback on MemPalace#659. Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
36deb35 to
23a349b
Compare
Addresses Copilot review feedback on MemPalace#659. Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
daf072b to
ca5b357
Compare
fork-direction.md and TODO-fork-improvements.md were scattered strategic thinking that belongs in the front-door README: competitive landscape, roadmap (P0-P6), and open problems. Merges them in and deletes the standalone files. PR-status tables (README + CLAUDE.md) were stale after today's rebases of MemPalace#659, MemPalace#660, MemPalace#661, MemPalace#673, MemPalace#681 — updated to reflect current mergeable state. MemPalace#673 specifically noted as cleanly rebased against MemPalace#863. test_readme_claims.py skips MCP-tool-table and dialect-reference checks when absent — our slimmed fork README doesn't reproduce upstream's tool table structure. Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
|
Hi, Severity: action required | Category: security How to fix: Filter diary read by agent Agent prompt to fix - you can give this to your LLM of choice:
We noticed a couple of other issues in this PR as well — happy to share if helpful. Found by Qodo code review |
|
Friendly ping on review — adds an optional |
Every remaining row in "Still ahead of upstream" now carries a status so the reader can tell at a glance whether each change is being upstreamed, pending a PR, or deliberately fork-local. Dropped: - "Guard ChromaDB 1.5.x metadata-mismatch segfault" — this row was overstated. The memory file for today's debugging notes that the try-get/except-create pattern is defensive code that never reproduced a specific crash (the actual crashes traced to HNSW drift). Leaving it in "Still ahead" implied an upstream-candidate fix, which it isn't. Code stays in place as defensive, but the README no longer claims it as a fork-ahead feature. Moved to Superseded: - "Stale HNSW mtime detection + mempalace_reconnect" — upstream took a different approach in MemPalace#757. Our broader inode+mtime detection and the mempalace_reconnect MCP tool remain as fork-local convenience; they're just not "ahead of upstream" anymore. Statuses now populated: - Linked PR number for the 7 changes with active upstream PRs (MemPalace#659, MemPalace#660, MemPalace#661, MemPalace#673 with APPROVED note, MemPalace#999, MemPalace#1000, MemPalace#1005). - "PR pending" for 3 items that are good candidates but unfiled: epsilon mtime comparison, max_distance parameter, tool output mining. - "fork-only" for 2 items we keep intentionally without pitching upstream: .blob_seq_ids_migrated marker (narrow), bulk_check_mined (complementary to upstream's MemPalace#784 file-locking). Legend sentence added above the table explains the three status values. 42 README-claim tests pass.
Code reviewFound 2 issues:
mempalace/mempalace/hooks_cli.py Lines 214 to 217 in ca5b357
mempalace/mempalace/mcp_server.py Lines 1017 to 1019 in ca5b357 🤖 Generated with Claude Code - If this code review was useful, please react with 👍. Otherwise, react with 👎. |
|
@jphein small fix needed: |
Addresses bensig's 2-issue review on PR MemPalace#659. 1. _wing_from_transcript_path() was returning bare project names (e.g. "myproject") while all existing wings follow the wing_* convention from AAAK_SPEC (wing_user, wing_agent, wing_code, wing_<project>). 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 (7 tests). Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
|
Both fixed in
All 51 hooks_cli + 62 mcp_server tests pass locally. |
…emPalace#1036 Overnight/morning: - MemPalace#681, MemPalace#1000, MemPalace#1023 merged — moved from "Still ahead" to "Merged upstream (post-v3.3.1)" - bensig reviewed MemPalace#659 (wing_ prefix + agent filter) and MemPalace#1021 (silent_guard default) — both addressed on their PR branches - MemPalace#673 needed re-rebase after overnight develop merges; done - MemPalace#1036 filed: paginate miner.status(), closes upstream MemPalace#802 and MemPalace#1015 Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
… 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>
Addresses Copilot review feedback on MemPalace#659. Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
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>
49704af to
1f5c7bf
Compare
…emPalace#1094 rows + MemPalace#659 CLEAN Three open-PR rows were missing (cmd_export, cmd_purge, None-metadata boundary). MemPalace#659 moved from MERGEABLE to CLEAN after the 2026-04-22 rebase onto current develop cleared its stale-merge blocker. Open-PR count: 4 → 7. Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
bensig merged MemPalace#659 (diary wing routing) into develop at 22:07 UTC today. Propagate through both the private CLAUDE.md tracker and the public README: - CLAUDE.md fork-ahead row 9 → merged - CLAUDE.md PR tracker header: 13 → 14 merged, 7 → 6 open - CLAUDE.md PR tracker rows: MemPalace#659 moves from open to merged (2026-04-23) - CLAUDE.md "Merged into upstream (post-v3.3.1)" list gains a bullet - README.md intro blurb gains MemPalace#659 in weekly merge list - README.md change queue: Diary wing routing row removed (no longer fork-ahead) - README.md PR tables: MemPalace#659 moved from open to merged-since-v3.3.1 Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
… path (MemPalace#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 MemPalace#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>
Restore-integrity release. Unbreaks fresh `pip install mempalace` from v3.3.2 by re-tagging current develop, which carries both the plugin.json consumer (shipped in 3.3.2) and the matching mempalace-mcp entry point in pyproject.toml (added on develop ~10h after the 3.3.2 tag via MemPalace#340 by @messelink). MemPalace#1093 diagnosed by @jphein. Bumps (all 5 sources agree per Version Guard / CLAUDE.md): - mempalace/version.py 3.3.2 → 3.3.3 - pyproject.toml 3.3.2 → 3.3.3 - .claude-plugin/plugin.json 3.3.2 → 3.3.3 - .claude-plugin/marketplace.json 3.3.2 → 3.3.3 - .codex-plugin/plugin.json 3.3.2 → 3.3.3 - CHANGELOG.md new [3.3.3] entry No code changes. The fix for MemPalace#1093 is already on develop via merged PRs MemPalace#340, MemPalace#1021, MemPalace#851, MemPalace#942, MemPalace#833, MemPalace#673, MemPalace#661, MemPalace#659, MemPalace#1097, MemPalace#1051, MemPalace#1001, MemPalace#945. Branch name intentionally outside the `release/*` ruleset so follow-up CI-fix commits aren't gated behind a nested PR. (Supersedes MemPalace#1143 — closed for exactly that reason after it missed 3 of 5 version files.) Smoke-tested locally from a fresh develop clone: grep mempalace-mcp pyproject.toml .claude-plugin/plugin.json # both ✓ python -m build --wheel # ✓ pip install …-py3-none-any.whl # ✓ which mempalace-mcp # ✓ mempalace-mcp --help # ✓
…safe graph cache 31 commits including: - v3.3.3 release (restores pip install integrity post-v3.3.2 MemPalace#1093 regression) - MemPalace#1097 empty-string filter normalization in mempalace_search - MemPalace#659 diary wing parameter (our fork PR, now upstream) - MemPalace#851/MemPalace#1097/MemPalace#1021/MemPalace#661/MemPalace#673 incorporated - website Crystal Lattice brand refresh - thread-safe graph cache in palace_graph.py Conflict resolutions (10 files): - README.md keep fork version; bump badge 3.3.1 → 3.3.3 for test compat - hooks/README.md keep fork silent/block architecture docs; keep MEMPAL_PYTHON (correct for legacy hook, upstream's rename is stale) - examples/HOOKS_TUTORIAL.md same treatment - mcp_server.py take upstream's sanitize_name(wing) — strictly better than our crude lowercase+underscore normalization - miner.py keep fork 10K batch size + comma formatting on status; adopt upstream's pagination rationale comment - palace_graph.py take upstream entirely — thread-safety improvements layered on top of our MemPalace#661 (which upstream already merged) - hooks_cli.py take upstream (Windows path-separator compat in _wing_from_transcript_path, Codex CLI format in _extract_recent_messages), then re-apply fork-ahead: use _wing_from_transcript_path in _ingest_transcript instead of hardcoded "sessions" — keeps transcript mining coherent with the diary wing derivation from MemPalace#659 - tests/test_hooks_cli.py take upstream's updated wing-kwarg assertions and new test_stop_hook_derives_wing_from_transcript_path; take upstream's mock-based security test (simpler than our three-way assertion, same property tested) Post-merge test state: - 1096 passed, 10 failed in tests/test_claude_plugin_hook_wrappers.py - The 10 failures are the fork-ahead MemPalace#19 divergence already documented in CLAUDE.md: our venv-aware hooks use `dirname`/`cat` which the test's scrubbed-PATH environment doesn't provide. Same class that correctly caught MemPalace#1115 and led us to withdraw it pending MemPalace#1069 arbitration. Expected. Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
Addresses two of the three findings @igorls flagged in MemPalace#1145 from the v3.3.3 smoke test. Same file class, same "LLM agent / edge-case user defaults to empty string" failure mode — batched into one PR. ## Bug 1: `_wing_from_transcript_path` regex was too narrow The regex only matched `-Projects-<project>` in the encoded transcript folder name. Users whose code lives outside `~/Projects/` — `~/dev/`, `~/src/`, `~/code/`, or any non-Projects convention — got transcript paths like `~/.claude/projects/-home-<user>-dev-<Parent>-<project>/...` with no `-Projects-` segment, fell through to `wing_sessions`, and lost the per-project diary scoping MemPalace#659 was meant to deliver. Fix: add a second strategy. Keep the explicit `-Projects-<x>` match first (preserves existing test coverage and behavior for code under `~/Projects/`), then fall back to the last `-`-delimited segment of the encoded folder name. That segment is the actual project directory name regardless of source layout. Four new tests cover the non-Projects scenarios igor raised plus a genuine fallback case (parent folder without `-` delimiters → still `wing_sessions`). Existing tests for the `-Projects-<x>` path stay green. ## Bug 2: `tool_diary_read` ignored MemPalace#1097's empty-string pattern `MemPalace#1097` fixed `mempalace_search` to treat `wing=""` / `room=""` as "no filter" — LLM agents frequently default optional string parameters to `""`. `tool_diary_read` still defaulted an empty `wing` to `wing_<agent_name>`, which silently hid entries written to named wings (e.g. the per-project wings from MemPalace#659). Fix: route `wing` through `_sanitize_optional_name()` (the helper MemPalace#1097 introduced) so empty / whitespace / `None` all coerce to `None` and drop out of the query filter. An explicit non-empty wing still scopes to that wing — verified by a regression test. Build the `where` clause from a list of required conditions (`room=diary`, `agent=<name>`) and conditionally append the wing filter — matches the pattern `tool_list_drawers` already uses. ## Bug 3 (Stop-hook auto-mine `MEMPALACE_PALACE_PATH` env propagation) — not in this PR Deferred to a follow-up PR pending local reproduction of the env- propagation behavior. Will file separately once I've confirmed root cause per the plan in my MemPalace#1145 comment. ## Tests 1067 passed on this branch (branched from `upstream/develop`). Six new tests: - tests/test_hooks_cli.py — four new `_wing_from_transcript_path` cases (`~/dev/`, `~/src/`, `~/code/`, bare fallback) - tests/test_mcp_server.py — two new `tool_diary_read` cases (empty-wing reads across wings, explicit wing still filters) Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
…alace#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 MemPalace#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.
MemPalace#1097 fixed mempalace_search to treat empty-string wing/room as no filter, matching how LLM agents default to filling every optional parameter with ''. The same pattern wasn't applied to diary_read: passing wing='' defaulted to wing_<agent_name>, siloing away entries that hooks had written to project-derived wings per MemPalace#659. When wing is empty/omitted, filter only on agent + room=diary so callers get a unified view of the agent's journal across every wing it has written to. Explicit wing=<name> continues to scope reads to that wing only. Adds test covering empty-wing read after writing to both the default and a non-default wing.
…stop hook Combined port of upstream df3ee28 (PR MemPalace#659) and 1fd16da (PR MemPalace#1145). The two upstream commits chain on the same diary scoping feature, so landing them as one fork commit keeps the reasoning in one place. mcp_server.py * tool_diary_write(): new optional `wing` param. Empty/whitespace falls back to the existing wing_<agent_name> default. * tool_diary_read(): new optional `wing` param. - When wing is provided → filter by wing + room=diary + agent. - When wing is empty/whitespace → span all wings this agent has written to (room=diary + agent only). Matches the LLM-friendly empty-string pattern from PR MemPalace#1097. The agent filter is applied unconditionally so callers sharing a project wing can only see their own entries (security note from bensig's PR MemPalace#659 review). * TOOLS schema: `wing` exposed as optional on both diary tools with descriptions explaining the per-project use case. hooks/mempal_save_hook.sh * Derive PROJECT_WING from $TRANSCRIPT_PATH. Claude Code transcripts live under ~/.claude/projects/-encoded-project-folder/<session>.jsonl; the final dash-separated token is a stable per-project handle. Adapted from upstream d158375 (Linux/cross-platform variant of the macOS-only path heuristic). * Append `wing="<derived>"` hint to the AUTO-SAVE block reason so the AI files diary entries via mempalace_diary_write under the project wing instead of the agent's bucket. * Reason is now emitted via python3 -c (json.dumps) so quoting the derived wing into the JSON string is safe. Adapted: upstream's df3ee28 also touches mempalace/hooks_cli.py with a _wing_from_transcript_path() helper. This fork has no hooks_cli.py (bash-only hooks), so the derivation lives directly in the bash hook. sanitize_name() is also not in this fork; minimal validation is done inline (strip + truthiness check) which matches the existing tool_search wing-handling pattern. 7 tests added covering: wing override on write, default fallback, whitespace fallback, wing+agent filter on read, empty-wing/whitespace spanning all wings. Upstream: MemPalace@df3ee28 MemPalace@1fd16da MemPalace@d158375 (hook-derivation portion adapted to bash) Co-authored-by: Jeffrey Hein <jp@jphein.com> Co-authored-by: Igor Lins e Silva <4753812+igorls@users.noreply.github.com> Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
Upstream merged MemPalace#1173, MemPalace#1177, MemPalace#1198, MemPalace#1201 into develop on 2026-04-26 (picked up by the 2026-04-27 develop sync). Strike out their fork-ahead rows in CLAUDE.md and add to the "Merged into upstream" section. Update PR table accordingly: 18 merged (was 14), 7 open (was 8). Bump version note: upstream shipped v3.3.3 on 2026-04-24 (carries our MemPalace#659/MemPalace#1021); v3.3.4 prep branch in flight. README: - "tracks upstream/develop through the 2026-04-27 sync" (was 2026-04-26) - 17 fork-ahead changes (was 22) - 1,510 tests pass on main (was 1,395 — upstream brought new suites) - "Open upstream PRs" 7 entries (was 11) - Drop merged rows from "Fork-ahead — open or pending" table; keep PreCompact recovery-marker row (still fork-only) scripts/check-docs.sh: clean (90 PR refs match upstream state, 13 fork hashes resolve, FORK_CHANGELOG renders clean from YAML). docs/fork-changes.yaml: no edit needed — already had merged_date on the 4 entries from the 2026-04-26 commit `5fd15db`.
Problem
All diary entries from the stop hook land in
wing_session-hookregardless of which project the Claude Code session is in. This makes per-project diary search impossible — everything piles into one wing.Solution
mcp_server.pywingparameter totool_diary_write(). If provided, sanitizes it (lowercase, spaces→underscores) and uses it directly. If omitted, falls back to the existingwing_{agent_name}behavior.wingparameter totool_diary_read()for filtering by target wing.wingin theinput_schemafor bothmempalace_diary_writeandmempalace_diary_readin the TOOLS dict.hooks_cli.py_wing_from_transcript_path(path)helper that extracts the project name from Claude Code transcript paths:~/.claude/projects/-home-jp-Projects-kiyo-xhci-fix/...→kiyo-xhci-fix"sessions"for unrecognized paths.hook_stop, derive the project wing from the transcript path and append awing=<project>hint to the block reason so Claude writes diary entries to the correct per-project wing.Backwards Compatibility
Fully backwards compatible —
wingis optional in both functions and the TOOLS schema. Omitting it produces identical behavior to before.Test Changes
Updated
test_stop_hook_blocks_at_intervalto usestartswith(STOP_BLOCK_REASON)since the block reason now includes a project wing suffix.Test plan
python -m pytest tests/ -q --ignore=tests/test_convo_miner.py— all 597 tests passruff check mempalace/mcp_server.py mempalace/hooks_cli.py— no issuestool_diary_write(agent_name="claude", entry="test", wing="my_project")stores inmy_project/diarytool_diary_write(agent_name="claude", entry="test")still stores inwing_claude/diary(backwards compat)wing=<project>for Claude Code sessions