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 README.md
Original file line number Diff line number Diff line change
Expand Up @@ -648,7 +648,7 @@ mcp_servers:
memory:
provider: agentmemory

Verify with `curl http://localhost:3111/agentmemory/health`. Open http://localhost:3113 for the real-time viewer. For deeper 6-hook memory provider integration (pre-LLM context injection, turn capture, MEMORY.md mirroring, system prompt block), copy integrations/hermes from the agentmemory repo to ~/.hermes/plugins/agentmemory.
Verify with `curl http://localhost:3111/agentmemory/health`. Open http://localhost:3113 for the real-time viewer. For deeper 6-hook memory provider integration (pre-LLM context injection, turn capture, archived local memory removals, system prompt block), copy integrations/hermes from the agentmemory repo to ~/.hermes/plugins/agentmemory.
```

Full guide: [`integrations/hermes/`](integrations/hermes/)
Expand Down
226 changes: 226 additions & 0 deletions docs/todos/2026-06-19-issue-335-hermes-memory-write-archive/plan.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,226 @@
# Hermes Memory Write Archive Implementation Plan

> **For agentic workers:** REQUIRED SUB-SKILL: Use superpowers:subagent-driven-development (recommended) or superpowers:executing-plans to implement this plan task-by-task. Steps use checkbox (`- [ ]`) syntax for tracking.

**Goal:** Fix Hermes `on_memory_write` sync semantics so add/update do not duplicate local memory into agentmemory and remove archives deleted local memory content.

**Architecture:** Keep the change inside `integrations/hermes/__init__.py`. Use existing `/agentmemory/remember` for archival because it preserves full content and already supports `project`, `sessionId`, `files`, `tags`, `external_id`, `metadata`, and `agentId`. Tests use the existing Python import/monkeypatch pattern from `test/integration-plaintext-http.test.ts`.

**Tech Stack:** Python Hermes plugin, TypeScript/Vitest test harness, existing agentmemory REST `/remember`.

---

Spec path: none. Source of truth is GitHub issue #335, the approved arena synthesis in `docs/todos/2026-06-19-issue-335-hermes-memory-write-archive/todo.md`, and the user's approval to implement.

GitHub PR prep: mandatory after implementation, but remote fetch/push/PR/issue comments require separate current-turn approval. Security-sensitive surface: host-integration hook behavior in `integrations/hermes/__init__.py`.

## Files

- Modify: `integrations/hermes/__init__.py`
- Modify: `integrations/hermes/README.md`
- Modify: `README.md`
- Modify: `test/integration-plaintext-http.test.ts`
- Modify: `docs/todos/2026-06-19-issue-335-hermes-memory-write-archive/todo.md`

## Task 1: Red Tests For New Hook Semantics

**Files:**
- Modify: `test/integration-plaintext-http.test.ts`

- [ ] **Step 1: Add failing Python-backed tests**

Add tests under `describe("Hermes plaintext bearer guard", ...)` or a nearby Hermes-specific describe that:

```typescript
it("skips add and update memory writes and archives remove actions", () => {
const script = String.raw`
import importlib.util
import os
import time
from pathlib import Path

for key in ("AGENTMEMORY_SECRET", "AGENTMEMORY_URL", "AGENTMEMORY_REQUIRE_HTTPS", "AGENTMEMORY_AGENT_ID", "AGENTMEMORY_HERMES_ARCHIVE_REMOVES"):
os.environ.pop(key, None)

spec = importlib.util.spec_from_file_location("agentmemory_hermes", "integrations/hermes/__init__.py")
mod = importlib.util.module_from_spec(spec)
assert spec.loader is not None
spec.loader.exec_module(mod)

provider = mod.AgentMemoryProvider()
hermes_home = Path(os.environ["HOME"]) / "custom-hermes"
hermes_home.mkdir()
provider.save_config({"url": "https://memory.example"}, str(hermes_home))

calls = []
def fake_api(base, path, body=None, method="POST", secret=""):
calls.append({"base": base, "path": path, "body": body})
return {"success": True}

mod._api = fake_api
provider.initialize(
"session-335",
hermes_home=str(hermes_home),
cwd="/tmp/project",
agent_identity="hermes-reviewer",
)
calls.clear()

provider.on_memory_write("add", "MEMORY.md", "added memory", session_id="session-335")
provider.on_memory_write("update", "MEMORY.md", "updated memory", session_id="session-335")
time.sleep(0.2)
assert calls == [], calls

provider.on_memory_write("remove", "MEMORY.md", "removed local memory", session_id="session-335")
time.sleep(0.2)

assert len(calls) == 1, calls
call = calls[0]
assert call["path"] == "remember", call
body = call["body"]
assert body["agentId"] == "hermes-reviewer", body
assert body["project"] == "/tmp/project", body
assert body["sessionId"] == "session-335", body
assert body["files"] == ["MEMORY.md"], body
assert body["tags"] == ["hermes", "memory-remove", "archive"], body
assert body["metadata"]["source"] == "hermes", body
assert body["metadata"]["hook"] == "on_memory_write", body
assert body["metadata"]["action"] == "remove", body
assert body["metadata"]["target"] == "MEMORY.md", body
assert body["external_id"].startswith("hermes:on_memory_write:remove:"), body
assert "Archived removed Hermes local memory entry from MEMORY.md" in body["content"], body
assert "removed local memory" in body["content"], body

calls.clear()
provider.on_memory_write("remove", "MEMORY.md", "removed local memory", session_id="session-335")
provider.on_memory_write("remove", "USER.md", "removed local memory", session_id="session-335")
provider.on_memory_write("remove", "MEMORY.md", "different removed memory", session_id="session-335")
time.sleep(0.2)
ids = [call["body"]["external_id"] for call in calls]
assert ids[0] == body["external_id"], ids
assert ids[1] != body["external_id"], ids
assert ids[2] != body["external_id"], ids
`;
const result = spawnSync("python3", ["-c", script], {
cwd: process.cwd(),
env: { ...process.env, HOME: home },
encoding: "utf8",
});
expect(result.status, result.stderr || result.stdout).toBe(0);
});
```

Also add tests for blank remove no-op and `AGENTMEMORY_HERMES_ARCHIVE_REMOVES=0` opt-out.

- [ ] **Step 2: Update existing expectations that assumed add mirroring**

In saved-config/runtime and agent identity tests, replace `provider.on_memory_write("add", ...)` assumptions with `remove` calls where the test needs to prove URL/secret/agentId propagation.

- [ ] **Step 3: Run targeted red test**

Run: `corepack pnpm test test/integration-plaintext-http.test.ts`

Expected before implementation: failures showing add/update still call `remember` and remove does not archive.

## Task 2: Implement Hermes Archive-On-Remove

**Files:**
- Modify: `integrations/hermes/__init__.py`

- [ ] **Step 1: Add local helpers**

Add `hashlib` import. Add helpers:

```python
def _archive_removed_memory_enabled(saved_config: dict | None = None) -> bool:
env = os.environ.get("AGENTMEMORY_HERMES_ARCHIVE_REMOVES")
if env is not None:
return env.strip().lower() not in ("0", "false", "no", "off")
config = saved_config if saved_config is not None else {}
value = config.get("archive_removed_memory")
if isinstance(value, bool):
return value
if isinstance(value, str):
return value.strip().lower() not in ("0", "false", "no", "off")
return True
```

Store `self._archive_removed_memory` during `initialize()` from saved config.

- [ ] **Step 2: Replace `on_memory_write` behavior**

Implement:
- return immediately for actions other than `remove`
- return if archive flag is false
- strip and ignore blank content
- build stable external ID with SHA-256 over target + NUL + stripped content
- build archive content prefix
- call `_api_bg(self._base, "remember", self._with_agent_id({...}), secret=self._secret)`

Payload fields:
- `content`
- `type: "fact"`
- `project: self._project`
- `sessionId: kwargs.get("session_id", self._session_id)`
- `files: [target]` when target is non-empty
- `tags: ["hermes", "memory-remove", "archive"]`
- `external_id`
- `metadata` with `source`, `hook`, `action`, `target`

- [ ] **Step 3: Run targeted green tests**

Run: `corepack pnpm test test/integration-plaintext-http.test.ts`

Expected after implementation: pass.

## Task 3: Documentation And Focused Verification

**Files:**
- Modify: `integrations/hermes/README.md`
- Modify: `README.md`
- Modify: `docs/todos/2026-06-19-issue-335-hermes-memory-write-archive/todo.md`

- [ ] **Step 1: Update README wording**

Change the hook bullet from mirroring all MEMORY.md writes to explicitly state:
- add/update are skipped to avoid duplicate agentmemory memories
- remove archives the removed content as an agentmemory archive entry
- `AGENTMEMORY_HERMES_ARCHIVE_REMOVES=0` disables remove archival

Also update the Hermes install prompt in `integrations/hermes/README.md` and the root README Hermes install/docs section so no public docs still promise broad memory-write mirroring.

- [ ] **Step 2: Verify no stale mirroring docs remain**

Run: `rg -n "MEMORY\\.md mirroring|memory-write mirroring|mirrors MEMORY|mirrors MEMORY\\.md|mirrors .*MEMORY" README.md integrations/hermes/README.md`

Expected: no stale statements claiming add/update MEMORY.md writes are mirrored to agentmemory. Statements about archiving removed local memory are allowed.

- [ ] **Step 3: Run focused Hermes tests**

Run: `corepack pnpm test test/integration-plaintext-http.test.ts test/hermes-plugin.test.ts`

Expected: pass.

- [ ] **Step 4: Run broader repo-native check if feasible**

Run: `corepack pnpm test`

Expected: pass, or record blocker and closest targeted verification.

- [ ] **Step 5: Run security checks**

Run: `semgrep scan --config p/default --error --metrics=off .`

Expected: pass, or record findings/blockers. If staging/committing, also run `gitleaks protect --staged --redact` after staging intended files.

- [ ] **Step 6: Update task record**

Update the Feature / Verification Matrix with test evidence, docs evidence, and any caveats.

## Plan Self-Review

- Spec coverage: add/update skip, remove archive, opt-out, payload metadata, docs, and verification are each covered.
- Placeholder scan: no TBD/TODO placeholders.
- Type consistency: Python helper names and payload field names match existing `/remember` API and Hermes provider patterns.
- Boundary review: no new API/schema/persistence/dependencies; remote writes remain gated.
- Pre-implementation review triage: accepted and fixed findings for exact `external_id` stability coverage, executable security-gate steps, and public docs stale-reference coverage.
Original file line number Diff line number Diff line change
@@ -0,0 +1,97 @@
# Issue 335 Hermes Memory Write Archive

Task id: `2026-06-19-issue-335-hermes-memory-write-archive`

## Scope

Handle GitHub issue #335, `on_memory_write hook: smarter sync - skip add/update, archive on remove`, on branch `issue/335-on-memory-write-archive-on-remove` from verified `origin/main` commit `67bb438b4158d74771ed285e06c9ac078985d603`.

## Sprint Contract

Goal: change the Hermes provider `on_memory_write` hook so local memory add/update events no longer create duplicate agentmemory entries, while removed local memory content is archived in agentmemory.

Scope:
- Modify the copyable Hermes integration under `integrations/hermes/`.
- Add focused Python-import regression coverage in the existing Vitest test suite.
- Update Hermes integration and root docs for the changed hook semantics and opt-out.
- Keep the fix inside the Hermes plugin boundary.

Non-goals:
- No new REST endpoints, MCP tools, public schemas, memory types, persistence tables, or migrations.
- No reintroduction of default add/update mirroring.
- No changes to non-Hermes integrations.
- No remote push, PR creation, or issue comments without separate current-turn approval.

Acceptance criteria:
- `on_memory_write("add", ...)` and `on_memory_write("update", ...)` do not call `/agentmemory/remember`.
- `on_memory_write("remove", target, content, ...)` archives non-blank removed content through existing `/agentmemory/remember`.
- Archived removed content is clearly marked as removed/archived, not a current active fact.
- Archive payload includes supported `remember` fields for project, session, files, tags, metadata, stable `external_id`, and existing agent identity propagation.
- Blank remove content is ignored.
- A plugin-local opt-out disables remove archiving.
- Existing saved-config URL/secret and agent identity tests are updated for the new add/update no-op semantics.

Intended verification:
- Red/green targeted Vitest: `corepack pnpm test test/integration-plaintext-http.test.ts test/hermes-plugin.test.ts`.
- Focused repo-native check for changed surface, then broader `corepack pnpm test` if feasible.
- Required security gates before final handoff or commit because this changes a host-integration hook: Semgrep and staged Gitleaks after staging if a commit is created.

Known boundaries:
- Host-integration behavior change was approved by the user after `$arena` selected the default-on archive-on-remove design.
- Full GitHub feature-loop invocation authorizes local PR-prep work only on task-owned surfaces. Fetch, push, PR creation, issue comments, merge, destructive cleanup, and remote state changes still require separate current-turn approval.
- Remote target is only `origin` (`https://github.com/wbugitlab1/agentmemory.git`); never target `https://github.com/rohitg00/agentmemory/`.

