Make Ra queries stable across covered peers

f34649b29f0a · Christopher David · · parent a99120717c3d

Make Ra queries stable across covered peers

Use module-function-argument descriptors for session-registry reads so distributed peers do not depend on a captured function identity. Record the successful merged coverage run in Gate 0.

Deploy story

What this commit did to the running system — joined from the forge receipt chain, the part a commit page elsewhere cannot show.

Not deployed through the forge lane

No push, promotion, build, or deploy receipt references this commit (receipts are scanned over a bounded recent window). Changes shipped by full node replacement carry their proof in the release gate receipt instead.

Changed files

  • modified docs/2026-08-20-integration-hardening-and-staging-readiness-recommendations.md
  • modified lib/openagents/cluster/session_registry.ex
  • modified test/openagents/cluster/session_registry_property_test.exs
  • modified test/openagents/cluster/session_registry_test.exs

Diff

4 files changed, +40 -21

docs/2026-08-20-integration-hardening-and-staging-readiness-recommendations.md modified +13 -4

@@ -77,8 +77,7 @@ The baseline at `d5679e8` recorded 1,218 default tests passing with 9 cluster

77 77
tests excluded, all 9 cluster tests passing separately, no compile warnings, no
78 78
hidden skips, and 83.14% line coverage before merging cluster coverage. Keep
79 79
that result as historical evidence, but rerun the complete gate for each
80
candidate. Gate 0 remains blocked until the missing JavaScript suite exists and
81
runs.
80
candidate. The implementation status below supersedes the historical blockers.
82 81
83 82
### Gate 0 implementation status
84 83

@@ -92,9 +91,14 @@ Completed on 2026-08-20:

92 91
  fails when browser-side voice behavior regresses.
93 92
- Removed the remaining test-compilation warnings and made
94 93
  `mix test --warnings-as-errors` part of `mix precommit`.
94
- Replaced Ra's captured session-query functions with stable
95
  module-function-argument descriptors. The cluster suite now runs under
96
  coverage without a peer crashing on an instrumented function identity.
97
- Merged the default and cluster coverage exports locally. The combined result
98
  is 83.37%; the exact-SHA receipt and enforced floor remain pending.
95 99
96
Gate 0 still requires the merged default and cluster coverage report, release
97
startup proof against a disposable database, and an exact-SHA gate receipt.
100
Gate 0 still requires an enforced coverage floor, release startup proof against
101
a disposable database, and an exact-SHA gate receipt.
98 102
99 103
Do not use the current green suite as evidence for untested code. The updated
100 104
coverage audit records strong Issues and Projects coverage and the defects it

@@ -1075,6 +1079,11 @@ names creation of the missing suite as blocking work.

1075 1079
`assets/package.json` provides `npm test`, and `mix precommit` runs the suite
1076 1080
through `mix assets.test`.
1077 1081
1082
The first merged coverage attempt also exposed an instrumented anonymous Ra
1083
query that crashed on a peer with `badfun`. Session-registry queries now use
1084
stable module-function-argument descriptors, and all 9 cluster tests complete
1085
under coverage. The merged default and cluster result is 83.37%.
1086
1078 1087
## A2. Blocker: staging is not isolated from production today
1079 1088
1080 1089
Gate 12 lists staging isolation as a requirement and Gate 15 calls for failure
lib/openagents/cluster/session_registry.ex modified +11 -7

@@ -91,14 +91,18 @@ defmodule OpenAgents.Cluster.SessionRegistry do

91 91
92 92
  # ── query builders (used with :ra.consistent_query) ────────────────────────
93 93
94
  @doc "A linearizable query function returning the entry for `id` (or nil)."
95
  def lookup(id), do: fn %{sessions: sessions} -> Map.get(sessions, id) end
94
  @doc "A linearizable query descriptor returning the entry for `id` (or nil)."
95
  def lookup(id), do: {__MODULE__, :lookup_query, [id]}
96 96
97
  @doc "A linearizable query function returning all session ids owned by `node`."
98
  def owned_by(owner) do
99
    fn %{sessions: sessions} ->
100
      for {id, %{owner: ^owner, status: :claimed}} <- sessions, do: id
101
    end
97
  @doc "A linearizable query descriptor returning all session IDs owned by `node`."
98
  def owned_by(owner), do: {__MODULE__, :owned_by_query, [owner]}
99
100
  @doc false
101
  def lookup_query(id, %{sessions: sessions}), do: Map.get(sessions, id)
102
103
  @doc false
104
  def owned_by_query(owner, %{sessions: sessions}) do
105
    for {id, %{owner: ^owner, status: :claimed}} <- sessions, do: id
102 106
  end
103 107
104 108
  # ── internal ───────────────────────────────────────────────────────────────
test/openagents/cluster/session_registry_property_test.exs modified +4 -1

@@ -34,7 +34,7 @@ defmodule OpenAgents.Cluster.SessionRegistryPropertyTest do

