LearnNewsExamplesServices
Frontmatter
id14260
titleHeal-event ledger records the heal ATTEMPT but not the dispatch OUTCOME
stateClosed
labels
enhancementaiarchitecture
assigneesneo-opus-grace
createdAtJun 27, 2026, 7:29 PM
updatedAtJun 27, 2026, 8:46 PM
githubUrlhttps://github.com/neomjs/neo/issues/14260
authorneo-opus-grace
commentsCount0
parentIssuenull
subIssues[]
subIssuesCompleted0
subIssuesTotal0
contentTrust
projected
quarantined0
signals[]
blockedBy[]
blocking[]
closedAtJun 27, 2026, 8:46 PM

Heal-event ledger records the heal ATTEMPT but not the dispatch OUTCOME

Closed v13.1.0/archive-v13-1-0-chunk-7 enhancementaiarchitecture
neo-opus-grace
neo-opus-grace commented on Jun 27, 2026, 7:29 PM

Context

Surfaced V-B-A'ing #14158's premise on dev (the chronic-unsafe-input detector): the dispatch heal outcome (unsafe-input / failed / healed / deferred, with detail) is produced and flows up the call chain, but is never persisted to any durable ledger. This is a foundational gap shared by two consumers, so it deserves its own lane.

The Problem

On the autonomous data-recovery dispatch path:

  • recordRun (the only durable write) records the pre-execution attempt (status: 'attempt') — see Orchestrator.mjs:455-456appendHealEvent({type: action, status: 'attempt'}). Not the outcome.
  • dispatchHeal returns the outcome {action, collection, status, detail, healedAt} (healActionDispatch.mjs), and DataRecoveryActuatorService.applyHeal returns it up to DataIntegrityDiagnosisService.applyHeals (:142-154), which collects it in heals and returns it — without persisting it.
  • recoveryRunStateStore's RECOVERY_RUN_STATUSES has no unsafe-input at all.

So the heal-event ledger holds attempts, not outcomes.

The Architectural Reality

  • ai/daemons/orchestrator/services/DataIntegrityDiagnosisService.mjs:129-158 (applyHeals — the runner cycle that holds each outcome).
  • ai/services/memory-core/helpers/healEventLedgerStore.mjs (appendHealEvent — the sink).
  • ai/daemons/orchestrator/Orchestrator.mjs:455-456 (the existing recordRun wiring to mirror).

The Fix

A recordHealOutcome recorder mirroring the existing recordRun: the runner persists the dispatch outcome ({action, collection, status, detail, at}) to the heal-event ledger after each applyHeal. The orchestrator wires it (appendHealEvent), reading the ledger dir at the use-site per ADR-0019 (NO const, NO hidden default).

Acceptance Criteria

  • Each dispatch outcome (including unsafe-input and failed, with detail) is appended to the heal-event ledger via the wired recorder.
  • No change to decideHealAction / dispatchHeal fail-closed semantics.
  • Ledger-dir resolution is a use-site AiConfig read (ADR-0019), not a const/threaded value.
  • Tested (the runner records the outcome; the orchestrator wiring resolves the dir).

Contract Ledger

Surface Field / Behavior Contract
DataRecoveryActuatorService.recordHealOutcome(outcome) injected recorder async ({action, collection, status, detail, healedAt}) => void; set-once injected dependency; null → not recorded (observability-only — never a heal gate)
↳ failure semantics recorder fault swallowed — a recording failure never breaks or re-triggers the heal; applyHeal still returns its outcome
heal-event ledger outcome row appendHealEvent({type, collection, status, detail}, {dir, now}) typeoutcome.action; collectionoutcome.collection; statusoutcome.status (healed / failed / deferred / unsafe-input); detailoutcome.detail; atoutcome.healedAt (the healedAtat mapping)
vs. recordRun (the attempt) distinct rows the pre-execution status:'attempt' rows remain SEPARATE from these outcome rows — attempt and outcome are distinct ledger entries on the same collection
anti-thrash recentRuns attempt rows ONLY healEventsToRecentRuns (the recentRunsReader projection for decideHealAction) filters to status:'attempt' — outcome rows are observability, NOT additional runs (counting both would double the per-(action,collection) run-count and tighten the rate-limit)
consumers downstream readers #14158 reads unsafe-input rows; #14179 slice-2 reads failed rows + detail (the decideSystemicCircuit inputs)

Out of Scope

The consumers — they read the recorded outcomes once this lands:

  • #14158 (chronic-unsafe-input detector) reads the unsafe-input outcomes.
  • #14179 slice-2 (systemic circuit-breaker wiring) reads the failed outcomes with detail (the decideSystemicCircuit decider merged in #14252 expects exactly these).

Decision Record impact

aligned-with ADR-0027 (the autonomous data-recovery actuator) + aligned-with ADR-0019 (use-site ledger-dir read).

Related

  • #14158 (consumer — detector), #14179 (consumer — systemic circuit slice-2), #14163 (the heal-event ledger), #14132 (the act-half epic).

Origin Session ID

090a68e6-1a28-4b20-a5fd-842ebac3e729

Retrieval Hint

query_summaries: "dispatch heal outcome recording heal-event ledger unsafe-input recordRun attempt only"; the #14158 reshape comment.

tobiu referenced in commit 342d723 - "feat(ai): persist the dispatch heal outcome to the heal-event ledger (#14260) (#14262) on Jun 27, 2026, 8:46 PM
tobiu closed this issue on Jun 27, 2026, 8:46 PM