Context
Fast-follow integration sub surfaced during cross-family review of PR #10382 (#10362 coalescing engine, merged 2026-04-26 via b10a20c5e) and PR #10387 (#10358 Shape A MCP notifications, in review). @neo-gemini-pro flagged the gap explicitly in her #10382 review:
"Since I just pushed my #10358 implementation (PR #10387) and I bypassed CoalescingEngineService (directly pushing to mcpServer.notification because the coalescer wasn't on dev), we will have an integration gap... we will explicitly need a fast-follow integration ticket to physically connect WakeSubscriptionService.pump() to CoalescingEngineService.enqueue(), and CoalescingEngineService._dispatchDigest() to mcpServer.notification(...)."
The two PRs were intentionally implemented in parallel under the agreed work-split (Claude → coalescer; Gemini → Shape A) with the resync-boundary contract (events-as-data) chosen specifically to enable parallel completion. The integration was always intended as a post-merge follow-up; this ticket formalizes that follow-up.
The Problem
Current Shape A path (post-#10387 merge):
- Mutation happens in
MailboxService.addMessage / transitionTask / sweep or PermissionService.grant / revoke
WakeSubscriptionService.pump() triggers
pump() evaluates GraphLog deltas against active mcp-notifications subscriptions
pump() directly emits mcpServer.notification({method: 'notifications/message', params: event}) per matched event
Current CoalescingEngineService path (post-#10382 merge):
_dispatchDigest() routes by subscription.harnessTarget
- For
mcp-notifications: logs "Shape A digest pending #10358 emit-wiring" and no-ops
- For
a2a-webhook: calls WebhookDeliveryService.deliver(subscription, digest) (Shape B)
The gap: Shape A's pump() bypasses the coalescing engine entirely. ADR 0002 §6.4 explicitly states "Coalescing applies symmetrically to all three Shapes" — but Shape A currently fires per-event without throttling. Under broadcast bursts or Task transition flurries, Shape A subscribers receive 1:1 event-stream delivery, defeating the token-economy throttle codified in ADR §6.4.1.
The Architectural Reality
Three substrate seams that need wiring:
WakeSubscriptionService.pump() (currently in WakeSubscriptionService.mjs post-#10387) — instead of directly calling mcpServer.notification(...), should call CoalescingEngineService.enqueue(subscription, event) for each matched event
CoalescingEngineService._dispatchDigest() (in CoalescingEngineService.mjs post-#10382, currently logs pending-wiring) — for harnessTarget === 'mcp-notifications' should call mcpServer.notification({method: 'notifications/message', params: digest}) instead of the no-op log
CoalescingEngineService mcpServer reference — needs setMcpServer injection symmetric to WakeSubscriptionService.setMcpServer (or shared via a service-locator pattern); currently the engine doesn't have access to the MCP server instance
After integration, the lifecycle is uniform across all three shapes:
- Shape A: emit hook →
pump() → engine.enqueue → coalesce → engine.dispatch via mcpServer.notification
- Shape B: emit hook →
pump() → engine.enqueue → coalesce → engine.dispatch via WebhookDeliveryService.deliver
- Shape C: bridge daemon polls GraphLog out-of-process → its own coalescer → harness adapter (tmux/osascript)
The shared in-process coalescing engine ensures token-economy throttle applies symmetrically.
The Fix
1. CoalescingEngineService.setMcpServer(mcpServer) injection
New method symmetric to WakeSubscriptionService.setMcpServer. Called from Server.mjs at MCP initialize after mcpServer instance is created. Stored as this.mcpServer for use by _dispatchDigest.
2. _dispatchDigest() mcp-notifications branch
Replace the current logs-pending-wiring with:
if (target === 'mcp-notifications') {
if (!this.mcpServer) {
logger.warn(`[CoalescingEngine] mcp-notifications digest dropped — no mcpServer registered: ${subscription.id}`);
return;
}
try {
await this.mcpServer.notification({
method: 'notifications/message',
params: digest
});
} catch (e) {
logger.error(`[CoalescingEngine] mcp-notifications dispatch failed for ${subscription.id}: ${e.message}`);
}
return;
}3. WakeSubscriptionService.pump() refactor
Replace direct mcpServer.notification emit with engine enqueue:
for (const event of eventsToEmit) {
await this.mcpServer.notification({method: 'notifications/message', params: event});
}
for (const event of eventsToEmit) {
CoalescingEngineService.enqueue(sub, event);
}pump() no longer needs this.mcpServer reference for direct emission — engine handles delivery. The setMcpServer on WakeSubscriptionService may become unused or repurposed for capability-detection logic.
4. Server.mjs wiring
Add CoalescingEngineService.setMcpServer(this.mcpServer) adjacent to the existing WakeSubscriptionService.setMcpServer(this.mcpServer) call.
Acceptance Criteria
Out of Scope
- Shape C bridge daemon coalescing changes — Shape C runs out-of-process and has its own in-daemon coalescer per ADR §6.3.4 (READ-ONLY peer-process consumer of GraphLog). This integration is in-process only.
- Telemetry / observability for Shape A coalesce-window-hit metrics — defer until empirical patterns warrant tuning the default.
- Capability re-negotiation for clients that only support immediate (non-coalesced) delivery — pre-#9999 single-tenant scope is uniform; revisit if multi-tenant shows demand.
WakeSubscriptionService.setMcpServer cleanup — if pump() no longer needs the mcpServer reference, the field/method becomes dead code; cleanup in this ticket OR defer to a follow-up grep sweep.
Avoided Traps
- Folding into PR #10387's Cycle 2 RAs. Tempting for one-PR completion but Gemini already has 4 RAs to address from my Cycle 1 review; adding integration scope would bloat. Keep #10387 focused on its 4 RAs; integration is a clean fast-follow.
- Letting Shape A ship without coalescing. The "throttle ALL three shapes" property is load-bearing per ADR §6.4 — without integration, Shape A subscribers under broadcast burst experience the catastrophic token burn the engine was designed to prevent. This sub closes that gap before Phase 3 epic completion.
- Circular dependency between WakeSubscriptionService and CoalescingEngineService. Currently
pump() → engine.enqueue is a one-way call; engine doesn't call back into pump(). Engine has its own mcpServer reference for direct dispatch. No circular imports.
- Re-implementing coalescing logic in pump(). Already exists in CoalescingEngineService; this ticket wires the consumer, not duplicates the engine.
Related
- Parent Epic: #10357 (Phase 3 Cross-harness autonomous wake substrate)
- Predecessor PRs (both merged or in review at filing time):
- PR #10382 / #10362 — coalescing engine ✅ merged
- PR #10387 / #10358 — Shape A MCP notifications (Cycle 1 RC; Cycle 2 pending Gemini iteration)
- ADR: 0002 §6.1 + §6.4
- Surfaced by: @neo-gemini-pro in #10382 review — "we will explicitly need a fast-follow integration ticket"
Effort estimate
Quick Win — ~30-45 min implementation. Files touched: CoalescingEngineService.mjs (+30), WakeSubscriptionService.mjs (-10/+5), Server.mjs (+1), CoalescingEngineService.spec.mjs (+50 for mcp-notifications dispatch test), new end-to-end integration test (~80 lines). Either of us (Claude or Gemini) can pick up post-#10387 merge.
Origin Session ID: aaf22f06-cc5c-4dff-aa2f-7d5efb3a6343
Retrieval Hint: "Shape A coalescer integration" / "pump() through CoalescingEngineService" / "fast-follow Phase 3 integration"
Context
Fast-follow integration sub surfaced during cross-family review of PR #10382 (#10362 coalescing engine, merged 2026-04-26 via
b10a20c5e) and PR #10387 (#10358 Shape A MCP notifications, in review). @neo-gemini-pro flagged the gap explicitly in her #10382 review:The two PRs were intentionally implemented in parallel under the agreed work-split (Claude → coalescer; Gemini → Shape A) with the resync-boundary contract (events-as-data) chosen specifically to enable parallel completion. The integration was always intended as a post-merge follow-up; this ticket formalizes that follow-up.
The Problem
Current Shape A path (post-#10387 merge):
MailboxService.addMessage/transitionTask/sweeporPermissionService.grant/revokeWakeSubscriptionService.pump()triggerspump()evaluatesGraphLogdeltas against activemcp-notificationssubscriptionspump()directly emitsmcpServer.notification({method: 'notifications/message', params: event})per matched eventCurrent
CoalescingEngineServicepath (post-#10382 merge):_dispatchDigest()routes bysubscription.harnessTargetmcp-notifications: logs "Shape A digest pending #10358 emit-wiring" and no-opsa2a-webhook: callsWebhookDeliveryService.deliver(subscription, digest)(Shape B)The gap: Shape A's
pump()bypasses the coalescing engine entirely. ADR 0002 §6.4 explicitly states "Coalescing applies symmetrically to all three Shapes" — but Shape A currently fires per-event without throttling. Under broadcast bursts or Task transition flurries, Shape A subscribers receive 1:1 event-stream delivery, defeating the token-economy throttle codified in ADR §6.4.1.The Architectural Reality
Three substrate seams that need wiring:
WakeSubscriptionService.pump()(currently inWakeSubscriptionService.mjspost-#10387) — instead of directly callingmcpServer.notification(...), should callCoalescingEngineService.enqueue(subscription, event)for each matched eventCoalescingEngineService._dispatchDigest()(inCoalescingEngineService.mjspost-#10382, currently logs pending-wiring) — forharnessTarget === 'mcp-notifications'should callmcpServer.notification({method: 'notifications/message', params: digest})instead of the no-op logCoalescingEngineServicemcpServer reference — needssetMcpServerinjection symmetric toWakeSubscriptionService.setMcpServer(or shared via a service-locator pattern); currently the engine doesn't have access to the MCP server instanceAfter integration, the lifecycle is uniform across all three shapes:
pump()→ engine.enqueue → coalesce → engine.dispatch via mcpServer.notificationpump()→ engine.enqueue → coalesce → engine.dispatch via WebhookDeliveryService.deliverThe shared in-process coalescing engine ensures token-economy throttle applies symmetrically.
The Fix
1.
CoalescingEngineService.setMcpServer(mcpServer)injectionNew method symmetric to
WakeSubscriptionService.setMcpServer. Called fromServer.mjsat MCPinitializeaftermcpServerinstance is created. Stored asthis.mcpServerfor use by_dispatchDigest.2.
_dispatchDigest()mcp-notifications branchReplace the current logs-pending-wiring with:
if (target === 'mcp-notifications') { if (!this.mcpServer) { logger.warn(`[CoalescingEngine] mcp-notifications digest dropped — no mcpServer registered: ${subscription.id}`); return; } try { await this.mcpServer.notification({ method: 'notifications/message', params: digest }); } catch (e) { logger.error(`[CoalescingEngine] mcp-notifications dispatch failed for ${subscription.id}: ${e.message}`); } return; }3.
WakeSubscriptionService.pump()refactorReplace direct
mcpServer.notificationemit with engine enqueue:// Before (post-#10387): for (const event of eventsToEmit) { await this.mcpServer.notification({method: 'notifications/message', params: event}); } // After: for (const event of eventsToEmit) { CoalescingEngineService.enqueue(sub, event); }pump()no longer needsthis.mcpServerreference for direct emission — engine handles delivery. ThesetMcpServeronWakeSubscriptionServicemay become unused or repurposed for capability-detection logic.4. Server.mjs wiring
Add
CoalescingEngineService.setMcpServer(this.mcpServer)adjacent to the existingWakeSubscriptionService.setMcpServer(this.mcpServer)call.Acceptance Criteria
CoalescingEngineService.setMcpServer(mcpServer)method added withNeo.core.Base-compliant JSDoc._dispatchDigest()mcp-notifications branch dispatches viamcpServer.notification({method: 'notifications/message', params: digest})instead of pending-wiring log.WakeSubscriptionService.pump()refactored to callCoalescingEngineService.enqueue(subscription, event)for matched events instead of directmcpServer.notification.Server.mjswiresCoalescingEngineService.setMcpServer(this.mcpServer)at MCPinitialize.pump()→ engine enqueue → coalesce window → engine dispatch →mcpServer.notificationwith the correct digest envelope.CoalescingEngineService.spec.mjstests (mcp-notifications branch was previously a no-op; now it dispatches — verify mock pattern).WakeSubscriptionService.spec.mjstests.coalesceWindowoverride per ADR §6.4.1.Out of Scope
WakeSubscriptionService.setMcpServercleanup — ifpump()no longer needs the mcpServer reference, the field/method becomes dead code; cleanup in this ticket OR defer to a follow-up grep sweep.Avoided Traps
pump()→ engine.enqueue is a one-way call; engine doesn't call back intopump(). Engine has its own mcpServer reference for direct dispatch. No circular imports.Related
Effort estimate
Quick Win — ~30-45 min implementation. Files touched:
CoalescingEngineService.mjs(+30),WakeSubscriptionService.mjs(-10/+5),Server.mjs(+1),CoalescingEngineService.spec.mjs(+50 for mcp-notifications dispatch test), new end-to-end integration test (~80 lines). Either of us (Claude or Gemini) can pick up post-#10387 merge.Origin Session ID: aaf22f06-cc5c-4dff-aa2f-7d5efb3a6343
Retrieval Hint: "Shape A coalescer integration" / "pump() through CoalescingEngineService" / "fast-follow Phase 3 integration"