Skip to content

fix(memory): restore beam initialization lifecycle#470

Closed
dplush wants to merge 2 commits into
mnemosyne-oss:mainfrom
dplush:fix/restore-beam-init-after-close
Closed

fix(memory): restore beam initialization lifecycle#470
dplush wants to merge 2 commits into
mnemosyne-oss:mainfrom
dplush:fix/restore-beam-init-after-close

Conversation

@dplush

@dplush dplush commented Jul 15, 2026

Copy link
Copy Markdown
Contributor

Summary

Restores the pre-#382 Phase 8 lazy-state and BeamMemory initialization to Mnemosyne.__init__. The WAL cleanup methods remain lifecycle-only.

The #382 insertion accidentally left this initialization under __del__, so normal instances had no beam attribute and the main CI failed.

Verification

  • Reproduced the current-main failure: TestMnemosyneIntegration::test_legacy_and_beam_dual_write raised AttributeError: Mnemosyne has no attribute beam.
  • Added constructor + idempotent-close regression coverage.
  • Focused affected matrix: 44 passed.
  • py_compile and git diff --check pass.

Note: the unrelated current-main polyphonic recall failure (PolyphonicResult has no content) remains outside this narrow regression fix.

Summary

This PR corrects Mnemosyne’s core memory lifecycle by restoring Phase 8 lazy-subsystem placeholder setup and BeamMemory construction back into Mnemosyne.__init__ (where it belongs), undoing an accidental relocation under __del__ introduced previously. That ensures BeamMemory is created during construction with the intended event wiring (event_emitter=self._stream_emit), while close() remains focused on WAL-cleanup and idempotent shutdown. It also adds constructor-and-close regression coverage, including safe behavior across multiple wrappers and exception paths.

  • Core architecture (tiered memory / BEAM): Re-establishes the intended BEAM tier initialization order by performing Phase 8 streaming/patterns/plugins/delta-sync wiring scaffolding during construction and then instantiating BeamMemory with the correct emitter. Removes duplicated/late Phase 8 initialization logic, preventing partially-initialized BEAM state from reaching runtime.
  • Connection lifecycle / long-term correctness: Introduces instance-scoped ownership/refcounting around thread-local SQLite and BEAM resources so multiple Mnemosyne wrappers in the same thread (and/or across different banks/db paths) don’t double-close shared connections. _close_connection/_close_beam_connection now support targeted db_path-aware cleanup to avoid invalidating a newer live instance that switched banks.
  • Retrieval / consolidation / veracity: No changes to retrieval strategies, consolidation pipeline, or the veracity system behavior are indicated; changes are architectural/lifecycle (initialization timing, correct event plumbing, and safe shutdown).
  • Sync layer: Phase 8’s delta-sync component is ensured to be included in the constructor-time wiring placeholders; no new sync semantics are described beyond ensuring the sync-related subsystem is initialized in the correct lifecycle phase.
  • Privacy posture & local-first guarantees: No evidence of added remote behavior or altered data movement. The PR is strictly about local lifecycle correctness (when BEAM is constructed and how local SQLite connections are cleaned up) while preserving the local-first storage model.
  • Agent integration surfaces (Hermes, MCP, CLI): No direct interface changes. Plugin-manager initialization is restored as part of the constructor-time Phase 8 setup, and BEAM’s config seeding behavior remains the same; this helps ensure agent-related plugin/event surfaces are available when the instance is actually constructed.
  • Maintainability: Improves lifecycle clarity by separating construction from teardown, adding exception-safety (if BEAM construction fails after provisional ownership acquisition, close() is invoked to release owners), and reinforcing close() idempotency. Regression tests explicitly validate constructor wiring and safe repeated/second-instance shutdown behavior.
  • Verification & remaining gaps: Tests add constructor wiring and shared-connection-close behavior; an unrelated polyphonic recall failure remains unresolved.

@coderabbitai

coderabbitai Bot commented Jul 15, 2026

Copy link
Copy Markdown

Review Change Stack

📝 Walkthrough

