Skip to content

feat(event-callback): OHE-3279 : add MemoryChangeCallbackProcessor to detect MEMORY.md updates - #451

Merged
tofarr merged 6 commits into
mainfrom
feat/memory-change-callback-processor
Sep 24, 2026
Merged

tofarr merged 6 commits into
mainfrom
feat/memory-change-callback-processor

Conversation

@tofarr

@tofarr tofarr commented Sep 21, 2026 •

Copy link
Copy Markdown
Contributor

HUMAN:

  • A human has tested these changes.

I have test this in a local context.

AGENT:

This PR was created by an AI agent (OpenHands) on behalf of the user.


Why

The enterprise app server needs a detection layer for persistent-memory (MEMORY.md) updates so that changes an agent makes to its memory file during a conversation can be recorded and later propagated to other conversations/sessions sharing the same memory tiers. Without it, memory edits are silent file writes with no observable signal on the app-server side.

Summary

  • New MemoryChangeCallbackProcessor (openhands/app_server/event_callback/memory_change_callback_processor.py) — an EventCallbackProcessor with event_kind='ObservationEvent' that filters for file_editor FileEditorObservation events whose path matches the project (.openhands/memory/MEMORY.md) or user (memory/MEMORY.md) memory relpath, skips read-only view commands and errored edits, and records the change as an EventCallbackResult(status=SUCCESS) with a JSON detail summary (memory_tier, path, old_content, new_content).
  • Registration in live_status_app_conversation_service.py — the callback is auto-added to the processors list when user.agent_settings.agent_context.load_memory is truthy (uses getattr for forward-compat), alongside the existing SetTitleCallbackProcessor registration. This is the primary app-server conversation creation path and has access to the user's agent_settings (the source of truth for load_memory). The webhook_router path was considered but rejected because it receives a fresh AgentContext that drops load_memory.
  • Stays ACTIVE — unlike SetTitleCallbackProcessor, this callback never self-disables, since memory can be updated multiple times within a single conversation.
  • Coverage limitation documented — only file_editor edits are detected; terminal-tool writes (echo >>, sed -i, etc.) are not. This is an accepted v1 tradeoff noted in the module docstring.

Issue Number

N/A — new detection layer (no tracking issue referenced).

How to Test

# Run the processor unit tests (Docker not required):
uv run python -m pytest tests/unit/app_server/test_memory_change_callback_processor.py -v --confcutdir=tests/unit/app_server

# Run the registration tests (Docker not required):
uv run python -m pytest tests/unit/app_server/test_live_status_app_conversation_service.py -v -k "memory_processor" --confcutdir=tests/unit/app_server

# Full suite (requires Docker for postgres testcontainers):
uv run pytest -n auto -s ./tests/unit

The processor tests construct real ObservationEvent + FileEditorObservation instances (no mocks) and verify:

  • Fires on project-tier and user-tier memory edits, capturing old/new content correctly
  • Handles create command with old_content=None
  • Stays ACTIVE after firing (does not self-disable)
  • Skips: non-ObservationEvent, non-file_editor tools, view commands, error observations, non-memory paths, and None paths

The registration tests verify:

  • MemoryChangeCallbackProcessor is saved when user.agent_settings.agent_context.load_memory=True
  • MemoryChangeCallbackProcessor is NOT saved when load_memory is off (default)

Pre-commit passes clean (ruff, ruff-format, mypy) on all changed files.

Video/Screenshots

Turn on Persistent Memory:
image

Start a conversation and ask the agent to remember some facts:
image
image

Recall the facts in a later conversation:
image

Type

  • Bug fix
  • Feature
  • Refactor
  • Breaking change
  • Docs / chore

