LearnNewsExamplesServices
Frontmatter
id12008
titleConsolidate wake evaluator paths: bridge-daemon delegates to WakeSubscriptionService (single source of truth)
stateOpen
labels
airefactoringarchitecture
assigneesneo-opus-4-7
createdAtMay 26, 2026, 2:59 AM
updatedAtMay 26, 2026, 3:45 AM
githubUrlhttps://github.com/neomjs/neo/issues/12008
authorneo-opus-4-7
commentsCount1
parentIssue11993
subIssues[]
subIssuesCompleted0
subIssuesTotal0
blockedBy[]
blocking[]

Consolidate wake evaluator paths: bridge-daemon delegates to WakeSubscriptionService (single source of truth)

Openairefactoringarchitecture
neo-opus-4-7
neo-opus-4-7 commented on May 26, 2026, 2:59 AM

PR #12004 (Epic #11993 Sub-i closure / route-evaluation gate fix) shipped the dual-patch reality where both WakeSubscriptionService._evaluateHeartbeatPulseAgainstSubscription (service-side) AND ai/daemons/bridge/daemon.mjs::evaluateSubscription (production-direct) gate on harnessTarget === 'bridge-daemon'. Both evaluators implement the SAME logic: iterate (subscriptions × GraphLog traces) and answer "should this subscription fire for this entity?".

The dual-write is an anti-pattern. Every trigger-semantics change requires patching both evaluators in lockstep. Sub-i #11994 originally added HEARTBEAT_PULSE handling to BOTH evaluators; the gate-fix on PR #12004 had to relax BOTH. Future trigger additions, filter semantic updates, and evaluator-logic refinements all carry the same dual-write coordination tax — empirically demonstrated by my cycle-1 attempt at the gate fix (agent/heartbeat-emit-gate-fix befa2fd00) which patched only the service-side evaluator and would have left production silently broken; @neo-gpt's V-B-A on the production bridge-daemon path caught the gap.

Operator architectural verdict 2026-05-26T00:5xZ: "dual-write sounds like an anti-pattern, doesn't it => single source of truth. WakeSubscriptionService should win, daemon.mjs as a main starting point should not even know it exists."

The Architectural Shape

WakeSubscriptionService owns the evaluation substrate entirely. ai/daemons/bridge/daemon.mjs keeps its bridge-daemon-specific concerns (poll cadence, lastSyncId state file at STATE_FILE, adapter dispatch via the harness adapter set, harness-target routing) BUT delegates ALL evaluation work to WakeSubscriptionService. Bridge-daemon doesn't know GraphLog exists at the matching-logic level; doesn't import SQLite query helpers; doesn't know what entity_type === 'heartbeat_pulse' means.

Substrate-correct flow:

  1. Bridge-daemon's pollLoop() checks lastSyncId from its state file
  2. Bridge-daemon calls WakeSubscriptionService.evaluateDeltaForActiveBridgeSubscriptions({sinceLogId: lastSyncId}) — receives {events: [...], lastLogId} enriched per active subscription
  3. Bridge-daemon dispatches each event via its adapter set (osascript / codex-app-server / antigravity-cli / claude-cli / tmux)
  4. Bridge-daemon writes new lastSyncId to state file

The current WakeSubscriptionService.resync() is per-subscription; the consolidation requires a new sibling API that returns events for ALL active bridge-daemon subscriptions in one call (avoids N × DB-reads for N subscriptions).

The Fix

  1. Add WakeSubscriptionService.evaluateDeltaForActiveBridgeSubscriptions({sinceLogId}) method that returns {events: [...], lastLogId} for all currently-active bridge-daemon subscriptions. Narrower contract than overloading resync(); avoids tangling per-subscription auth semantic with the daemon-batch path.

  2. Delete ai/daemons/bridge/daemon.mjs::evaluateSubscription entirely. Delete the GraphLog SQL reads (getGraphLogEntries, getNodesData, getEdgesData, getDbNode). Delete the entity-map iteration. Delete the per-trigger conditional branches (SENT_TO_ME / TASK_STATE_CHANGED / PERMISSION_GRANTED / HEARTBEAT_PULSE matching).

  3. Replace pollLoop body with:

       const result = await WakeSubscriptionService.evaluateDeltaForActiveBridgeSubscriptions({sinceLogId: lastSyncId});
    for (const event of result.events) queueEvent(event.subscription, event.payload);
    lastSyncId = result.lastLogId;
  4. Delete now-orphan helpers in bridge/daemon.mjs: parseHeartbeatPulseEntityId (duplicates WakeSubscriptionService._parseHeartbeatPulseEntityId), any getActiveShapeCSubscriptions SQL helper if unused post-consolidation, etc. Audit for what becomes dead after the evaluator delete.

  5. WakeSubscriptionService.evaluateDeltaForActiveBridgeSubscriptions implementation: single GraphLog read via storage.getDeltaLog(sinceLogId), single iteration that queries active bridge-daemon subscriptions (via existing _getCandidateSubscriptions filtered on harnessTarget), per-trace-per-subscription evaluation using existing _evaluate*AgainstSubscription private helpers (already the substrate-of-record). Return enriched event payloads suitable for direct dispatch.

  6. Test surface migration: delete daemon.spec.mjs tests that exercise the deleted evaluator paths (or rewrite them to assert "bridge-daemon delegates to service"). Add WakeSubscriptionService.spec.mjs coverage for the new batch API.

Contract Ledger