Walkthrough

Mnemosyne now tracks shared per-thread database ownership, initializes BeamMemory during construction, and conditionally closes shared resources only for the final owner. Tests cover emitter wiring, idempotent cleanup, shared connections, and constructor failure recovery.

Changes

Shared connection lifecycle

Layer / File(s) Summary
Connection ownership and cleanup
mnemosyne/core/memory.py, mnemosyne/core/beam.py
Per-thread, per-database reference counts and database-aware close helpers prevent active instances from closing shared SQLite or BEAM connections.
Constructor Beam wiring
mnemosyne/core/memory.py, tests/test_beam.py
Construction initializes lifecycle state, schema, lazy subsystems, and BeamMemory with the stream emitter; failure cleanup, repeated close calls, shared instances, and event delivery are tested.

Estimated code review effort: 3 (Moderate) | ~25 minutes

Suggested reviewers: axdsan, kohai-ut, milgauss

🚥 Pre-merge checks | ✅ 5
✅ Passed checks (5 passed)
Check name Status Explanation
Description Check ✅ Passed Check skipped - CodeRabbit’s high-level summary is enabled.
Title check ✅ Passed The title clearly reflects the main change: restoring Beam initialization lifecycle in memory handling.
Docstring Coverage ✅ Passed No functions found in the changed files to evaluate docstring coverage. Skipping docstring coverage check.
Linked Issues check ✅ Passed Check skipped because no linked issues were found for this pull request.
Out of Scope Changes check ✅ Passed Check skipped because no linked issues were found for this pull request.
✨ Finishing Touches
🧪 Generate unit tests (beta)
  • Create PR with unit tests

Comment @coderabbitai help to get the list of available commands.

@coderabbitai coderabbitai Bot left a comment

Copy link
Copy Markdown

Choose a reason for hiding this comment

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

Actionable comments posted: 1

🤖 Prompt for all review comments with AI agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.

Inline comments:
In `@tests/test_beam.py`:
- Around line 955-965: Extend test_constructor_wires_beam_before_close to assert
that the constructed BeamMemory retains Mnemosyne._stream_emit as its
event-emitter callback, using the appropriate BeamMemory callback attribute.
Keep the existing construction, idempotent close, and successor lifecycle
assertions unchanged.
🪄 Autofix (Beta)

Fix all unresolved CodeRabbit comments on this PR:

  • Push a commit to this branch (recommended)
  • Create a new PR with the fixes

ℹ️ Review info
⚙️ Run configuration

Configuration used: Path: .coderabbit.yaml

Review profile: ASSERTIVE

Plan: Pro Plus

Run ID: fe1675fa-1e0e-4287-a4e2-b1a47c98adac

📥 Commits

Reviewing files that changed from the base of the PR and between 0a64d2e and c991481.

📒 Files selected for processing (2)
  • mnemosyne/core/memory.py
  • tests/test_beam.py

Comment thread tests/test_beam.py Outdated
@dplush
dplush force-pushed the fix/restore-beam-init-after-close branch from c991481 to 3037260 Compare July 15, 2026 21:16

@coderabbitai coderabbitai Bot left a comment

Copy link
Copy Markdown

Choose a reason for hiding this comment

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

Actionable comments posted: 3

🤖 Prompt for all review comments with AI agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.

Inline comments:
In `@mnemosyne/core/memory.py`:
- Around line 209-214: Initialize Mnemosyne._closed before constructing
BeamMemory, then wrap the BeamMemory construction in an exception path that
invokes close() or the established cleanup routine before re-raising the
original exception. Ensure failed construction cannot reach __del__ with missing
lifecycle state or leave opened connections, while preserving normal
initialization behavior.
- Around line 209-214: Update Mnemosyne’s BeamMemory lifecycle around the
constructor wiring and close() so shared thread-local BeamMemory handles are not
closed while another live instance uses them. Implement per-instance
ownership/refcounting or enforce a single live owner, ensuring each Mnemosyne
retains a valid self.beam.conn until its own cleanup is complete.

