LearnNewsExamplesServices
Frontmatter
id16987
titleA provider-activity row outlives the process that opened it, and no reader can tell
stateOpen
labels
bugai
assigneesneo-opus-grace
createdAtAug 11, 2026, 5:32 PM
updatedAtAug 11, 2026, 5:57 PM
githubUrlhttps://github.com/neomjs/neo/issues/16987
authorneo-opus-grace
commentsCount1
parentIssuenull
subIssues[]
subIssuesCompleted0
subIssuesTotal0
contentTrust
projected
quarantined0
signals[]
blockedBy[]
blocking[]

A provider-activity row outlives the process that opened it, and no reader can tell

neo-opus-grace
neo-opus-grace commented on Aug 11, 2026, 5:32 PM

Split out of #16880 after PR #16943 shipped a fix for it that broke that ticket's own subject. Reverted at 9bcb7623f2; recording the defect properly rather than losing it with the commit.

The defect

provider_activity_log rows are durable. The limiter that admits them is per-process and in-memory. So a process that dies mid-flight leaves rows with completed_at IS NULL that nothing will ever complete — the thing that would have completed them is gone — and the live-demand projection counts them as current work.

Measured shape: {cap: 1, executing: 1} immediately after a restart that admitted nothing, and it never clears.

Same class as the rest of the #16706 incident: a durable record outliving the thing it describes, read as current.

Why the obvious fix is wrong — evidence, not prediction

I shipped it and @neo-gpt falsified it.

Stamping a module-scope PROCESS_EPOCH on each row and filtering the projection to process_epoch = @process_epoch means a reader sees only rows it wrote. An MC or Orchestrator reader then reads Knowledge Base live demand as executing: 0 — and cross-service admission visibility is the entire subject of #16880. It closes a latent boundary by opening a live one.

A same-module test cannot catch this. Two services in one process share one epoch, so the writer-A / observer-B split never occurs. My arm passed for a reason unrelated to what it claimed.

What a correct fix needs

A liveness signal, not a stronger filter. The requirement is asymmetric:

  • exclude rows whose owning process is gone
  • include rows whose owning process is live, including other services

Time alone cannot separate those. A dead process's uncompleted row and a live process's slow row are identical from the ledger — which is precisely the waiting-versus-broken conflation that made this incident unreadable for weeks. Any threshold on age reproduces it.

So the epoch has to be joined against something that says whether that generation is still running: a heartbeat table, a registration row with a TTL, or an equivalent. That is a shared-ledger schema decision and it deserves its own design rather than a patch.

Acceptance criteria

  • AC-1 — @neo-gpt's cross-module witness: writer A reports executing: 1 and observer B, in a different module, reports the same row as executing: 1. A same-module two-service arm does not satisfy this and must not be offered as if it did.
  • AC-2 — a row whose owning process is gone is excluded from live demand.
  • AC-3 — NON-VACUITY: a row whose owning process is live but slow is still counted. AC-2 must not be satisfied by an age threshold.
  • AC-4 — rows are RETAINED for history; only their contribution to demand changes. A restart is not a reason to lose the record that something was in flight.
  • AC-5 — the liveness signal fails CLOSED: if a generation's state is unknown, its rows are reported as unknown rather than as absent demand. Unknown must not read as zero — a fabricated 0 reads as "admission is closed", which is the most alarming possible value.

Deltas

  • Latent, not live. It needs a process death mid-flight, and the misreport then persists for that store. It is not what pegged the cores on the affected plane.
  • #16880 keeps the per-process KEYING fix (service::provider), which is unaffected and shipped in PR #16943.