Surface Source of Authority Behavior Evidence
WakeSubscriptionService.evaluateDeltaForActiveBridgeSubscriptions({sinceLogId}) This ticket + Epic #11993 cycle-3 framing Single GraphLog read + per-active-bridge-daemon-subscription evaluation via existing private _evaluate*AgainstSubscription helpers; returns {events: [...], lastLogId} New unit test in WakeSubscriptionService.spec.mjs: matrix of trigger types × subscription presence × entity-type matches
ai/daemons/bridge/daemon.mjs::evaluateSubscription (deleted) This ticket Function removed entirely; bridge-daemon no longer knows about GraphLog or trigger semantics Static grep: grep -rn "evaluateSubscription|entity_type|heartbeat_pulse|HEARTBEAT_PULSE" ai/daemons/bridge/ returns zero matches post-merge
ai/daemons/bridge/daemon.mjs::pollLoop This ticket Calls service API, dispatches events; no SQL, no entity resolution, no trigger matching Existing daemon.spec.mjs end-to-end tests (the real-daemon-process tests from PR #12004) still pass against the consolidated implementation
parseHeartbeatPulseEntityId (bridge/daemon.mjs helper) This ticket Deleted — duplicates _parseHeartbeatPulseEntityId in service Grep clean

Acceptance Criteria

  • AC1 — WakeSubscriptionService.evaluateDeltaForActiveBridgeSubscriptions({sinceLogId}) exists; returns {events: [...], lastLogId}; events are enriched per the existing _wrapEvent envelope.
  • AC2 — ai/daemons/bridge/daemon.mjs::evaluateSubscription deleted entirely. Static grep returns zero matches.
  • AC3 — pollLoop body collapsed to "fetch via service + dispatch + update lastSyncId" shape. No SQL, no entity-map iteration, no trigger conditionals in bridge/daemon.mjs.
  • AC4 — Orphan helpers in bridge/daemon.mjs removed (parseHeartbeatPulseEntityId, any unused SQL queries). Static grep clean.
  • AC5 — WakeSubscriptionService.spec.mjs unit coverage for the new batch API: trigger matrix (SENT_TO_ME edge / TASK_STATE_CHANGED node / PERMISSION_GRANTED edge / heartbeat_pulse) × subscription presence (active / inactive / non-bridge) × delivery shape.
  • AC6 — Existing bridge-daemon end-to-end tests (the real-daemon-process tests from PR #12004 daemon.spec.mjs) PASS against the consolidated implementation. Bridge-daemon dispatch contract preserved end-to-end.
  • AC7 — Live smoke: orchestrator + bridge-daemon launch with the consolidated substrate; pulse delivery to Codex Desktop succeeds for @neo-gpt post-restart (substrate-correct shape proven in the same path Epic #11993 AC8 measures).
  • AC8 — Cross-family review per pull-request §6.1.

Out of Scope

  • Bridge-daemon's adapter dispatch logic (osascript / codex-app-server / antigravity-cli / claude-cli / tmux) — that's bridge-daemon-specific delivery substrate, stays in bridge-daemon.
  • Bridge-daemon's lastSyncId state file management — process-local state, stays in bridge-daemon.
  • Other consumers of WakeSubscriptionService.resync() (e.g., harness-facing MCP callers using the per-subscription API) — keep resync() intact alongside the new batch API.
  • Performance optimization beyond single-DB-read-per-poll-cycle — micro-tuning is a separate concern if profiling shows it.

Avoided Traps

  • Shared helper module instead of delegation — would leave bridge-daemon knowing about GraphLog, trigger semantics, and entity types. Operator-explicit: "daemon.mjs as a main starting point should not even know it exists." Delegation is the substrate-correct shape; helper-module would be a half-measure.
  • Backwards-compat preservation — none required. Bridge-daemon is process-internal; no external consumer of its evaluateSubscription symbol. Per operator standing rule: no backwards-compat for these substrate-evolution migrations.
  • Folding into adjacent lanes (#12003 candidate-discovery, #12005 daemon.mjs cleanup) — different substrate concerns. #12003 is candidate-set discovery; #12005 is orchestrator-daemon thinning; this ticket is bridge-daemon thinning + WakeSubscriptionService consolidation. Adjacent but non-overlapping.
  • Touching the bridge-daemon adapter set — wake delivery shape stays as-is; only the evaluation half consolidates.

Related

  • Parent Epic: #11993 (Wake substrate evolution — graduated from Discussion #11992)
  • Triggering V-B-A: @neo-gpt cycle-1 review on my PR #11999 / commit befa2fd00, then the subsequent dual-patch reality in PR #12004 (merged) — both evaluator surfaces had to be touched in lockstep
  • Architectural verdict: @tobiu 2026-05-26T00:5xZ — "dual-write sounds like an anti-pattern... single source of truth. WakeSubscriptionService should win, daemon.mjs as a main starting point should not even know it exists."
  • Adjacent lanes (non-overlapping):
    • #12003active-a2a-participants candidate discovery (touches swarmHeartbeat.mjs + SwarmHeartbeatService.mjs)
    • #12005 — daemon.mjs router cleanup (PR #12006, approved, touches Orchestrator entry-point)
    • #12007 — DreamService.spec noise-floor (touches different spec files)

Handoff Retrieval Hint: "Wake evaluator consolidation single source of truth bridge-daemon delegates WakeSubscriptionService evaluateDeltaForActiveBridgeSubscriptions Epic 11993 follow-up dual-write anti-pattern".