## Arena Synthesis

Decision: use Candidate B as the base, with Candidate C's archive wording/metadata caution grafted in.

Resulting design:
- Skip add/update by default.
- Archive remove through existing `/remember`, not `/observe`, because `/observe` synthetic compression truncates narrative and is weaker for preserving full removed content.
- Mark the archive content clearly as a removed Hermes local memory entry.
- Use a stable external ID derived from target and content to avoid repeat archive spam.
- Add a local opt-out with default enabled.

## Feature / Verification Matrix

| Change | Verification method | Status | Evidence |
| --- | --- | --- | --- |
| Issue legitimacy | Repo evidence + `$arena` | Done | Current `on_memory_write` mirrors add/update and ignores remove; arena selected `/remember` archive-on-remove. |
| Task state and plan | Local files | Done | `todo.md` and `plan.md` under this directory. |
| Add/update skipped | Targeted regression test | Done | Red run failed because add/update still called `remember`; green run passed in `corepack pnpm test test/integration-plaintext-http.test.ts test/hermes-plugin.test.ts` with 27 tests. |
| Remove archived | Targeted regression test | Done | `test/integration-plaintext-http.test.ts` asserts remove writes one `/remember` archive payload with project, session, files, tags, metadata, agentId, and archive wording. |
| Opt-out behavior | Targeted regression test | Done | `AGENTMEMORY_HERMES_ARCHIVE_REMOVES=0` test passes and blank remove content is ignored. |
| Stable archive identity | Targeted regression test | Done | Same target/content keeps the same `external_id`; different target/content differs. |
| Blank target handling | Targeted regression test | Done | Blank target uses `local memory` only for display/metadata and does not attach a pseudo-file. |
| Docs updated | Diff inspection and stale-reference search | Done | `README.md` and `integrations/hermes/README.md` updated; `rg -n "MEMORY\\.md mirroring|memory-write mirroring|mirrors MEMORY|mirrors MEMORY\\.md|mirrors .*MEMORY" README.md integrations/hermes/README.md` returned no matches. |
| Focused verification | Vitest | Done | `corepack pnpm test test/integration-plaintext-http.test.ts test/hermes-plugin.test.ts`: 2 files, 27 tests passed. |
| Full test suite | Vitest | Blocked | `corepack pnpm test` failed twice with only `test/codex-sdk-provider.test.ts` timeout failures; `corepack pnpm test test/codex-sdk-provider.test.ts` passed in isolation with 3 tests. |
| Security gate | Semgrep/Gitleaks as applicable | Partial | `semgrep scan --config p/default --error --metrics=off .`: 0 findings. Staged Gitleaks still required if staging/committing. |