34 34
               "command #{inspect(command)} on #{inspect(s)} => #{inspect(reply)}, expected #{inspect(expected)}"
35 35
36 36
        # The committed generation in the machine never goes backwards.
37
        if entry = Reg.lookup(id).(new_machine) do
37
        if entry = query(Reg.lookup(id), new_machine) do
38 38
          assert entry.generation >= s.gen
39 39
        end
40 40

@@ -75,4 +75,7 @@ defmodule OpenAgents.Cluster.SessionRegistryPropertyTest do

75 75
  # Before any claim (gen 0) the session has no entry, so the fence replies nil.
76 76
  defp fence_gen(0), do: nil
77 77
  defp fence_gen(gen), do: gen
78
79
  defp query({module, function, arguments}, state),
80
    do: Kernel.apply(module, function, arguments ++ [state])
78 81
end
test/openagents/cluster/session_registry_test.exs modified +12 -9

@@ -10,11 +10,14 @@ defmodule OpenAgents.Cluster.SessionRegistryTest do

10 10
11 11
  defp run(cmd, state), do: Kernel.apply(Reg, :apply, [%{}, cmd, state])
12 12
13
  defp query({module, function, arguments}, state),
14
    do: Kernel.apply(module, function, arguments ++ [state])
15
13 16
  test "claim bumps the generation monotonically and records the owner" do
14 17
    s0 = Reg.init(%{})
15 18
    assert {s1, {:ok, 1}} = run({:claim, "j1", :delegation, :a@h}, s0)
16 19
17
    assert Reg.lookup("j1").(s1) == %{
20
    assert query(Reg.lookup("j1"), s1) == %{
18 21
             kind: :delegation,
19 22
             generation: 1,
20 23
             owner: :a@h,

@@ -24,8 +27,8 @@ defmodule OpenAgents.Cluster.SessionRegistryTest do

24 27
25 28
    # A re-claim (handoff to another node) bumps the generation and re-owns.
26 29
    assert {s2, {:ok, 2}} = run({:claim, "j1", :delegation, :b@h}, s1)
27
    assert Reg.lookup("j1").(s2).owner == :b@h
28
    assert Reg.lookup("j1").(s2).generation == 2
30
    assert query(Reg.lookup("j1"), s2).owner == :b@h
31
    assert query(Reg.lookup("j1"), s2).generation == 2
29 32
  end
30 33
31 34
  test "checkpoint is accepted for the current generation and fenced for a stale one" do

@@ -35,12 +38,12 @@ defmodule OpenAgents.Cluster.SessionRegistryTest do

35 38
36 39
    # The new owner (gen 2) checkpoints successfully.
37 40
    assert {s3, :ok} = run({:checkpoint, "j1", 2, %{step: 5}}, s2)
38
    assert Reg.lookup("j1").(s3).checkpoint == %{step: 5}
41
    assert query(Reg.lookup("j1"), s3).checkpoint == %{step: 5}
39 42
40 43
    # The superseded zombie (gen 1) is fenced — its checkpoint is rejected and
41 44
    # the committed state is untouched.
42 45
    assert {^s3, {:fenced, 2}} = run({:checkpoint, "j1", 1, %{step: 99}}, s3)
43
    assert Reg.lookup("j1").(s3).checkpoint == %{step: 5}
46
    assert query(Reg.lookup("j1"), s3).checkpoint == %{step: 5}
44 47
  end
45 48
46 49
  test "finish is fenced for a stale generation; a zombie cannot terminal-commit" do

@@ -50,11 +53,11 @@ defmodule OpenAgents.Cluster.SessionRegistryTest do

50 53
51 54
    # Zombie (gen 1) tries to finish — fenced, state unchanged.
52 55
    assert {^s2, {:fenced, 2}} = run({:finish, "j1", 1}, s2)
53
    assert Reg.lookup("j1").(s2).status == :claimed
56
    assert query(Reg.lookup("j1"), s2).status == :claimed
54 57
55 58
    # Live owner (gen 2) finishes.
56 59
    assert {s3, :ok} = run({:finish, "j1", 2}, s2)
57
    assert Reg.lookup("j1").(s3).status == :terminal
60
    assert query(Reg.lookup("j1"), s3).status == :terminal
58 61
  end
59 62
60 63
  test "a terminal session is never re-claimed" do

@@ -72,7 +75,7 @@ defmodule OpenAgents.Cluster.SessionRegistryTest do

72 75
    {s3, _} = run({:claim, "j3", :job, :b@h}, s2)
73 76
    {s4, :ok} = run({:finish, "j1", 1}, s3)
74 77
75
    assert Enum.sort(Reg.owned_by(:a@h).(s4)) == ["j2"]
76
    assert Reg.owned_by(:b@h).(s4) == ["j3"]
78
    assert Enum.sort(query(Reg.owned_by(:a@h), s4)) == ["j2"]
79
    assert query(Reg.owned_by(:b@h), s4) == ["j3"]
77 80
  end
78 81
end

This page updates live while a promote is in flight · changelog