In `@tests/test_beam.py`:
- Around line 955-967: Strengthen test_constructor_wires_beam_before_close by
spying on mem.stream.emit, enabling streaming with mem.enable_streaming(),
invoking mem.beam._event_emitter with a sentinel event, and asserting the stream
receives that event. If lifecycle ownership requires it, also cover closing two
live Mnemosyne instances independently while preserving the existing
idempotent-close assertions.
🪄 Autofix (Beta)

Fix all unresolved CodeRabbit comments on this PR:

  • Push a commit to this branch (recommended)
  • Create a new PR with the fixes

ℹ️ Review info
⚙️ Run configuration

Configuration used: Path: .coderabbit.yaml

Review profile: ASSERTIVE

Plan: Pro Plus

Run ID: f22eb3cb-82bd-445a-adce-54627f803348

📥 Commits

Reviewing files that changed from the base of the PR and between c991481 and 3037260.

📒 Files selected for processing (2)
  • mnemosyne/core/memory.py
  • tests/test_beam.py

Comment thread mnemosyne/core/memory.py Outdated
Comment thread tests/test_beam.py Outdated

@coderabbitai coderabbitai Bot left a comment

Copy link
Copy Markdown

Choose a reason for hiding this comment

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

Actionable comments posted: 2

🤖 Prompt for all review comments with AI agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.

Inline comments:
In `@mnemosyne/core/memory.py`:
- Around line 30-31: Update mnemosyne/core/memory.py lines 30-31 to store
thread-local connections per database path, then adjust the acquisition and
release logic in the relevant memory connection functions (lines 82-136 and
240-286) to retain, resolve, and close the handle for the released owner key.
Update mnemosyne/core/beam.py lines 490-507 to replace the single cached BEAM
connection with path-addressable storage, preserving correct cache invalidation
and lifecycle cleanup for both BEAM and core memory.

In `@tests/test_beam.py`:
- Around line 976-999: Expand the lifecycle tests around
test_close_keeps_shared_beam_connection_for_live_instance and
test_constructor_failure_releases_provisional_connection to assert both
second.conn and second.beam.conn remain usable after first.close(). After
recovery and final close, verify the failed owner’s connection/registry key is
removed. Add coverage with two live Mnemosyne instances using different database
paths, asserting each SQLite and BEAM handle closes when its final owner is
released.
🪄 Autofix (Beta)

Fix all unresolved CodeRabbit comments on this PR:

  • Push a commit to this branch (recommended)
  • Create a new PR with the fixes

ℹ️ Review info
⚙️ Run configuration

Configuration used: Path: .coderabbit.yaml

Review profile: ASSERTIVE

Plan: Pro Plus

Run ID: 54409b7f-a014-411b-b74a-3a79682c4b14

📥 Commits

Reviewing files that changed from the base of the PR and between 3037260 and 5d6da2c.

📒 Files selected for processing (3)
  • mnemosyne/core/beam.py
  • mnemosyne/core/memory.py
  • tests/test_beam.py

Comment thread mnemosyne/core/memory.py
Comment on lines +30 to +31
_instance_connection_lock = threading.Lock()
_instance_connection_refcounts: Dict[tuple, int] = {}

Copy link
Copy Markdown

Choose a reason for hiding this comment

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

🩺 Stability & Availability | 🟠 Major | 🏗️ Heavy lift

Align per-database ownership with per-database connection storage.

The refcounts distinguish databases, but each module retains only one thread-local connection. Switching databases displaces the prior handles, and final-owner cleanup cannot find them.

  • mnemosyne/core/memory.py#L30-L31: store connection ownership alongside actual per-path handles.
  • mnemosyne/core/memory.py#L82-L136: resolve and close the handle associated with the released owner key.
  • mnemosyne/core/memory.py#L240-L286: retain per-instance/per-path handles through acquisition and final release.
  • mnemosyne/core/beam.py#L490-L507: replace the single cache slot with path-addressable BEAM connections.