## Subagent Ledger

| Workstream | Scope | Edits allowed | Expected output | Result | Residual risk |
| --- | --- | --- | --- | --- | --- |
| Arena candidate A | Read-only Hermes design | No | Recommendation | Complete | Preferred `/remember`, less explicit external ID. |
| Arena candidate B | Read-only Hermes design | No | Recommendation | Complete | Selected base. |
| Arena candidate C | Read-only Hermes design | No | Recommendation | Complete | `/observe` rejected for truncation risk; archive wording grafted. |
| Cross-judge | Read-only synthesis | No | Scores and base pick | Complete | Recommended Candidate B + C graft. |
| Pre-implementation review | Plan/task record | No | High/Medium plan findings | Complete | Found stable external ID, security-gate, and docs-scope gaps; all fixed in plan before code. |
| Final security review | Current diff | No | ACCEPT or findings | Complete | ACCEPT; no High/Medium security findings after Semgrep 0 findings. |
| Final test coverage review | Current diff | No | ACCEPT or findings | Complete | ACCEPT; no High/Medium coverage gaps. |
| Final maintainability review | Current diff | No | ACCEPT or findings | Complete | Found pseudo-file target, hidden saved-config key, and stale task evidence. Code/test fixes applied for first two; task evidence updated here. |

## Progress Notes

