Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
16 changes: 16 additions & 0 deletions changelog.d/1128.fixed.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,16 @@
- Fixed: the nested `claude -p` the summarizer spawns for a save, an NDC
compression, or a staging consolidation inherited `CLAUDE_CODE_EFFORT_LEVEL`
from the parent environment. Anyone who sets Claude's own effort to `max` in
`~/.claude/settings.json` (the only way to keep it at `max` across sessions)
had every nested Haiku call run at max effort instead of the plugin's own
choice: small saves still finished but cost about five times the output
tokens, and the larger NDC and consolidation calls stopped finishing inside
their 180s timeout at all. Memory kept working but got slower on every save,
then `now.md` stopped rotating once NDC itself always timed out, grew past
`session_start_max_bytes`, and SessionStart silently stopped injecting it --
with nothing in the logs explaining why. `CLAUDE_CODE_EFFORT_LEVEL` is now
removed from the nested call's environment the same way the plugin already
removes the parent session's own identity variables, and restored afterward.
`CLAUDE_EFFORT` is deliberately left alone: it is not a Claude Code session
variable, and unsetting only `CLAUDE_CODE_EFFORT_LEVEL` was enough to fix
the measured timeouts (#1128).
12 changes: 8 additions & 4 deletions docs/configuration.md
Original file line number Diff line number Diff line change
Expand Up @@ -110,10 +110,14 @@ does not pass as that session
back afterwards: `CLAUDECODE`, `CLAUDE_JOB_DIR`, `CLAUDE_PROJECT_DIR`,
`CLAUDE_CODE_SESSION_ID`, `CLAUDE_CODE_ENTRYPOINT`, `CLAUDE_CODE_CHILD_SESSION`,
`CLAUDE_CODE_SESSION_ATTENDED`, `CLAUDE_CODE_EXECPATH`, `CLAUDE_CODE_MESSAGING_SOCKET`,
`CLAUDE_CODE_SSE_PORT` -- the reason for each is a comment beside the code in
`pipeline/summarizer.py`. `CLAUDE_CODE_MESSAGING_TOKEN`, the handshake paired with the messaging
socket, is not on the list and reaches the nested call: with the socket removed, the nested
call has no channel to present it on. Your login credential is never on that list
`CLAUDE_CODE_SSE_PORT`, `CLAUDE_CODE_EFFORT_LEVEL` -- the reason for each is a comment beside
the code in `pipeline/summarizer.py`. `CLAUDE_CODE_MESSAGING_TOKEN`, the handshake paired with
the messaging socket, is not on the list and reaches the nested call: with the socket removed,
the nested call has no channel to present it on. `CLAUDE_EFFORT` is not on the list either and
also reaches the nested call -- only `CLAUDE_CODE_EFFORT_LEVEL` outranks `--effort` and every
settings-file source, so only it needs stripping
([#1128](https://github.com/Digital-Process-Tools/claude-remember/issues/1128)). Your login
credential is never on that list
([#131](https://github.com/Digital-Process-Tools/claude-remember/issues/131)); provider
settings such as Bedrock selection are not either, so they reach the nested call
([#316](https://github.com/Digital-Process-Tools/claude-remember/issues/316)). To keep any
Expand Down
11 changes: 11 additions & 0 deletions pipeline/summarizer.py
Original file line number Diff line number Diff line change
Expand Up @@ -382,6 +382,15 @@ def _without_session_env():
saved_messaging_socket = os.environ.pop("CLAUDE_CODE_MESSAGING_SOCKET", None)
# The parent's IDE connection; the child would attach to the user's IDE.
saved_sse_port = os.environ.pop("CLAUDE_CODE_SSE_PORT", None)
# #1128: outranks --effort and every settings-file source, so a parent
# session that pins "max" (the only way to keep max effort across
# sessions, since effortLevel stops at xhigh) sends Haiku into max-effort
# thinking on a summarization task -- NDC and consolidation then blow
# their 180s call timeout. CLAUDE_EFFORT is deliberately NOT on this
# list: the reporter measured that unsetting only this one variable fixed
# it, and CLAUDE_EFFORT is not a Claude Code session variable the child
# would otherwise look like a resumable part of.
saved_effort_level = os.environ.pop("CLAUDE_CODE_EFFORT_LEVEL", None)
try:
yield
finally:
Expand All @@ -405,6 +414,8 @@ def _without_session_env():
os.environ["CLAUDE_CODE_MESSAGING_SOCKET"] = saved_messaging_socket
if saved_sse_port is not None:
os.environ["CLAUDE_CODE_SSE_PORT"] = saved_sse_port
if saved_effort_level is not None:
os.environ["CLAUDE_CODE_EFFORT_LEVEL"] = saved_effort_level


@contextlib.contextmanager
Expand Down
31 changes: 30 additions & 1 deletion tests/test_summarizer_env_898.py
Original file line number Diff line number Diff line change
Expand Up @@ -55,7 +55,9 @@
# #204 already covered, and the IDE port (reasoned: set when an IDE is
# attached, not observed in that dump). Same names, same order as round 16's
# shipped `haiku.strip_session_env`, minus the messaging handshake (round
# 18). A future Claude Code session variable that is not here reaches the
# 18), plus CLAUDE_CODE_EFFORT_LEVEL (#1128 -- not from round 16's dump or
# `strip_session_env`; it outranks --effort and every settings-file source).
# A future Claude Code session variable that is not here reaches the
# summarizer until a release adds it to `_without_session_env` AND here.
EXPECTED_SESSION_NAMES = (
"CLAUDECODE",
Expand All @@ -68,7 +70,15 @@
"CLAUDE_CODE_EXECPATH",
"CLAUDE_CODE_MESSAGING_SOCKET",
"CLAUDE_CODE_SSE_PORT",
# #1128: outranks --effort and every settings-file source; a parent that
# pins "max" for its own sessions sends Haiku into max-effort thinking on
# a summarization task, which blows the 180s call timeout.
"CLAUDE_CODE_EFFORT_LEVEL",
)
# #1128: the *other* effort variable. The reporter measured that unsetting only
# CLAUDE_CODE_EFFORT_LEVEL fixed the timeouts and left this one alone -- it is
# not a Claude Code session variable, so it is inherited like any other.
OTHER_EFFORT_VAR = "CLAUDE_EFFORT"
# Round 18: inherited, no longer on the list.
MESSAGING_HANDSHAKE = "CLAUDE_CODE_MESSAGING_TOKEN"

Expand Down Expand Up @@ -265,6 +275,25 @@ def test_child_inherits_an_arbitrary_variable(mock_run, monkeypatch, isolated_co
assert seen[-1]["env"].get("REMEMBER_TEST_ARBITRARY_898") == "kept"


@patch("pipeline.summarizer.subprocess.run")
def test_effort_level_is_stripped_other_effort_var_is_inherited(
mock_run, monkeypatch, isolated_config):
"""#1128: CLAUDE_CODE_EFFORT_LEVEL outranks --effort and every settings
file, so a parent that pins "max" sends Haiku into max-effort thinking on
a summarization task and blows the call's own timeout. CLAUDE_EFFORT is
the positive control: the reporter measured that unsetting only the
_LEVEL variable fixed it, and it is not a Claude Code session variable,
so it must reach the child like any other."""
monkeypatch.setenv("CLAUDE_CODE_EFFORT_LEVEL", "max")
monkeypatch.setenv(OTHER_EFFORT_VAR, "max")
seen = _capture(mock_run)
call_haiku("p")
env = seen[-1]["env"]
assert "CLAUDE_CODE_EFFORT_LEVEL" not in env
assert env.get(OTHER_EFFORT_VAR) == "max", "positive control: not everything is stripped"
assert os.environ.get("CLAUDE_CODE_EFFORT_LEVEL") == "max", "restored after the spawn"


@pytest.mark.parametrize("name", EXPECTED_SESSION_NAMES)
@patch("pipeline.summarizer.subprocess.run")
def test_each_listed_session_variable_is_removed(mock_run, name, monkeypatch, isolated_config):
Expand Down
Loading