fix(mem0-agent): detached hooks lost their payload, so nothing was ever written

Observed live: real sessions flagged durable candidates (a preference, two
insights, a convention) but every session showed 'flush events: 0'. The manifest
backgrounded flush and assist-error with '( cmd &)', so the parent exited and
closed stdin before the child could read it -- the child then had no session_id,
fell back to 'no-session', and drained an empty buffer.

Now the CLI reads the payload itself and re-execs a detached worker with
start_new_session, so the hook still returns in ~0.27s. Verified end to end:
sent=1, session_state=created.

Also records a surface hint per event and shows it in 'sessions', since the
manifest pins MEM0_EDITOR to the same value on every client.

Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
This commit is contained in:
Deshraj Yadav
2026-07-29 20:22:43 -07:00
co-authored by Claude Fable 5
parent 10b8fc4bed
commit 1ea8c2e5fe
6 changed files with 97 additions and 18 deletions
@@ -13,6 +13,8 @@ from mem0_agent.settings import SessionState, Settings
class Args:
def __init__(self, **kw):
self.session_id = "sess-cli"
self.worker = True # tests exercise the worker path directly
self.payload_file = None
for k, v in kw.items():
setattr(self, k, v)
+10 -3
View File
@@ -106,11 +106,18 @@ def test_user_prompt_submit_is_declared_local_only(spec, manifest):
assert entry["background"] is False and entry["blocking"] is True
def test_flush_and_assist_hooks_are_detached(spec, manifest):
def test_write_hooks_are_not_shell_backgrounded(spec, manifest):
"""These hooks must NOT be wrapped in `( ... &)`.
A shell-backgrounded hook loses stdin the instant the parent exits, so the child
sees no session_id and no transcript_path and drains the wrong buffer -- observed
live as flagged candidates that were never written. The CLI now reads the payload
first and re-execs itself detached, so the hook still returns in milliseconds.
"""
for event in ("PostToolUse", "Stop", "PreCompact", "SessionEnd"):
command = manifest["hooks"][event][0]["hooks"][0]["command"]
assert command.startswith("(") and command.endswith("&)"), event
assert ">/dev/null" in command, event
assert not command.strip().endswith("&)"), f"{event} must not be backgrounded by the shell"
assert "mem0-agent" in command, event
def test_editor_env_is_pinned(manifest):