Skip to content

fix: add wing param to diary_write/diary_read, derive from transcript path#659

Merged
bensig merged 3 commits intoMemPalace:developfrom
jphein:fix/diary-wing-param
Apr 23, 2026
Merged

fix: add wing param to diary_write/diary_read, derive from transcript path#659
bensig merged 3 commits intoMemPalace:developfrom
jphein:fix/diary-wing-param

Conversation

@jphein
Copy link
Copy Markdown
Collaborator

@jphein jphein commented Apr 12, 2026

Problem

All diary entries from the stop hook land in wing_session-hook regardless of which project the Claude Code session is in. This makes per-project diary search impossible — everything piles into one wing.

Solution

mcp_server.py

  • Add optional wing parameter to tool_diary_write(). If provided, sanitizes it (lowercase, spaces→underscores) and uses it directly. If omitted, falls back to the existing wing_{agent_name} behavior.
  • Add optional wing parameter to tool_diary_read() for filtering by target wing.
  • Expose wing in the input_schema for both mempalace_diary_write and mempalace_diary_read in the TOOLS dict.

hooks_cli.py

  • Add _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
    • Falls back to "sessions" for unrecognized paths.
  • In hook_stop, derive the project wing from the transcript path and append a wing=<project> hint to the block reason so Claude writes diary entries to the correct per-project wing.

Backwards Compatibility

Fully backwards compatible — wing is optional in both functions and the TOOLS schema. Omitting it produces identical behavior to before.

Test Changes

Updated test_stop_hook_blocks_at_interval to use startswith(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 pass
  • ruff check mempalace/mcp_server.py mempalace/hooks_cli.py — no issues
  • Manual: tool_diary_write(agent_name="claude", entry="test", wing="my_project") stores in my_project/diary
  • Manual: tool_diary_write(agent_name="claude", entry="test") still stores in wing_claude/diary (backwards compat)
  • Manual: stop hook block reason includes wing=<project> for Claude Code sessions

Copilot AI review requested due to automatic review settings April 12, 2026 02:42
Copy link
Copy Markdown
Contributor

Copilot AI left a comment

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

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 wing parameter to tool_diary_write() and tool_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.

Comment thread mempalace/mcp_server.py
Comment on lines 550 to 560
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"
Copy link

Copilot AI Apr 12, 2026

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

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”.

Copilot uses AI. Check for mistakes.
Copy link
Copy Markdown
Collaborator Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Fixed — replaced wing.lower().replace(" ", "_") with sanitize_name(wing) for proper path-traversal/length/character validation.

Comment thread mempalace/mcp_server.py
Comment on lines +611 to 620
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()
Copy link

Copilot AI Apr 12, 2026

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

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.

Copilot uses AI. Check for mistakes.
Copy link
Copy Markdown
Collaborator Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Fixed — added sanitize_name() for both agent_name and wing in tool_diary_read(), plus try/except ValueError matching tool_diary_write()'s pattern.

Comment thread mempalace/mcp_server.py
Comment on lines 613 to 614
Read an agent's recent diary entries. Returns the last N entries
in chronological order — the agent's personal journal.
Copy link

Copilot AI Apr 12, 2026

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

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

Suggested change
Read an agent's recent diary entries. Returns the last N entries
in chronological orderthe 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.

Copilot uses AI. Check for mistakes.
Copy link
Copy Markdown
Collaborator Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Fixed — updated docstring to document wing parameter and how it changes the data source.

Comment thread mempalace/hooks_cli.py Outdated
~/.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)
Copy link

Copilot AI Apr 12, 2026

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

_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.

Suggested change
match = re.search(r"-Projects-([^/]+?)(?:/|$)", transcript_path)
normalized_path = transcript_path.replace("\\", "/")
match = re.search(r"-Projects-([^/]+?)(?:/|$)", normalized_path)