- 2026-06-19: Confirmed repo context, clean branch, remotes, and worktree state.
- 2026-06-19: Validated current code reproduces issue shape: add/update call `/remember`; remove is ignored.
- 2026-06-19: Ran `$arena`; selected default skip add/update plus archive-on-remove via `/remember`.
- 2026-06-19: User approved implementation via `$github-feature-loop`.
- 2026-06-19: Pre-implementation review found missing exact `external_id` stability coverage, missing executable security-gate steps, and narrow docs scope. Plan updated before code.
- 2026-06-19: Added failing Hermes regression tests. Red verification: `corepack pnpm test test/integration-plaintext-http.test.ts` failed because add/update still called `/remember`.
- 2026-06-19: Implemented Hermes-only behavior change: add/update no-op; remove archives non-blank content through `/remember` with archive wording, stable external ID, metadata/tags/session/project, and env opt-out.
- 2026-06-19: Updated Hermes and root docs; stale mirroring `rg` check returned no matches.
- 2026-06-19: Focused verification passed: `corepack pnpm test test/integration-plaintext-http.test.ts test/hermes-plugin.test.ts` (2 files, 27 tests).
- 2026-06-19: Full `corepack pnpm test` failed twice only in `test/codex-sdk-provider.test.ts` due 2000ms Codex CLI timeout; the same file passed in isolation (`3 passed`), so this is recorded as a full-suite blocker outside the Hermes surface.
- 2026-06-19: Semgrep passed with 0 findings.
- 2026-06-19: Final reviews: security ACCEPT, test coverage ACCEPT. Maintainability found pseudo-file target and hidden saved-config key; fixed by removing undocumented saved-config support and only setting `files` for original non-blank targets, with an added blank-target regression test.
Loading
Loading