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
2 changes: 1 addition & 1 deletion .claude-plugin/plugin.json
Original file line number Diff line number Diff line change
@@ -1,7 +1,7 @@
{
"name": "remember",
"description": "Continuous memory for Claude Code. Extracts, summarizes, and compresses conversations into tiered daily logs. Claude remembers what you did yesterday.",
"version": "0.8.1",
"version": "0.8.2",
"author": {
"name": "Digital Process Tools"
},
Expand Down
6 changes: 6 additions & 0 deletions CHANGELOG.md
Original file line number Diff line number Diff line change
Expand Up @@ -7,6 +7,12 @@ and this project adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0

## [Unreleased]

## [0.8.2] — Oversized-extract guard keeps long sessions saving

### Fixed

- **A very long session could silently halt all memory saves** ([#96](https://github.com/Digital-Process-Tools/claude-remember/issues/96)) — a single long-lived session can grow an extract larger than Haiku's context window. `build-prompt` embedded the full extract with no size cap, so the Haiku call failed, the save aborted, and daily rotation stopped. Worse, it was self-reinforcing: a failed save never advanced the saved position, so the same session re-extracted the full transcript and failed identically on every subsequent save. The extract is now capped at `thresholds.extract_max_bytes` (default 300 KB), keeping the most-recent tail with a truncation note so the summary still reflects current work. Set to `0` to disable. Thanks to [@selvi5006-commits](https://github.com/selvi5006-commits) for the precise diagnosis and a tested patch.

## [0.8.1] — Handoff survives context-preview truncation

### Fixed
Expand Down
5 changes: 3 additions & 2 deletions README.md
Original file line number Diff line number Diff line change
Expand Up @@ -6,7 +6,7 @@
[![Python](https://img.shields.io/badge/python-3.9%2B-blue)](https://www.python.org/)
[![OS](https://img.shields.io/badge/tested%20on-Linux%20%7C%20macOS%20%7C%20Windows-blue)](https://github.com/Digital-Process-Tools/claude-remember/actions/workflows/tests.yml)
[![License](https://img.shields.io/badge/license-Community-brightgreen)](LICENSE)
[![Version](https://img.shields.io/badge/version-0.8.1-orange)](.claude-plugin/plugin.json)
[![Version](https://img.shields.io/badge/version-0.8.2-orange)](.claude-plugin/plugin.json)

Claude Code starts every session blank. It doesn't know what you worked on yesterday, what conventions your team follows, or what mistakes it already made. You re-explain everything, every time.

Expand Down Expand Up @@ -35,7 +35,7 @@ To update later:

Claude Remember is also available in the official Anthropic Marketplace. In Claude Code, type `/plugin` and search for "remember".

**Known issue — stuck on v0.5.0:** The Anthropic marketplace is still serving v0.5.0, which has known bugs ([#54](https://github.com/Digital-Process-Tools/claude-remember/issues/54) hook stderr redirect fails on first session, [#14](https://github.com/Digital-Process-Tools/claude-remember/issues/14) NDC subshell killed by `set -e`). Anthropic takes a long time to roll updates to the official marketplace. All of these are fixed in v0.8.1 — install from the DPT marketplace above to get the current version.
**Known issue — stuck on v0.5.0:** The Anthropic marketplace is still serving v0.5.0, which has known bugs ([#54](https://github.com/Digital-Process-Tools/claude-remember/issues/54) hook stderr redirect fails on first session, [#14](https://github.com/Digital-Process-Tools/claude-remember/issues/14) NDC subshell killed by `set -e`). Anthropic takes a long time to roll updates to the official marketplace. All of these are fixed in v0.8.2 — install from the DPT marketplace above to get the current version.

**Known issue — `plugin update`:** The official marketplace's `plugin update` command may report "already at latest version" even when it's not — it checks a stale local cache without pulling first ([#37252](https://github.com/anthropics/claude-code/issues/37252), [#38271](https://github.com/anthropics/claude-code/issues/38271)). Another reason to use our marketplace instead.

Expand Down Expand Up @@ -214,6 +214,7 @@ Put cross-project preferences (timezone, cooldowns) in `~/.remember/config.json`
| `cooldowns.git_backup_seconds` | `900` | Minimum seconds between auto-backup commits (no-op if `~/.remember/` is not a git repo) |
| `thresholds.min_human_messages` | `3` | Minimum messages before saving |
| `thresholds.delta_lines_trigger` | `50` | Tool call output lines that trigger auto-save |
| `thresholds.extract_max_bytes` | `300000` | Max UTF-8 size of the session extract sent to Haiku. Larger extracts are truncated to their most-recent tail so a very long session can't overflow the model's context window and silently stall saves. `0` disables the cap. |
| `features.ndc_compression` | `true` | Enable hourly compression of daily files |
| `features.recovery` | `true` | Recover missed saves on session start |
| `timezone` | _(system local)_ | IANA name (e.g. `America/New_York`, `Europe/Paris`) for timestamps and daily file boundaries. Omit or leave empty to use the system clock's local zone. Set this explicitly on a VPS whose system clock is UTC. |
Expand Down
3 changes: 2 additions & 1 deletion config.example.json
Original file line number Diff line number Diff line change
Expand Up @@ -7,7 +7,8 @@
},
"thresholds": {
"min_human_messages": 3,
"delta_lines_trigger": 50
"delta_lines_trigger": 50,
"extract_max_bytes": 300000
},
"features": {
"ndc_compression": true,
Expand Down
18 changes: 18 additions & 0 deletions pipeline/shell.py
Original file line number Diff line number Diff line change
Expand Up @@ -100,6 +100,7 @@ def cmd_build_prompt(
time: str,
branch: str,
output_file: str,
max_extract_bytes: int = 0,
) -> None:
"""Build the save-summary prompt and write it to an output file.

Expand All @@ -112,12 +113,28 @@ def cmd_build_prompt(
time: Current timestamp string (e.g., "14:32").
branch: Current git branch name.
output_file: Path where the assembled prompt will be written.
max_extract_bytes: Upper bound on the extract's UTF-8 byte size. A
long-lived session can accumulate an extract larger than Haiku's
context window, making the prompt unsendable and silently halting
daily rotation (#96). When the extract exceeds this size, keep only
the most-recent tail (the work worth summarizing) and prepend a
truncation note. ``0`` disables the cap.
"""
with open(extract_file, encoding="utf-8", errors="replace") as f:
extract = f.read().strip()
with open(last_entry_file, encoding="utf-8", errors="replace") as f:
last_entry = f.read().strip()

if max_extract_bytes > 0:
raw = extract.encode("utf-8")
if len(raw) > max_extract_bytes:
kept = raw[-max_extract_bytes:].decode("utf-8", errors="replace")
extract = (
f"[NOTE: transcript truncated to the last {max_extract_bytes} "
f"of {len(raw)} bytes — summarize the most recent work below]"
f"\n\n{kept}"
)

prompt = build_save_prompt(
time=time,
branch=branch,
Expand Down Expand Up @@ -349,6 +366,7 @@ def main() -> None:
time=sys.argv[4],
branch=sys.argv[5],
output_file=sys.argv[6],
max_extract_bytes=int(sys.argv[7]) if len(sys.argv) > 7 else 0,
)
elif cmd == "build-ndc-prompt":
cmd_build_ndc_prompt(memory_file=sys.argv[2], output_file=sys.argv[3])
Expand Down
3 changes: 2 additions & 1 deletion scripts/save-session.sh
Original file line number Diff line number Diff line change
Expand Up @@ -162,7 +162,8 @@ fi
TMP_PROMPT=$(mktemp "${TMPDIR:-/tmp}"/remember-prompt-XXXXXX)
CLEANUP_FILES+=("$TMP_PROMPT")

cd "$PIPELINE_DIR" && $PYTHON -m pipeline.shell build-prompt "$EXTRACT_FILE" "$TMP_LAST_ENTRY" "$CURRENT_TIME" "$BRANCH" "$TMP_PROMPT"
EXTRACT_MAX_BYTES=$(config ".thresholds.extract_max_bytes" 300000)
cd "$PIPELINE_DIR" && $PYTHON -m pipeline.shell build-prompt "$EXTRACT_FILE" "$TMP_LAST_ENTRY" "$CURRENT_TIME" "$BRANCH" "$TMP_PROMPT" "$EXTRACT_MAX_BYTES"

[ ! -s "$TMP_PROMPT" ] && { log "prompt" "ERROR: empty"; exit 1; }
grep -q '{{TIME}}\|{{BRANCH}}\|{{LAST_ENTRY}}\|{{EXTRACT}}' "$TMP_PROMPT" && { log "prompt" "ERROR: unsubstituted placeholders in prompt"; exit 1; }
Expand Down
56 changes: 56 additions & 0 deletions tests/test_prompts.py
Original file line number Diff line number Diff line change
Expand Up @@ -9,6 +9,7 @@
sys.path.insert(0, os.path.join(os.path.dirname(__file__), ".."))

from pipeline import prompts
from pipeline import shell


def _make_template(tmpdir: str, name: str, content: str) -> None:
Expand Down Expand Up @@ -119,3 +120,58 @@ def test_build_consolidation_prompt_empty_staging(monkeypatch):
assert "Staging:\n" in result
assert "# Recent" in result
assert "# Archive" in result


def _run_build_prompt(monkeypatch, extract, max_extract_bytes):
"""Drive shell.cmd_build_prompt with a stub template and return the prompt."""
with tempfile.TemporaryDirectory() as d:
_make_template(d, "save-session.prompt.txt", "{{EXTRACT}}")
monkeypatch.setattr(prompts, "PROMPTS_DIR", d)

extract_file = os.path.join(d, "extract.txt")
last_entry_file = os.path.join(d, "last.txt")
output_file = os.path.join(d, "prompt.txt")
with open(extract_file, "w", encoding="utf-8") as f:
f.write(extract)
with open(last_entry_file, "w", encoding="utf-8") as f:
f.write("(no previous entry)")

shell.cmd_build_prompt(
extract_file=extract_file,
last_entry_file=last_entry_file,
time="10:30",
branch="master",
output_file=output_file,
max_extract_bytes=max_extract_bytes,
)
with open(output_file, encoding="utf-8") as f:
return f.read()


def test_build_prompt_caps_oversized_extract(monkeypatch):
"""An extract larger than the cap is truncated to its tail with a NOTE."""
extract = "HEAD_MARKER\n" + ("x" * 5000) + "\nTAIL_MARKER"
result = _run_build_prompt(monkeypatch, extract, max_extract_bytes=200)

assert "TAIL_MARKER" in result # most-recent work survives
assert "HEAD_MARKER" not in result # oldest content dropped
assert "truncated to the last 200" in result
# Body (note + kept tail) stays within cap + a small note allowance.
assert len(result.encode("utf-8")) < 200 + 200


def test_build_prompt_keeps_small_extract_intact(monkeypatch):
"""An extract under the cap is passed through unchanged (no NOTE)."""
extract = "[HUMAN] hi\n[AGENT] hello"
result = _run_build_prompt(monkeypatch, extract, max_extract_bytes=300000)

assert result == extract
assert "truncated" not in result


def test_build_prompt_cap_disabled_with_zero(monkeypatch):
"""max_extract_bytes=0 disables the cap entirely (back-compat default)."""
extract = "A" * 10000
result = _run_build_prompt(monkeypatch, extract, max_extract_bytes=0)

assert result == extract
16 changes: 16 additions & 0 deletions tests/test_shell.py
Original file line number Diff line number Diff line change
Expand Up @@ -630,6 +630,22 @@ def test_main_dispatches_build_prompt():
time="15m",
branch="main",
output_file="out",
max_extract_bytes=0,
)


def test_main_dispatches_build_prompt_with_max_extract_bytes():
with patch("pipeline.shell.cmd_build_prompt") as mock_fn:
with patch("sys.argv",
["shell.py", "build-prompt", "ef", "lef", "15m", "main", "out", "300000"]):
main()
mock_fn.assert_called_once_with(
extract_file="ef",
last_entry_file="lef",
time="15m",
branch="main",
output_file="out",
max_extract_bytes=300000,
)


Expand Down
Loading