Copilot uses AI. Check for mistakes.
Copy link
Copy Markdown
Collaborator Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Fixed — added path separator normalization (replace("\\\\", "/")) before regex match. Added dedicated tests for Windows backslash paths.

Comment thread tests/test_hooks_cli.py Outdated
Comment on lines 172 to 174
assert result["reason"].startswith(STOP_BLOCK_REASON)


Copy link

Copilot AI Apr 12, 2026

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

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.

Suggested change
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"]

Copilot uses AI. Check for mistakes.
Copy link
Copy Markdown
Collaborator Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

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.

jphein added a commit to jphein/mempalace that referenced this pull request Apr 12, 2026
Addresses Copilot review feedback on MemPalace#659.

Co-Authored-By: Claude Opus 4.6 <[email protected]>
@jphein jphein force-pushed the fix/diary-wing-param branch from 7c311ee to 36deb35 Compare April 12, 2026 04:20
@jphein
Copy link
Copy Markdown
Collaborator Author

jphein commented Apr 12, 2026

Rebased onto current develop (post-#647 and #667). The wing param in diary_read now preserves #647's sanitize_name() validation and last_n clamping — both combined cleanly with the wing parameter logic.

jphein added a commit to jphein/mempalace that referenced this pull request Apr 13, 2026
Addresses Copilot review feedback on MemPalace#659.

Co-Authored-By: Claude Opus 4.6 <[email protected]>
@jphein jphein force-pushed the fix/diary-wing-param branch from 36deb35 to 23a349b Compare April 13, 2026 14:22
@igorls igorls added area/hooks Claude Code hook scripts (Stop, PreCompact, SessionStart) area/mcp MCP server and tools bug Something isn't working labels Apr 14, 2026
jphein added a commit to jphein/mempalace that referenced this pull request Apr 16, 2026
Addresses Copilot review feedback on MemPalace#659.

Co-Authored-By: Claude Opus 4.6 <[email protected]>
@jphein jphein force-pushed the fix/diary-wing-param branch from daf072b to ca5b357 Compare April 16, 2026 16:01
jphein added a commit to jphein/mempalace that referenced this pull request Apr 16, 2026
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) <[email protected]>
@Qodo-Free-For-OSS
Copy link
Copy Markdown

Hi, tool_diary_read() ignores agent_name in its Chroma where filter when wing is provided, so it can return entries written by other agents into the same wing.

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:

Issue description

tool_diary_read() can return other agents’ diary entries when callers pass a custom wing, because the Chroma where filter does not include the agent metadata field.

Issue Context

tool_diary_write() stores diary entries with metadata including agent. tool_diary_read() requires agent_name but currently does not use it in the DB query, relying on default wing naming for isolation.

Fix Focus Areas

  • mempalace/mcp_server.py[989-1045]
    • Update the where clause to include { "agent": agent_name } (and keep wing/room filters).

We noticed a couple of other issues in this PR as well — happy to share if helpful.


Found by Qodo code review

@jphein
Copy link
Copy Markdown
Collaborator Author

jphein commented Apr 18, 2026

Friendly ping on review — adds an optional wing parameter to tool_diary_write / tool_diary_read and derives the project wing from the Claude Code transcript path when writing from the stop hook, so per-project sessions don't all collapse into the default session-hook wing. Clean against current develop.

jphein added a commit to jphein/mempalace that referenced this pull request Apr 18, 2026
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.
@bensig
Copy link
Copy Markdown
Collaborator

bensig commented Apr 19, 2026

Code review

Found 2 issues:

  1. _wing_from_transcript_path() returns bare project names (e.g., "myproject") without the wing_ prefix, but all existing wings follow the wing_* convention (documented in AAAK_SPEC: "WINGS: wing_user, wing_agent, wing_team, wing_code, wing_myproject..."). Diary entries written via the stop hook land in wing "myproject" while diary_read defaults to "wing_claude" -- entries become unreachable by default reads.

