Skip to content

feat: coordinator event wiring + DTU end-to-end profile#4

Merged
michaeljabbour merged 16 commits intomichaeljabbour:mainfrom
colombod:feat/coordinator-event-wiring
Apr 30, 2026
Merged

feat: coordinator event wiring + DTU end-to-end profile#4
michaeljabbour merged 16 commits intomichaeljabbour:mainfrom
colombod:feat/coordinator-event-wiring

Conversation

@colombod
Copy link
Copy Markdown
Contributor

Summary

Wires all five mempalace hooks and the palace tool to the Amplifier coordinator event bus so that memory-mempalace:* events appear in events.jsonl — making palace activity observable by hooks-logging, context-intelligence, and any future hook without special-casing.

Adds a complete DTU end-to-end profile that provisions a real Amplifier session with a live MemPalace instance for integration testing, and integrates your macOS guide fixes from docs(dtu): correct CLI references and flow in dtu.md (#3).


Changes

Coordinator Event Wiring (core feature)

All five hooks and the tool now emit memory-mempalace:* events through coordinator.hooks.emit() with a dual-write strategy — the private ~/.mempalace/events/{sid}.jsonl continues to work for palace events, while coordinator events flow through hooks-logging → standard events.jsonl:

Module Events emitted
hooks-mempalace-capture memory-mempalace:drawer_filed, memory-mempalace:capture_failed
hooks-mempalace-briefing memory-mempalace:briefing_assembled, memory-mempalace:briefing_skipped
hooks-mempalace-interject memory-mempalace:memory_surfaced, memory-mempalace:interject_skipped
hooks-project-context memory-mempalace:coordination_read, memory-mempalace:coordination_scaffolded, memory-mempalace:curator_handoff_requested
tool-mempalace memory-mempalace:garden_completed

Each module calls coordinator.register_contributor("observability.events", name, callback) at mount time so hooks-logging and context-intelligence can discover the contributed events via collect_contributions("observability.events").

The interject hook also registers a cross-hook listener for memory-mempalace:briefing_assembled to populate _briefed_ids, replacing the previous coordinator.get_capability() polling pattern.

Tests

  • tests/test_coordinator_bridge.py — unit tests for all coordinator bridge implementations
  • tests/integration/test_event_wiring.py — DTU-backed integration tests verifying events appear in events.jsonl
  • tests/test_contract.py — extended with register_contributor contract assertions
  • tests/test_hook_emissions.py — updated for new event signatures

DTU End-to-End Profile

.amplifier/digital-twin-universe/profiles/memory-bundle-e2e.yaml — provisions an Ubuntu 24.04 Incus container with:

  • MemPalace installed + seeded palace fixtures
  • Amplifier CLI with the memory bundle active (--app install)
  • pytest-asyncio for async integration tests

Two non-trivial fixes required during investigation:

  1. amplifier-core 1.4.1 pin (step 12.5): uv tool install git+https://github.com/microsoft/amplifier may resolve an older PyPI release. register_contributor / collect_contributions and the on_session_ready lifecycle require >= 1.4.1.

  2. Amplifier module cache shadowing (step 15, root cause investigated via systematic 4-layer debug): The Amplifier CLI caches modules in ~/.amplifier/cache/. The loader adds these cache dirs to sys.path before site-packages. amplifier_module_hooks_logging gets imported from the old cache (no on_session_ready) and placed in sys.modules before _load_entry_point can run. B2 detection in loader.py then finds has_osr=False, never sets __on_session_ready__, and Phase 6 queue stays at 0. Fix: install the hooks-logging fork via pip AND overwrite all Amplifier cache copies with the fork's __init__.py.

The hooks-logging fork used is colombod/amplifier-module-hooks-logging@feat/on-session-ready which adds collect_contributions("observability.events") in on_session_ready (upstream PR pending).

Agent Frontmatter + bundle.dot

  • Agents archivist.md, curator.md, docent.md: hoisted name and description to top-level frontmatter (Amplifier convention for agent bundles)
  • bundle.dot: added repository architecture diagram

dtu.md macOS Guide Integration

docs/development/dtu.md: forward-integrated your commit 2ba94f7 (macOS guide fixes — Colima prereq, amplifier-digital-twin install from git, amplifier-gitea create + mirror-from-github, correct launch section). No conflict with our changes (you only touched dtu.md; we only touched the profile YAML).


Verification

Verified end-to-end in DTU (Ubuntu 24.04 / Incus container):

  • 6–13 memory-mempalace:* coordinator events per session appearing in events.jsonl
  • PHASE6:queue_size=1on_session_ready properly enqueued and dispatched
  • All 45 unit tests pass (pytest tests/ -x)
  • Private ~/.mempalace/events/ JSONL continues to work (dual-write intact)

Diego Colombo and others added 16 commits April 29, 2026 22:52
- Add asyncio import for event loop capture
- Add _SYNC_BRIDGE_EMIT module-level holder (mirrors _QUEUE/_DRAIN_THREAD pattern)
- Add sync_bridge_emit keyword-only param to MempalaceCaptureHook.__init__
  with no-op default (lambda e, p: None) for test safety
- In _process_job() success branch: call _SYNC_BRIDGE_EMIT after emit_event
  with memory-mempalace:drawer_filed and same payload fields
- In _process_job() except branch: call _SYNC_BRIDGE_EMIT after emit_event
  with memory-mempalace:capture_failed and reason=mcp_error
- In _drain_loop() last-resort guard: call _SYNC_BRIDGE_EMIT inside
  if job.emit_events guard with reason=worker_exception
- Replace mount() to: capture running loop, register_contributor on
  observability.events channel, define sync_bridge_emit closure using
  run_coroutine_threadsafe, set _SYNC_BRIDGE_EMIT global, instantiate
  MempalaceCaptureHook with bridge closure
- All coordinator calls wrapped in try/except; private emit_event calls preserved
… drawer_ids

- Add TestBriefingCoordinatorBridge to test_coordinator_bridge.py with 3 tests:
  * test_register_contributor_called_at_mount: verifies mount() calls register_contributor
    with 'observability.events' channel and event list including briefing_assembled/skipped
  * test_briefing_assembled_emits_with_drawer_ids: verifies bridge emit carries drawer_ids
    derived from results_after_rerank dicts
  * test_emit_events_false_suppresses_both_channels: emit_events=False suppresses both
    private JSONL and coordinator bridge channels

- Modify _build_briefing return type: tuple[str, list[str], int, list[dict], list[dict]]
  * results_fetched: list = [] (was int = 0)
  * results_after_rerank: list = [] (was int = 0)
  * results_fetched = list(raw_results) (was len(raw_results))
  * results_after_rerank = list(results) (was len(results))

- Add bridge_emit keyword-only param to MempalaceBriefingHook.__init__
  * Stores as self._bridge_emit (defaults to async no-op)

- In __call__, derive drawer_ids from results_after_rerank and emit to bridge:
  * After briefing_assembled: bridge emits ok=True with drawer_ids and metadata
  * After briefing_skipped (no_content): bridge emits ok=False with reason
  * In except block (unavailable): bridge emits ok=False with reason

- Update mount() to register_contributor and wire bridge_emit:
  * Calls coordinator.register_contributor('observability.events', ...)
  * Creates async bridge_emit closure calling coordinator.hooks.emit
  * Instantiates hook with bridge_emit=bridge_emit

- Fix test_hook_emissions.py existing test:
  * Update mock from (100, 3, 3) to (100, [], []) for list return type
  * Update assertion to use len() comparison for list type
- Add bridge_emit keyword-only param to MempalaceInterjectHook.__init__
  with _noop async fallback when not provided
- Add _briefed_ids: set[str] initialized to empty set in __init__
- Thread bridge_emit into all 3 handlers (on_prompt_submit, on_tool_pre,
  on_orchestrator_complete): 11 interject_skipped + 3 memory_surfaced sites,
  each wrapped in try/except
- Replace mount() to:
  * register_contributor('observability.events', 'memory-mempalace-interject', ...)
    with callback returning ['memory-mempalace:memory_surfaced', 'memory-mempalace:interject_skipped']
  * define async bridge_emit closure calling coordinator.hooks.emit
  * define async _on_briefing_assembled listener that updates hook._briefed_ids
    from data['drawer_ids'] and register it for 'memory-mempalace:briefing_assembled'
  * register prompt:submit, tool:pre, orchestrator:complete handlers at priority=20
  * return version 1.1.0 with full config dict including emit_events
- Add TestInterjectCoordinatorBridge with 4 tests:
  * test_register_contributor_called_at_mount
  * test_briefing_assembled_listener_registered_in_mount
  * test_briefed_ids_populated_from_briefing_event
  * test_memory_surfaced_emits_to_coordinator
- Add bridge_emit keyword param to ProjectContextStartHook.__init__ and
  ProjectContextEndHook.__init__ with async _noop fallback
- Emit memory-mempalace:coordination_scaffolded bridge event after scaffolding
- Emit memory-mempalace:coordination_read bridge event after reading tier-1 files
- Emit memory-mempalace:curator_handoff_requested bridge event at session end
- Replace mount() to call register_contributor with all 3 bridge events and
  wire a single bridge_emit closure shared by both hooks
- Add TestProjectContextCoordinatorBridge with 2 tests for register_contributor
  and curator_handoff_requested bridge call
- Fix _FakeCoordinator in test_contract.py to support register_contributor
  (also fixes pre-existing test_briefing_mounts_and_dispatches failure)
…d_emit

- Add _SYNC_BRIDGE_EMIT module-level holder for coordinator bridge callback
- Register 'memory-mempalace-tool' contributor in mount() with events:
  garden_completed, garden_progress
- Define sync_bridge_emit closure using asyncio.run_coroutine_threadsafe
  for fire-and-forget forwarding from garden sync thread to event loop
- Add combined_emit closure in execute() garden block to dual-emit to
  private JSONL and coordinator bridge
- Bridge garden_completed on TimeoutError (ok=False) and success path (ok=True)
- Add FakeCoordinator.mount() async no-op for tool bridge tests
- Add TestToolMempalaceCoordinatorBridge.test_register_contributor_called_at_mount
…syncio

- Add pytest_collection_modifyitems to tests/integration/conftest.py:
  skips all integration tests on host when /root/.mempalace sentinel
  path is absent (or PermissionError on non-root host). Tests only run
  inside memory-bundle-e2e DTU container.

- Add tests/integration/test_event_wiring.py: validates the full
  coordinator event wiring chain (hook fires -> coordinator.hooks.emit
  -> hooks-logging writes events.jsonl) with three tests:
    * test_drawer_filed_appears_in_events_jsonl
    * test_briefing_assembled_payload_has_drawer_ids
    * test_briefed_ids_prevents_reinjection (skipped — requires
      content-pinned session with deterministic retrieval)

- Update .amplifier/digital-twin-universe/profiles/memory-bundle-e2e.yaml:
  add pytest-asyncio to pip install step (step 4) so async test
  infrastructure is available inside the DTU container.
- fix(briefing): restore integer counts in private JSONL emit_event for
  results_fetched and results_after_rerank (was incorrectly changed to
  full list payloads, violating the out-of-scope private schema contract)

- fix(capture): add loop.is_closed() guard to sync_bridge_emit closure to
  prevent RuntimeWarning from unawaited coroutine in closed event loop
  (drain thread outlives asyncio.run() in test teardown)

- fix(integration): add Path | None return type + assert-not-None guards
  to _latest_events_jsonl() so tests fail with a clear message instead of
  IndexError when no events.jsonl exists

- fix(test): update test_briefing_emits_on_assemble assertion to match
  restored integer schema (isinstance int checks replace len() calls)

- fix(interject): apply ruff format (1 blank line + 3 dict expansions)

Fixes post-code-review findings michaeljabbour#1, michaeljabbour#2, michaeljabbour#3, #6 from quality review.
All 45 tests pass, 0 warnings.

Co-authored-by: Amplifier <amplifier@example.com>
…dle.dot

- agents/{docent,archivist,curator}.md: add top-level name: and description: fields
  alongside existing agent.name/agent.description (Option A flat schema)
  Clears 6 false-positive warnings from the bundle validator (v3.4.0 classifier
  requires top-level fields; nested agent.name was not detected)
- bundle.dot: generated by generate-bundle-docs recipe — structural overview of
  the bundle architecture (7-cluster DOT covering behaviors, agents, modules,
  context, and external references)
…or events

Two provisioning additions to memory-bundle-e2e.yaml:

1. Step 12.5 — amplifier-core 1.4.1 + latest amplifier-app-cli
   register_contributor() / collect_contributions() and the on_session_ready
   lifecycle only work correctly from amplifier-core >= 1.4.1. The uv tool
   install from git may resolve an older PyPI release; --reinstall-package
   forces the correct versions.

2. Step 15 — hooks-logging fork (feat/on-session-ready)
   The upstream hooks-logging does not yet call collect_contributions in
   on_session_ready, so memory-mempalace: coordinator events are invisible
   in events.jsonl without this fork. Workaround until the upstream PR merges.
   Fork: colombod/amplifier-module-hooks-logging (feat/on-session-ready)

Verified: register_contributor/collect_contributions round-trip works correctly
with amplifier-core 1.4.1 Rust engine (direct API test). Private JSONL confirms
all 5 hook modules are running. Full session coordinator event flow needs further
investigation of on_session_ready invocation in the live session lifecycle.
…nstall

Root cause of coordinator events not appearing in events.jsonl:

The Amplifier CLI caches modules in ~/.amplifier/cache/. The loader adds
these cache dirs to sys.path BEFORE site-packages. On session start,
amplifier_module_hooks_logging is imported from the OLD cache file
(without on_session_ready) and placed in sys.modules. When B2 detection
in _load_entry_point() runs, it finds has_osr=False and never sets
__on_session_ready__ on the hook mount function. Phase 6 queue stays 0
and on_session_ready() is never called. coordinator.hooks.emit() fires
but no handler is registered for memory-mempalace:* events.

Fix: after `uv pip install` of the fork, also find all Amplifier cache
copies of hooks_logging/__init__.py and overwrite them with the fork's
version. This ensures both the pip install AND the Amplifier cache have
on_session_ready.

Investigation evidence (session 9201aaf7):
- EP_ENTRY_DEBUG confirmed _load_entry_point IS called for hooks-logging
- AFTER_LOAD_DEBUG confirmed ep.load() succeeds (fn_module correct)
- B2_MOD_DEBUG confirmed the module in sys.modules is the OLD cache:
    mod_file=~/.amplifier/cache/amplifier-module-hooks-logging-2d15c63e3ceed7b8/...
    has_osr=False
- After fix: has_osr=True, PHASE6:queue_size=1, A:on_session_ready_called
- Result: 6+ memory-mempalace: events per session in events.jsonl

Both provision (step 15) and update section include the cache patch.
Forward-integrate commit 2ba94f7 from michaeljabbour/amplifier-bundle-memory
into this branch. MJ corrected four factual errors in dtu.md that prevented
the guide from working on macOS (Colima 0.10.1, Incus 6.0.0 in-VM):

- amplifier-digital-twin: install from git repo, not PyPI
- amplifier-gitea: add as prerequisite with install instructions
- One-Time Gitea Setup: replace non-existent subcommands and manual
  curl migrate with amplifier-gitea create + mirror-from-github
- Launch: explicit profile path + --name pattern

No conflict with our branch: MJ touched only docs/development/dtu.md;
we only touched .amplifier/digital-twin-universe/profiles/memory-bundle-e2e.yaml.
…odule

Move the duplicated bridge_emit closure from all 5 modules into
amplifier_module_tool_mempalace.coordinator_bridge — the natural home
since tool-mempalace is already imported by all hooks via event_emitter.

New module exposes:
  make_async_bridge(coordinator)  → AsyncBridge
  make_sync_bridge(coordinator)   → SyncBridge  (captures loop at mount time)
  register_events(coordinator, contributor, events)
  NOOP_ASYNC_BRIDGE / NOOP_SYNC_BRIDGE  (testable no-op defaults)

Ten inconsistencies eliminated:
- _SYNC_BRIDGE_EMIT module globals deleted from capture and tool-mempalace
- PalaceTool gains bridge_emit constructor param + NOOP default (testable
  without mount())
- Parameter name unified: sync_bridge_emit → bridge_emit everywhere
- register_contributor try/except centralised in register_events (always
  best-effort; was only in capture before)
- Async bridge try/except guard added to interject + project-context
  (was missing; briefing already had it)
- Sync bridge loop.is_closed() guard added to tool-mempalace (was
  missing; capture already had it)
- Sync bridge try/except added to tool-mempalace (was missing)
- register_events snapshots the events list (prevents mutation hazard
  from project-context's _COORDINATOR_EVENTS pattern)
- unused asyncio import removed from capture

45 tests pass. ruff clean.
…, smoke test

Four improvements based on pain points found during the coordinator event
wiring investigation:

T1.1 — Replace chicken-and-egg hooks-logging patch with rm -rf:
  The old approach patched the cache file AFTER a primer session ran (timing
  dependency + ~15 lines of fragile shell). New: install fork, then delete the
  cache dir. Python falls through to site-packages on every session start.
  No timing issue. No primer session needed. Verified working in DTU.

T1.2 — Targeted cache invalidation in update (not full wipe):
  Old update section: rm -rf /root/.amplifier/cache/ — wiped provider-anthropic,
  loop-streaming, and all foundation modules. Sessions failed with 'No providers
  available' until Amplifier was reinstalled (~5 min). New: only removes the
  memory bundle and hooks-logging cache dirs.

T1.3 — Post-provision smoke test (step 17):
  Runs a real session after provisioning. Checks on_session_ready, no shadowing
  cache, and memory-mempalace:* events in events.jsonl. Fails the provision
  loudly rather than silently producing a broken environment.

T3.1 — Troubleshooting docs in dtu.md:
  Three new entries: cache shadowing (zero coordinator events), full-cache-wipe
  breakage, and the Python scoping gotcha with import inside if blocks.
@michaeljabbour michaeljabbour merged commit 627b4ad into michaeljabbour:main Apr 30, 2026
2 checks passed
@colombod colombod deleted the feat/coordinator-event-wiring branch April 30, 2026 12:06
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.

2 participants