LearnNewsExamplesServices
Frontmatter
id17647
titlehealthcheck reports an unreadable liveness file as not-running
stateClosed
labels
bugaimodel-experienceagent-os
assigneesneo-opus-grace
createdAtAug 23, 2026, 9:29 PM
updatedAtAug 23, 2026, 11:18 PM
githubUrlhttps://github.com/neomjs/neo/issues/17647
authorneo-opus-grace
commentsCount0
parentIssuenull
subIssues[]
subIssuesCompleted0
subIssuesTotal0
contentTrust
projected
quarantined0
signals[]
blockedBy[]
blocking[]
closedAtAug 23, 2026, 11:18 PM

healthcheck reports an unreadable liveness file as not-running

Closed Backlog/active-chunk-19 bugaimodel-experienceagent-os
neo-opus-grace
neo-opus-grace commented on Aug 23, 2026, 9:29 PM

Context

Surfaced while investigating #17646. The operator raised heartbeat delivery as a possible cause of A2A wake noise and flagged the guess as uncertain — "absolutely NOT sure here, because we previously disabled it on purpose."

That uncertainty is the defect. No live surface can answer it, and the one that looks like it can gives the same answer either way.

The Problem

healthcheck returns:

"wake": {"gateState": "unknown", "daemonRunning": false, "lastPulseAt": null,}

HealthService.mjs documents daemonRunning as a heuristic over the heartbeat-liveness file's mtime, and the block degrades to those values without throwing when that file cannot be read at all. So a false produced by looking and finding nothing is byte-identical to a false produced by not being able to look — a permission wall, or a path belonging to another container's realm.

An unobservable value rendered as a measured negative is the failure mode, and it is not hypothetical: it cost the #17646 wake-noise investigation its first wrong turn. daemonRunning: false was read as evidence the heartbeat was uninvolved — the right conclusion, reached from an instrument that would have reported identically had the heartbeat been the cause.

What this ticket does NOT claim (corrected 2026-08-23): that the payload can identify why a pulse file is absent. It cannot, and after this fix it still cannot. A configured-off lane and an enabled lane that has never pulsed are the same ENOENT. The repair is to stop a non-answer from wearing an answer's clothes, not to add a configuration verdict.

The Architectural Reality

  • WakeSubscriptionService.mjs:111validHarnessTargets = ['mcp-notifications', 'a2a-webhook', 'bridge-daemon', 'disabled', 'none'].
  • configBase.mjs:1968 / :1987bridgeDaemonEnabled and swarmHeartbeatEnabled both default false, deliberately: "because the Stop hook makes wake + heartbeat redundant."
  • HealthService.mjs:480-481 — the degraded defaults are literals in the failure path, indistinguishable from measured values.

The block is not wrong about what it measures. It is scoped to one route and reports as if it described the wake plane, and it cannot signal its own blindness.

The Fix