match = re.search(r"-Projects-([^/]+?)(?:/|$)", normalized)
if match:
return match.group(1).lower().replace(" ", "_")
return "sessions"

  1. tool_diary_read() does not include agent_name in the ChromaDB where filter when a custom wing is provided. The filter is {"$and": [{"wing": wing}, {"room": "diary"}]} with no {"agent": agent_name} clause. Any caller passing a custom wing can read diary entries written by other agents into that wing -- a cross-agent information leak. (Also flagged by Qudo, unresolved.)

)
if not results["ids"]:

🤖 Generated with Claude Code

- If this code review was useful, please react with 👍. Otherwise, react with 👎.

@bensig
Copy link
Copy Markdown
Collaborator

bensig commented Apr 19, 2026

@jphein small fix needed: _wing_from_transcript_path() returns bare project names (e.g. myproject) but existing wings all use the wing_ prefix (wing_myproject). Entries written via the stop hook will be unreachable by default diary_read calls. See review comment above for details.

jphein added a commit to jphein/mempalace that referenced this pull request Apr 19, 2026
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) <[email protected]>
@jphein
Copy link
Copy Markdown
Collaborator Author

jphein commented Apr 19, 2026

Both fixed in 49704af:

  1. _wing_from_transcript_path() now returns wing_<project> (matches AAAK_SPEC) and falls back to wing_sessions. 6 existing tests updated.
  2. tool_diary_read() now includes {"agent": agent_name} in the $and filter — no cross-agent read access when a custom wing is shared.

All 51 hooks_cli + 62 mcp_server tests pass locally.

jphein added a commit to jphein/mempalace that referenced this pull request Apr 19, 2026
…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) <[email protected]>
jphein and others added 3 commits April 22, 2026 10:22
… 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 <[email protected]>
Addresses Copilot review feedback on MemPalace#659.

Co-Authored-By: Claude Opus 4.6 <[email protected]>
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) <[email protected]>
@jphein jphein force-pushed the fix/diary-wing-param branch from 49704af to 1f5c7bf Compare April 22, 2026 17:27
jphein added a commit to jphein/mempalace that referenced this pull request Apr 22, 2026
…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) <[email protected]>
@bensig bensig merged commit df3ee28 into MemPalace:develop Apr 23, 2026
6 checks passed
jphein added a commit to jphein/mempalace that referenced this pull request Apr 23, 2026
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) <[email protected]>
andrew-gamakon pushed a commit to Gamakon/mempalace that referenced this pull request Apr 24, 2026
… 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 <[email protected]>

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

Addresses Copilot review feedback on MemPalace#659.

Co-Authored-By: Claude Opus 4.6 <[email protected]>

* 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) <[email protected]>

---------

Co-authored-by: Claude Sonnet 4.6 <[email protected]>
jphein pushed a commit to jphein/mempalace that referenced this pull request Apr 24, 2026
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                                           # ✓
jphein added a commit to jphein/mempalace that referenced this pull request Apr 24, 2026
…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) <[email protected]>
jphein added a commit to jphein/mempalace that referenced this pull request Apr 24, 2026
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) <[email protected]>
gut-puncture pushed a commit to gut-puncture/mempalace that referenced this pull request Apr 24, 2026
…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.
gut-puncture pushed a commit to gut-puncture/mempalace that referenced this pull request Apr 24, 2026
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.
@jphein jphein deleted the fix/diary-wing-param branch April 25, 2026 14:53
gnusam pushed a commit to gnusam/mempalace-pgsql that referenced this pull request Apr 25, 2026
…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 <[email protected]>
Co-authored-by: Igor Lins e Silva <[email protected]>
Co-Authored-By: Claude Opus 4.7 (1M context) <[email protected]>
jphein added a commit to jphein/mempalace that referenced this pull request Apr 27, 2026
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`.
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

area/hooks Claude Code hook scripts (Stop, PreCompact, SessionStart) area/mcp MCP server and tools bug Something isn't working

Projects

None yet

Development

Successfully merging this pull request may close these issues.

5 participants