Skip to content

fix(hooks): restore silent-save visibility on Claude Code 2.1.114#1021

Merged
bensig merged 7 commits intoMemPalace:developfrom
jphein:upstream-fix/silent-save-visibility
Apr 22, 2026
Merged

fix(hooks): restore silent-save visibility on Claude Code 2.1.114#1021
bensig merged 7 commits intoMemPalace:developfrom
jphein:upstream-fix/silent-save-visibility

Conversation

@jphein
Copy link
Copy Markdown
Collaborator

@jphein jphein commented Apr 18, 2026

Two related silent-save fixes exposed by Claude Code 2.1.114's plugin-scope Stop hook dispatch. Both are producer-side — getting clean JSON with systemMessage onto fd 1 where Claude Code reads it. The separate UI-side regression that's preventing the <Line> from persisting on screen is filed at anthropics/claude-code#50542.

Fix 1 — honor silent_save when stop_hook_active: true

Claude Code 2.1.114 sets stop_hook_active: true on every Stop fire after the first in a session (at least for plugin-dispatched hooks). The legacy guard at the top of hook_stop was written for block-mode — when the hook returned {"decision":"block"} to ask Claude to save, a re-fire with the flag set meant "you already blocked, don't block again" so we output {} and bailed.

Silent-save mode (default since #673) never blocks — it saves directly and returns. The flag is meaningless in that mode, but the old guard was still suppressing every auto-save after the first one in a Claude Code session:

  • First fire: stop_hook_active: false → saves fine, systemMessage output
  • Every subsequent fire: stop_hook_active: true → legacy guard returns {}, silent no-op, log stays empty, marker stuck

The fix only applies the guard when the user is actually in block mode:

if str(stop_hook_active).lower() in ("true", "1", "yes"):
    try:
        silent_guard = MempalaceConfig().hook_silent_save
    except Exception:
        silent_guard = True
    if not silent_guard:
        _output({})
        return

Fix 2 — write hook JSON to real stdout

mempalace.mcp_server redirects stdout → stderr at module-level import (Python-level sys.stdout = sys.stderr and fd-level os.dup2(2, 1)) to protect the MCP JSON-RPC channel from ChromaDB's C-level noise. That redirect was added in #864 and has lived peacefully alongside silent-save for months.

Silent-save imports mcp_server transitively via _save_diary_directfrom .mcp_server import tool_diary_write. By the time _output() runs, sys.stdout is stderr, and print(json.dumps(...)) lands on fd 2. Claude Code reads the hook's stdout (fd 1), finds it empty, and never sees the systemMessage payload.

Historically this didn't bite because… well, I'm honestly not sure. The redirect's been in place since early March, silent-save's been in place since early April, and the systemMessage line was rendering through at least 2026-04-17. Something in 2.1.114's dispatch path seems stricter — possibly a shift from reading both streams and parsing the first JSON-like chunk to strictly parsing stdout. Either way, writing to the real fd is the correct long-term fix.

_output() now writes to _REAL_STDOUT_FD (saved by mcp_server before the redirect, exported as a module attribute) via os.write(), falling back to sys.stdout only when unavailable (e.g., hooks_cli used standalone without mcp_server). The full schema including continue: true, suppressOutput: false is emitted for completeness:

{
  "continue": true,
  "suppressOutput": false,
  "systemMessage": "✦ 30 memories woven into the palace — themes"
}

Test

  • Existing suite passes (58/58 in tests/test_hooks_cli.py).
  • Manual verification — bash hooks/mempal-stop-hook.sh 2>/dev/null now shows only the JSON on stdout, 2>&1 >/dev/null shows only the log line on stderr. Cleanly separated streams.

UI-side status

The corresponding <Line> rendering regression in Claude Code 2.1.114 is tracked at anthropics/claude-code#50542. That one's not fixable from our side; this PR ensures we're producing correct output once the client fix lands.

Test plan

  • python -m pytest tests/test_hooks_cli.py -q — 58 tests pass
  • Install the plugin; run Claude Code 2.1.114; confirm diary_* drawers land at the 15-exchange mark
  • Inspect hook script output directly: clean JSON on stdout, log noise on stderr

🤖 Generated with Claude Code

Copilot AI review requested due to automatic review settings April 18, 2026 21:59
jphein added a commit to jphein/mempalace that referenced this pull request Apr 18, 2026
hooks_cli: add continue:true, suppressOutput:false alongside systemMessage
for the full Stop hook schema documented in Claude Code's hook output
format. Doesn't change behaviour (defaults match), but leaves no field
implicit in case the renderer starts requiring the full shape.

README: document that Claude Code 2.1.114 regressed the systemMessage
Line rendering — the `✦ N memories woven` notification flashes for ~1s
and is then discarded instead of persisting as a visible Line. Saves
complete correctly; only the UX is gone. Tracked upstream at
anthropics/claude-code#50542. Fork producer-side fixes in
MemPalace#1021. Workaround: hook_desktop_toast for
notify-send popups.

Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
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 updates the Python hook implementation to keep Claude Code stop-hook output visible/parseable after changes in Claude Code 2.1.114 by ensuring hook JSON is written to “real” stdout and by refining the stop_hook_active guard so it only suppresses behavior in block-mode.

Changes:

  • Update _output() to write JSON to the original stdout file descriptor (when available) rather than the potentially redirected sys.stdout.
  • Change hook_stop() so the stop_hook_active early-return guard is applied only when hook_silent_save is disabled (block mode).

💡 Add Copilot custom instructions for smarter, more guided reviews. Learn how to get started.

Comment thread mempalace/hooks_cli.py Outdated
Comment on lines +137 to +153
"""Print JSON to the real stdout, even if mcp_server has hijacked sys.stdout.

mempalace.mcp_server redirects stdout → stderr at module import (fd and
sys-level) to protect the MCP stdio protocol from ChromaDB's C-level
prints. Silent-save imports it transitively via _save_diary_direct, so
sys.stdout is stderr by the time we get here. Claude Code reads hook
output from fd 1, so we write there directly using the saved fd.
"""
payload = json.dumps(data, indent=2, ensure_ascii=False) + "\n"
try:
from .mcp_server import _REAL_STDOUT_FD
if _REAL_STDOUT_FD is not None:
os.write(_REAL_STDOUT_FD, payload.encode("utf-8"))
return
except Exception:
pass
sys.stdout.write(payload)
Copy link

Copilot AI Apr 18, 2026

Choose a reason for hiding this comment

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

_output() unconditionally importing mempalace.mcp_server has significant side effects: that module redirects sys.stdout to sys.stderr at import time and pulls in heavy dependencies. In environments where fd-level dup/dup2 isn’t available (mcp_server sets _REAL_STDOUT_FD=None), the fallback path here will then write the hook JSON to sys.stdout (now stderr), breaking Claude Code’s expectation that hook responses are on fd 1. Consider avoiding the import: read _REAL_STDOUT_FD only if mempalace.mcp_server is already loaded (via sys.modules), otherwise write to fd 1 directly (or maintain your own saved “real stdout” fd captured at process start).

Suggested change
"""Print JSON to the real stdout, even if mcp_server has hijacked sys.stdout.
mempalace.mcp_server redirects stdoutstderr at module import (fd and
sys-level) to protect the MCP stdio protocol from ChromaDB's C-level
prints. Silent-save imports it transitively via _save_diary_direct, so
sys.stdout is stderr by the time we get here. Claude Code reads hook
output from fd 1, so we write there directly using the saved fd.
"""
payload = json.dumps(data, indent=2, ensure_ascii=False) + "\n"
try:
from .mcp_server import _REAL_STDOUT_FD
if _REAL_STDOUT_FD is not None:
os.write(_REAL_STDOUT_FD, payload.encode("utf-8"))
return
except Exception:
pass
sys.stdout.write(payload)
"""Print JSON to stdout without importing modules that may redirect streams.
If mempalace.mcp_server is already loaded, reuse its saved real stdout fd.
Otherwise, write directly to fd 1 so hook responses still go to stdout even
if sys.stdout has been redirected elsewhere.
"""
payload = (json.dumps(data, indent=2, ensure_ascii=False) + "\n").encode("utf-8")
real_stdout_fd = None
mcp_server_module = sys.modules.get("mempalace.mcp_server") or sys.modules.get(
f"{__package__}.mcp_server" if __package__ else "mcp_server"
)
if mcp_server_module is not None:
real_stdout_fd = getattr(mcp_server_module, "_REAL_STDOUT_FD", None)
try:
os.write(real_stdout_fd if real_stdout_fd is not None else 1, payload)
return
except OSError:
pass
sys.stdout.buffer.write(payload)

Copilot uses AI. Check for mistakes.
Comment thread mempalace/hooks_cli.py Outdated
Comment on lines +145 to +150
payload = json.dumps(data, indent=2, ensure_ascii=False) + "\n"
try:
from .mcp_server import _REAL_STDOUT_FD
if _REAL_STDOUT_FD is not None:
os.write(_REAL_STDOUT_FD, payload.encode("utf-8"))
return
Copy link

Copilot AI Apr 18, 2026

Choose a reason for hiding this comment

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

When writing via os.write(), it’s possible (though uncommon) to get partial writes or EINTR. To make hook output robust, consider looping until the entire payload is written (and handling InterruptedError) rather than assuming a single os.write() writes all bytes.

Copilot uses AI. Check for mistakes.
Comment thread mempalace/hooks_cli.py Outdated
Comment on lines +139 to +143
mempalace.mcp_server redirects stdout → stderr at module import (fd and
sys-level) to protect the MCP stdio protocol from ChromaDB's C-level
prints. Silent-save imports it transitively via _save_diary_direct, so
sys.stdout is stderr by the time we get here. Claude Code reads hook
output from fd 1, so we write there directly using the saved fd.
Copy link

Copilot AI Apr 18, 2026

Choose a reason for hiding this comment

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

The new _output() docstring references a silent-save import path via “_save_diary_direct”, but that function doesn’t exist in this module or elsewhere in the repo. This makes the explanation misleading; please update the wording to match the actual call/import chain that causes mcp_server to be imported, or remove the reference.

Suggested change
mempalace.mcp_server redirects stdoutstderr at module import (fd and
sys-level) to protect the MCP stdio protocol from ChromaDB's C-level
prints. Silent-save imports it transitively via _save_diary_direct, so
sys.stdout is stderr by the time we get here. Claude Code reads hook
output from fd 1, so we write there directly using the saved fd.
Some hook code paths may already have imported ``mempalace.mcp_server``.
That module redirects stdoutstderr at import time (both fd- and
sys-level) to protect the MCP stdio protocol from ChromaDB's C-level
prints. When that has already happened, ``sys.stdout`` no longer points
at the hook response stream, so Claude Code will only see output written
to the saved original stdout fd exposed as ``_REAL_STDOUT_FD``.

Copilot uses AI. Check for mistakes.
Comment thread mempalace/hooks_cli.py Outdated
Comment on lines +233 to +237
try:
from .config import MempalaceConfig
silent_guard = MempalaceConfig().hook_silent_save
except Exception:
silent_guard = True
Copy link

Copilot AI Apr 18, 2026

Choose a reason for hiding this comment

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

In the stop_hook_active guard, the broad except defaulting silent_guard=True means the infinite-loop prevention is disabled if config import/load fails (falling through to normal processing). If this code path is intended as a safety guard, consider narrowing the exception and defaulting to the previous behavior (treat as block mode / keep the guard) when the setting can’t be determined, or at least logging a warning so repeated Stop fires don’t unexpectedly change behavior.

Suggested change
try:
from .config import MempalaceConfig
silent_guard = MempalaceConfig().hook_silent_save
except Exception:
silent_guard = True
silent_guard = False
try:
from .config import MempalaceConfig
except ImportError as exc:
_log(
f"WARNING: Failed to import MempalaceConfig while evaluating stop hook guard: {exc}. "
"Preserving block-mode guard behavior."
)
else:
try:
silent_guard = MempalaceConfig().hook_silent_save
except AttributeError as exc:
_log(
f"WARNING: Failed to read hook_silent_save while evaluating stop hook guard: {exc}. "
"Preserving block-mode guard behavior."
)

Copilot uses AI. Check for mistakes.
Comment thread mempalace/hooks_cli.py
Comment on lines +146 to 156
try:
from .mcp_server import _REAL_STDOUT_FD
if _REAL_STDOUT_FD is not None:
os.write(_REAL_STDOUT_FD, payload.encode("utf-8"))
return
except Exception:
pass
sys.stdout.write(payload)
sys.stdout.flush()


Copy link

Copilot AI Apr 18, 2026

Choose a reason for hiding this comment

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

There are unit tests for hook_stop(), but they patch _output(), so the new “write to real stdout fd” behavior isn’t exercised. Adding a regression test that imports mempalace.mcp_server (to trigger stdout redirection) and then asserts _output() still emits JSON on captured stdout (and not stderr) would help prevent this from breaking again across Claude Code/plugin dispatch changes.

Suggested change
try:
from .mcp_server import _REAL_STDOUT_FD
if _REAL_STDOUT_FD is not None:
os.write(_REAL_STDOUT_FD, payload.encode("utf-8"))
return
except Exception:
pass
sys.stdout.write(payload)
sys.stdout.flush()
payload_bytes = payload.encode("utf-8")
try:
from .mcp_server import _REAL_STDOUT_FD
if _REAL_STDOUT_FD is not None:
os.write(_REAL_STDOUT_FD, payload_bytes)
return
except Exception:
pass
try:
os.write(1, payload_bytes)
return
except OSError:
pass
stream = getattr(sys, "__stdout__", None) or sys.stdout
stream.write(payload)
stream.flush()

Copilot uses AI. Check for mistakes.
jphein added a commit to jphein/mempalace that referenced this pull request Apr 18, 2026
- _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>
@jphein jphein requested a review from igorls as a code owner April 18, 2026 22:50
@jphein
Copy link
Copy Markdown
Collaborator Author

jphein commented Apr 18, 2026

Addressed all five Copilot review points:

  • Unconditional import: _output() now uses sys.modules.get() to check if mcp_server is already loaded rather than importing it — avoids triggering the stdout redirect as a side effect in any process that calls _output() cold.
  • Partial writes: added a write-all loop around os.write(), retrying on InterruptedError and falling back to sys.stdout.buffer on OSError.
  • Docstring: removed the inaccurate _save_diary_direct reference; the doc now just describes what the function actually does.
  • Broad except in guard: narrowed to ImportError/AttributeError, default silent_guard=False (safe: preserves block-mode loop prevention when config can't be loaded), logs a warning instead of silently changing behavior.
  • Regression test: two new tests — one asserts _output() writes to the _REAL_STDOUT_FD pipe when mcp_server is in sys.modules, one asserts it falls back to fd 1 when it isn't. Both verify the JSON payload round-trips correctly.

jphein added a commit to jphein/mempalace that referenced this pull request Apr 19, 2026
- _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>
jphein added a commit to jphein/mempalace that referenced this pull request Apr 19, 2026
Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
jphein added a commit to jphein/mempalace that referenced this pull request Apr 19, 2026
… count 8 open

Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
jphein and others added 4 commits April 18, 2026 18:05
Claude Code 2.1.114 passes stop_hook_active:true on every Stop fire
after the first in a session (plugin-dispatched hooks in particular).
The legacy guard at line 426 was written for block-mode, where a
re-fire with the flag set meant "you already blocked, don't block
again" — correct loop prevention when the hook returns
{"decision":"block"}.

Silent-save mode (default since MemPalace#673) never blocks — it saves
directly and returns. The flag is meaningless there, so the old
guard was suppressing every auto-save after the first one in a
Claude Code session. Symptom: terminal never shows the "✦ N
memories woven" notification again, hook.log stays silent, save
marker stuck.

Fix: only skip on stop_hook_active when block mode is configured.
Silent mode runs through as normal — the save is deterministic and
idempotent, no loop risk.

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

mempalace.mcp_server redirects stdout → stderr at module-level import
(both Python-level and fd-level via os.dup2) to protect the MCP stdio
protocol from ChromaDB's C-level noise. Silent-save imports mcp_server
transitively via _save_diary_direct, so by the time _output() calls
print(), sys.stdout is actually stderr.

Claude Code reads hook output from fd 1. With the redirect in effect,
fd 1 points to fd 2, so our {"systemMessage": "✦ N memories woven..."}
JSON lands on stderr and Claude Code never renders it. The save still
happens, the marker still advances — the user just never sees the
beautiful checkpoint notification in their terminal.

Fix: _output() now writes to _REAL_STDOUT_FD (saved by mcp_server before
the redirect) via os.write(), falling back to sys.stdout only when the
saved fd is unavailable (e.g., hooks_cli imported without mcp_server).

Test: bash hook script 2>/dev/null now shows only the JSON;
2>&1 >/dev/null shows only the Diary entry log line — clean separation
restored.

Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
- _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>
Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
@jphein jphein force-pushed the upstream-fix/silent-save-visibility branch from 5723f04 to 1531a25 Compare April 19, 2026 01:05
Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
@bensig
Copy link
Copy Markdown
Collaborator

bensig commented Apr 19, 2026

Code review

Found 1 issue:

  1. Guard fallback defaults to wrong state, re-enabling the bug this PR fixes. silent_guard is initialized to False (line 244). When MempalaceConfig import fails (ImportError) or property access fails (AttributeError), neither exception handler reassigns silent_guard -- it stays False. Then if not silent_guard: evaluates to True, firing the guard and suppressing the save. This is the exact opposite of the intended behavior: in silent mode (the default), saves should proceed when config is unavailable. The safe default should be silent_guard = True so that on any error, the guard is skipped and saves are not suppressed.

if str(stop_hook_active).lower() in ("true", "1", "yes"):
silent_guard = False
try:
from .config import MempalaceConfig
except ImportError as exc:
_log(
f"WARNING: could not import MempalaceConfig for stop guard: {exc}; preserving block-mode guard"
)
else:
try:
silent_guard = MempalaceConfig().hook_silent_save
except AttributeError as exc:
_log(
f"WARNING: could not read hook_silent_save: {exc}; preserving block-mode guard"
)
if not silent_guard:
_output({})
return

🤖 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 pls see comment above

…suppress saves

Addresses bensig's review on PR MemPalace#1021.

silent_guard was initialized to False, so when both MempalaceConfig
import and .hook_silent_save attribute access failed, silent_guard
stayed False. Then `if not silent_guard:` fired and returned empty —
silently dropping the save. In silent mode (the default since v3.3.0),
saves should ALWAYS proceed on config-read failure. Changing the
initial value to True makes that the safe default.

Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
jphein added a commit to jphein/mempalace that referenced this pull request Apr 19, 2026
…suppress saves

Addresses bensig's review on PR MemPalace#1021.

silent_guard was initialized to False, so when both MempalaceConfig
import and .hook_silent_save attribute access failed, silent_guard
stayed False. Then `if not silent_guard:` fired and returned empty —
silently dropping the save. In silent mode (the default since v3.3.0),
saves should ALWAYS proceed on config-read failure. Changing the
initial value to True makes that the safe default.

Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
@jphein
Copy link
Copy Markdown
Collaborator Author

jphein commented Apr 19, 2026

Good catch, thanks. Fixed in 2629ae5: silent_guard now defaults to True, and the warning log text was updated to say "defaulting to silent mode" so the behaviour in the logs matches the new default. Tests still green on all 3 platforms + both Python versions.

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) <noreply@anthropic.com>
@jphein
Copy link
Copy Markdown
Collaborator Author

jphein commented Apr 19, 2026

Small correction on my earlier comment — the CI matrix is 3 Python versions (3.9, 3.11, 3.13) × 3 platforms, not 2. All still green.

@jphein
Copy link
Copy Markdown
Collaborator Author

jphein commented Apr 19, 2026

One more correction on the same thread — the "tests still green" claim I made was premature: the CI run for 2629ae5 was actually still in flight when I posted, and it failed shortly after on a ruff format rule (one log call needed to be on a single line). Fix pushed in d657626; that run is green across all 3 platforms × 3 Python versions. Apologies for the sloppy narration.

@bensig bensig merged commit 02aafc0 into MemPalace:develop Apr 22, 2026
6 checks passed
jphein added a commit to jphein/mempalace that referenced this pull request Apr 22, 2026
… (2026-04-22)

Ben's batched queue-clear pass merged four PRs at 00:38 UTC:
graph cache (MemPalace#661), deterministic hook saves (MemPalace#673), Claude Code
2.1.114 hook stdout + silent_save guard (MemPalace#1021), and upstream's
own MemPalace#851 pagination fix (closing MemPalace#1036 as superseded).

Moved four rows out of the "Fork Changes" / "Fork change queue"
tables into their respective merged-upstream history sections.
Intro sentence PR count reduced from 7 → 4 open.

Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
sha2fiddy added a commit to sha2fiddy/mempalace that referenced this pull request Apr 22, 2026
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) <noreply@anthropic.com>
@jphein jphein deleted the upstream-fix/silent-save-visibility branch April 25, 2026 14:53
sha2fiddy added a commit to sha2fiddy/mempalace that referenced this pull request Apr 26, 2026
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`.
sha2fiddy added a commit to sha2fiddy/mempalace that referenced this pull request Apr 29, 2026
sha2fiddy added a commit to sha2fiddy/mempalace that referenced this pull request Apr 29, 2026
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

3 participants