As per path instructions, verify cache invalidation correctness and lifecycle cleanup for BEAM and core memory.

📍 Affects 2 files
  • mnemosyne/core/memory.py#L30-L31 (this comment)
  • mnemosyne/core/memory.py#L82-L136
  • mnemosyne/core/memory.py#L240-L286
  • mnemosyne/core/beam.py#L490-L507
🤖 Prompt for AI Agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.

In `@mnemosyne/core/memory.py` around lines 30 - 31, Update
mnemosyne/core/memory.py lines 30-31 to store thread-local connections per
database path, then adjust the acquisition and release logic in the relevant
memory connection functions (lines 82-136 and 240-286) to retain, resolve, and
close the handle for the released owner key. Update mnemosyne/core/beam.py lines
490-507 to replace the single cached BEAM connection with path-addressable
storage, preserving correct cache invalidation and lifecycle cleanup for both
BEAM and core memory.

Source: Path instructions

Comment thread tests/test_beam.py
Comment on lines +976 to +999
def test_close_keeps_shared_beam_connection_for_live_instance(self, temp_db):
first = Mnemosyne(session_id="first", db_path=temp_db)
second = Mnemosyne(session_id="second", db_path=temp_db)

first.close()
assert second.beam.conn.execute("SELECT 1").fetchone()[0] == 1
second.close()

def test_constructor_failure_releases_provisional_connection(self, temp_db, monkeypatch):
import mnemosyne.core.memory as memory_module

real_beam = memory_module.BeamMemory

def fail_beam(*args, **kwargs):
raise RuntimeError("beam construction failed")

monkeypatch.setattr(memory_module, "BeamMemory", fail_beam)
with pytest.raises(RuntimeError, match="beam construction failed"):
Mnemosyne(session_id="broken", db_path=temp_db)

monkeypatch.setattr(memory_module, "BeamMemory", real_beam)
recovered = Mnemosyne(session_id="recovered", db_path=temp_db)
assert isinstance(recovered.beam, BeamMemory)
recovered.close()

Copy link
Copy Markdown

Choose a reason for hiding this comment

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

🎯 Functional Correctness | 🟠 Major | ⚡ Quick win

Make the lifecycle tests prove both resources are released.

The shared-instance test checks only beam.conn, while the constructor-failure test still passes if the failed owner leaks a refcount. Extend this group to:

  • verify second.conn remains usable;
  • assert the failed owner key is removed after recovery and close;
  • exercise two live instances using different database paths and confirm each displaced SQLite/BEAM handle is closed at its final-owner release.

As per path instructions, tests require comprehensive coverage and meaningful assertions rather than merely successful reconstruction.

🧰 Tools
🪛 Ruff (0.15.21)

[warning] 989-989: Missing return type annotation for private function fail_beam

Add return type annotation: NoReturn

(ANN202)


[warning] 989-989: Missing type annotation for *args

(ANN002)


[warning] 989-989: Unused function argument: args

(ARG001)


[warning] 989-989: Missing type annotation for **kwargs

(ANN003)


[warning] 989-989: Unused function argument: kwargs

(ARG001)


[warning] 990-990: Avoid specifying long messages outside the exception class

(TRY003)

🤖 Prompt for AI Agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.

In `@tests/test_beam.py` around lines 976 - 999, Expand the lifecycle tests around
test_close_keeps_shared_beam_connection_for_live_instance and
test_constructor_failure_releases_provisional_connection to assert both
second.conn and second.beam.conn remain usable after first.close(). After
recovery and final close, verify the failed owner’s connection/registry key is
removed. Add coverage with two live Mnemosyne instances using different database
paths, asserting each SQLite and BEAM handle closes when its final owner is
released.

Source: Path instructions

@dplush

dplush commented Jul 16, 2026

Copy link
Copy Markdown
Contributor Author

Superseded by #477: reverting the unsafe #382 lifecycle change is safer than continuing the incomplete connection-ownership refactor. The lifecycle redesign will return separately with an explicit lease contract.

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

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

1 participant