Notes

  • The load_memory flag lives on AgentContext in the SDK. The registration reads it from user.agent_settings.agent_context (the user's persisted settings) rather than from the AgentContext on the built StartConversationRequest.agent, because _build_start_conversation_request_for_user creates a fresh AgentContext that drops load_memory. Uses getattr for forward-compat with SDK versions that predate the field.
  • Path-based tier inference: a path ending with the project relpath is classified as project tier; otherwise user tier. When the user persistence dir defaults to ~/.openhands, a user-tier path collides with the project relpath suffix and is classified as project tier — this is the accepted ambiguity of a path-only heuristic and is noted in code comments.
  • This is the detection/recording layer only; a future change will consume the recorded EventCallbackResult records to propagate memory updates across conversations.

Enterprise server image for this PR:

ghcr.io/openhands/enterprise-server:sha-e6e1bac

@github-actions github-actions Bot added the type: feat A new feature label Sep 21, 2026
@tofarr tofarr changed the title feat(event-callback): add MemoryChangeCallbackProcessor to detect MEMORY.md updates feat(event-callback): OHE-3279 : add MemoryChangeCallbackProcessor to detect MEMORY.md updates Sep 21, 2026
@tofarr tofarr changed the title feat(event-callback): OHE-3279 : add MemoryChangeCallbackProcessor to detect MEMORY.md updates feat(event-callback): add MemoryChangeCallbackProcessor to detect MEMORY.md updates Sep 21, 2026
@github-actions

github-actions Bot commented Sep 21, 2026 •

Copy link
Copy Markdown

Coverage report

Click to see where and how coverage changed

FileStatementsMissingCoverageCoverage
(new stmts)
Lines missing
  migrations/versions
  167_add_user_memory_context.py 24-33, 40-41
  openhands/app_server/app_conversation
  app_conversation_service_base.py 305-315, 850, 869-896
  live_status_app_conversation_service.py 2272
  openhands/app_server/event_callback
  memory_change_callback_processor.py 77, 147-198
  openhands/app_server/settings
  settings_models.py
  storage
  saas_settings_store.py
  user.py
Project Total  

This report was generated by python-coverage-comment-action

@github-actions

Copy link
Copy Markdown

⚠️ This PR contains migrations. Please synchronize before merging to prevent conflicts.

… updates

Add an EventCallbackProcessor that detects when an agent edits its
persistent memory file (MEMORY.md) via the file_editor tool during a
conversation, and records the before/after content so changes can be
propagated to other conversations sharing the same memory tiers.

- New MemoryChangeCallbackProcessor (event_kind=ObservationEvent) that
  filters for file_editor FileEditorObservation events whose path matches
  the project or user memory relpath, skips view/error edits, and records
  the change as an EventCallbackResult with a JSON detail summary
  (memory_tier, path, old_content, new_content).
- Register the callback for new conversations when
  agent.agent_context.load_memory is enabled (webhook_router.py).
- The callback stays ACTIVE for the conversation lifetime (memory can
  change multiple times); does not self-disable.
- Documents the terminal-tool coverage limitation.
- 11 unit tests using real ObservationEvent/FileEditorObservation instances.

Co-authored-by: openhands <openhands@all-hands.dev>
…tion to primary conversation-start path

Move the MemoryChangeCallbackProcessor registration from webhook_router.py
(the secondary path for agent-server-created conversations) to
live_status_app_conversation_service.py (the primary app-server conversation
creation path), alongside the existing SetTitleCallbackProcessor registration.

Why: the primary path has access to the user's agent_settings.agent_context
(the source of truth for load_memory). The webhook path receives a fresh
AgentContext that drops load_memory, so the callback would never fire there.

- live_status_app_conversation_service.py: auto-add MemoryChangeCallbackProcessor
  to the processors list when user.agent_settings.agent_context.load_memory is True
- webhook_router.py: revert the previous registration (back to SetTitle-only)
- Add two tests: registers when load_memory=True, skips when False

Co-authored-by: openhands <openhands@all-hands.dev>
…rd with callback persistence and sandbox injection

Add  and  columns to the User model
(Alembic migration 165) and corresponding fields to the Settings model. The
SaasSettingsStore.store() guards  from being clobbered by
normal settings saves — it is written exclusively by the callback.

The MemoryChangeCallbackProcessor now persists the new MEMORY.md content to
the user record via a column-specific UPDATE (resolved through the
conversation's created_by_user_id). The processor never self-disables.

Callback registration now gates on  (a top-level user
setting) rather than the SDK's  field, which is dropped by the
fresh AgentContext built at conversation start.

When  is true, the app conversation service:
- stamps  on the AgentContext so the SDK reads MEMORY.md
- writes the stored  to  in the
  sandbox during run_setup_scripts (via )

Co-authored-by: openhands <openhands@all-hands.dev>
Add an Agent Context section to the enterprise agent settings page with a SettingsSwitch for the enable_memory_context top-level user setting. The toggle uses the uncontrolled (defaultIsToggled) mode and an override pattern so the dirty state is tracked independently of the component's internal state.

Co-authored-by: openhands <openhands@all-hands.dev>
…next_reset)

Co-authored-by: openhands <openhands@all-hands.dev>
@tofarr
tofarr force-pushed the feat/memory-change-callback-processor branch from e65bdc0 to 1b4c63b Compare September 24, 2026 16:31
@tofarr
tofarr marked this pull request as ready for review September 24, 2026 17:48

@hieptl hieptl left a comment

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

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

Thank you! 🙏

@tofarr tofarr changed the title feat(event-callback): add MemoryChangeCallbackProcessor to detect MEMORY.md updates feat(event-callback): OHE-3279 : add MemoryChangeCallbackProcessor to detect MEMORY.md updates Sep 24, 2026
@tofarr
tofarr enabled auto-merge (squash) September 24, 2026 17:55
@tofarr
tofarr merged commit 0853ff4 into main Sep 24, 2026
25 checks passed
@tofarr
tofarr deleted the feat/memory-change-callback-processor branch September 24, 2026 17:59
@openhands-release-bot openhands-release-bot Bot added the released: 1.65.0 Shipped in 1.65.0 label Sep 25, 2026
@openhands-release-bot

Copy link
Copy Markdown
Contributor

🚀 Released in 1.65.0.

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

released: 1.65.0 Shipped in 1.65.0 type: feat A new feature

Projects

None yet

Development

Successfully merging this pull request may close these issues.

3 participants