Narrowed 2026-08-23 (@neo-gpt-emmy, RA-1 on PR #17652). The original four bullets prescribed a configuration surface this block cannot deliver — it reads one liveness file and consults no config. They are struck below rather than deleted, so a later reader sees what was proposed and why it was dropped.

Make the liveness figure report what the read did, so a non-answer cannot wear an answer's clothes:

  • daemonRunning becomes tri-state. true fresh, false when the file was looked for and is absent, null when the read could not happen at all — a permission wall, or a path in another container's realm. An unobservable value must never render as a measured negative.
  • A companion livenessReason names which read occurred: observed | no-pulse-file | unreadable. no-pulse-file is defined as exactly ENOENT and carries no claim about configuration.
  • Durable prose states the limit plainly: a configured-off lane and an enabled-but-silent lane are the same absent file and stay indistinguishable here.

- Distinguish disabled from unreadable. gateState should carry a third value… - Report the configured route (harnessTarget)… - Report liveness for the route in use - Include the effective swarmHeartbeatEnabled / bridgeDaemonEnabled values…

Why those four are dropped, not deferred: all of them require reading the orchestrator's config tree or the subscription's route, neither of which the memory-core config slice carries. Reaching across that boundary to improve a diagnostic label costs more than it buys, and the urgency behind them died with #17648 — the wake noise had nothing to do with the heartbeat. The one that survived, daemonRunning: null, is the only one that was ever a correctness problem rather than a labelling one.

Contract Ledger Matrix

Narrowed with The Fix above. The prior three rows required route identity and effective enable flags; they are struck and replaced by the two surfaces this contract actually changes.

# Target surface Source of authority Before After Fallback Evidence
1 healthcheck.features.wake.daemonRunning HealthService.mjs false for both "absent" and "unreadable" tri-state: null when the read could not happen false stays the answer for a genuine absent-file read the catch swallowed every fs.stat failure into one default
2 healthcheck.features.wake.livenessReason same absent observed | no-pulse-file | unreadable; no-pulse-file is ENOENT none — always present names the read, never the configuration

| gateState — distinct states for configured-off vs unobservable | · | daemonRunning scoped to the named route | · | wake carries harnessTarget + effective enable flags | — dropped per The Fix.

Acceptance Criteria

Narrowed 2026-08-23 after @neo-gpt-emmy's RA-1 on PR #17652. The original four ACs described a configuration surface this block cannot deliver — see Out of Scope. What remains is the observation contract: this function reads one file, and its payload must report what that read did.

  • With the liveness file unreadable, the payload reports the value as unobservable rather than as false. Verified by making it unreadable, not by reading the code path — an unobservable-vs-false distinction asserted only in prose is the defect restated.
  • The payload names what the read did — observed | no-pulse-file | unreadable — and no-pulse-file is defined as exactly ENOENT, never as a configuration verdict.
  • A control proves the block is not vacuous: with the liveness file fresh, daemonRunning reads true and lastPulseAt is populated. Without this, every arm above passes on a payload that is permanently and uninformatively "off".
  • No durable prose — docblock, comments, or test names — claims the payload distinguishes a configured-off lane from an enabled-but-silent one. It cannot; both are the same absent file.

Out of Scope

  • Distinguishing a configured-off heartbeat from an enabled-but-dead one. This was the ticket's original headline and it is dropped, not deferred. buildWakeFeaturesBlock reads one liveness file and no configuration; swarmHeartbeatEnabled lives in the orchestrator's config tree, which the memory-core slice does not carry. Reaching across that boundary to improve a diagnostic label buys less than the coupling costs. The two states remain the same ENOENT and the payload now says so plainly instead of implying otherwise.

    Dropped rather than parked because the pressure behind it is gone: the wake noise that made this urgent was #17648's schema-default defect, fixed and deployed, and the heartbeat is confirmed disabled by config default. If someone later needs the distinction, they will need a config read and should file it against that need, not inherit a stale pointer from here.

  • Reporting the delivery route (harnessTarget) alongside the liveness figure. Same boundary, and #17646 already owns the delivery-route question.

  • The wake-noise defect itself#17646 / #17648. This ticket is about the instrument, not the noise.

  • Changing any default. swarmHeartbeatEnabled: false and bridgeDaemonEnabled: false are deliberate and correct.

Avoided Traps

  • "It reports false and the thing is false, so it is correct." It is accidentally correct today, for a reason unrelated to what it measures, and would be identically false if the heartbeat were enabled and broken. A value that cannot be wrong cannot be evidence.
  • Adding a new health field and leaving the old one. daemonRunning is the field readers already consult; a truer sibling beside a misleading incumbent means the misleading one keeps being read.
  • Fixing this by documenting it. The conflation is already documented at HealthService.mjs:437 — I read that docblock and still had to go to the config and the container logs to answer the question. Prose that accurately describes an ambiguous value does not disambiguate it.

Decision Record impact

none — a diagnostics-surface correction; no wake semantics change.

Structure-map gate: N/A — lands in the existing HealthService seam, no file introduced or relocated.

Live latest-open sweep: checked latest 20 open issues at 2026-08-23T19:28Z; no equivalent found. A2A in-flight claim sweep: latest 40 all-state messages; no overlapping claim.

Related

#17646 (the wake-noise defect this was raised against; heartbeat exonerated there) · #14486 (established that the heartbeat/wake disable toggles already existed and the real gap was discoverability — this is that same gap, one surface over)

Origin Session ID: eb671e6e-ca17-4a53-8069-64fd5885ce84

Retrieval Hint: query_raw_memories("healthcheck wake gateState unknown daemonRunning liveness file disabled vs unreadable harnessTarget")

tobiu referenced in commit b909f8b - "fix(memory-core): wake liveness distinguishes unreadable from not-running (#17647) (#17652) on Aug 23, 2026, 11:18 PM
tobiu closed this issue on Aug 23, 2026, 11:18 PM