Skip to repository content

tenant.openagents/omega

No repository description is available.

OpenAgents Git authority 2026-07-28T00:58:26.713Z Public web read
NIP-34 coordinate30617:7649603503856e5148d571eac2766b288a8ff1e9e35d380337a1d2b0015b4f92:omega
MaintainersHidden in public view
References2 branches · 1 tag
Read-only clonegit clone https://openagents.com/git/tenant.openagents/omega.git

Revision diff

83862c2a 9660afa6
diff --git a/Cargo.lock b/Cargo.lock
index 7fa5bd2ea1..c06cd14861 100644
--- a/Cargo.lock
+++ b/Cargo.lock
@@ -6608,6 +6608,7 @@ dependencies = [
66086608  "editor",
66096609  "futures 0.3.32",
66106610  "gpui",
6611+ "log",
66116612  "nostr",
66126613  "omega_effectd",
66136614  "omega_front_door",
diff --git a/crates/full_auto_ui/Cargo.toml b/crates/full_auto_ui/Cargo.toml
index 0178f0fb1c..d5cd5f7607 100644
--- a/crates/full_auto_ui/Cargo.toml
+++ b/crates/full_auto_ui/Cargo.toml
@@ -18,6 +18,7 @@ anyhow.workspace = true
1818 editor.workspace = true
1919 futures.workspace = true
2020 gpui.workspace = true
21+log.workspace = true
2122 omega_effectd.workspace = true
2223 omega_front_door.workspace = true
2324 project.workspace = true
diff --git a/crates/full_auto_ui/fixtures/observation_stub_effectd.mjs b/crates/full_auto_ui/fixtures/observation_stub_effectd.mjs
new file mode 100644
index 0000000000..10ada926bd
--- /dev/null
+++ b/crates/full_auto_ui/fixtures/observation_stub_effectd.mjs
@@ -0,0 +1,118 @@
1+// A daemon stub for testing `issue31_observation` (omega#97).
2+//
3+// This exists to drive the OBSERVER's own failure handling, which a real
4+// `omega-effectd` will not do on command: there is no way to ask a healthy
5+// daemon to list a run and then refuse to describe it.
6+//
7+// It is a test double for our error paths, never host authority. Nothing it
8+// emits is published to a relay by any test, and the omega#97 live proof
9+// (`a_running_daemon_supplies_the_reading_a_paired_device_reads_on_a_live_relay`)
10+// runs against the packaged `omega-effectd` instead — because putting recorded
11+// or scripted daemon output on the wire in place of live host state is exactly
12+// the substitution omega#49's exit forbids.
13+//
14+// The scenario lives in the data root so parallel tests cannot collide through
15+// a shared environment variable.
16+
17+import { readFileSync } from "node:fs"
18+import path from "node:path"
19+import readline from "node:readline"
20+
21+const SCHEMA = "openagents.omega.effectd.v1"
22+const dataRoot = process.env.OPENAGENTS_OMEGA_EFFECTD_DATA_ROOT
23+if (!dataRoot) {
24+  process.stderr.write("OPENAGENTS_OMEGA_EFFECTD_DATA_ROOT is required\n")
25+  process.exit(1)
26+}
27+
28+const scenario = JSON.parse(readFileSync(path.join(dataRoot, "scenario.json"), "utf8"))
29+const runs = scenario.runs ?? []
30+const details = scenario.details ?? {}
31+const reports = scenario.reports ?? {}
32+const receipts = scenario.receipts ?? {}
33+const refuseGetRun = new Set(scenario.refuseGetRun ?? [])
34+const refuseListRuns = scenario.refuseListRuns === true
35+const refuseCapacity = scenario.refuseCapacity === true
36+
37+const respond = (id, ok, result, error) => {
38+  const frame = { schema: SCHEMA, kind: "response", id, generation: 1, ok }
39+  if (result !== undefined) frame.result = result
40+  if (error !== undefined) frame.error = error
41+  process.stdout.write(`${JSON.stringify(frame)}\n`)
42+}
43+
44+const refusal = (message) => ({ code: "internal", message })
45+
46+readline.createInterface({ input: process.stdin }).on("line", (line) => {
47+  if (!line.trim()) return
48+  let request
49+  try {
50+    request = JSON.parse(line)
51+  } catch {
52+    return
53+  }
54+  if (request.kind !== "request") return
55+  const { id, method, params } = request
56+
57+  switch (method) {
58+    case "initialize":
59+      respond(id, true, {
60+        schema: SCHEMA,
61+        protocolVersion: 1,
62+        serviceVersion: "0.0.0-observation-stub",
63+        generation: 1,
64+        capabilities: ["list_runs", "get_run", "get_capacity", "get_report", "get_receipt"],
65+        dataRoot,
66+        activeRunLimit: 8,
67+      })
68+      return
69+    case "list_runs":
70+      if (refuseListRuns) {
71+        respond(id, false, undefined, refusal("The stub was told to refuse list_runs."))
72+        return
73+      }
74+      respond(id, true, { runs })
75+      return
76+    case "get_capacity":
77+      if (refuseCapacity) {
78+        respond(id, false, undefined, refusal("The stub was told to refuse get_capacity."))
79+        return
80+      }
81+      respond(id, true, scenario.capacity ?? { activeRunLimit: 8, activeRunCount: 0, lanes: [] })
82+      return
83+    case "get_run": {
84+      const runRef = params?.runRef
85+      if (refuseGetRun.has(runRef)) {
86+        respond(id, false, undefined, refusal(`The stub was told to refuse get_run for ${runRef}.`))
87+        return
88+      }
89+      const run = details[runRef]
90+      if (!run) {
91+        respond(id, false, undefined, { code: "run_not_found", message: `No run ${runRef}.` })
92+        return
93+      }
94+      respond(id, true, { run })
95+      return
96+    }
97+    case "get_report": {
98+      const report = reports[params?.runRef]
99+      if (!report) {
100+        respond(id, false, undefined, { code: "run_not_found", message: "No report." })
101+        return
102+      }
103+      respond(id, true, { report })
104+      return
105+    }
106+    case "get_receipt": {
107+      const receipt = receipts[params?.runRef]
108+      if (!receipt) {
109+        respond(id, false, undefined, { code: "run_not_found", message: "No receipt." })
110+        return
111+      }
112+      respond(id, true, { receipt })
113+      return
114+    }
115+    default:
116+      respond(id, false, undefined, { code: "unsupported", message: `Unsupported ${method}.` })
117+  }
118+})
diff --git a/crates/full_auto_ui/src/full_auto_ui.rs b/crates/full_auto_ui/src/full_auto_ui.rs
index 3be14e7e7b..9c2014cce6 100644
--- a/crates/full_auto_ui/src/full_auto_ui.rs
+++ b/crates/full_auto_ui/src/full_auto_ui.rs
@@ -8,6 +8,7 @@ mod draft;
88 mod evidence_chain;
99 mod issue31_adjunct;
1010 mod issue31_delivery;
11+mod issue31_observation;
1112 mod panel;
1213 mod provider_roster;
1314 mod thread_run_link;
@@ -28,6 +29,9 @@ pub use issue31_delivery::{
2829     issue31_provider_roster_source,
2930     latest_issue31_live_reading, set_issue31_live_reading, Issue31FullAutoReading,
3031 };
32+pub use issue31_observation::{
33+    observe_issue31_full_auto, Issue31ObservationError, MAX_ISSUE31_PROJECTED_RUNS,
34+};
3135 pub use panel::FullAutoPanel;
3236 pub use provider_roster::{parse_provider_accounts, ProviderAccountRow};
3337 pub use thread_run_link::{
diff --git a/crates/full_auto_ui/src/issue31_delivery.rs b/crates/full_auto_ui/src/issue31_delivery.rs
index 3b30b71e55..5514e33d16 100644
--- a/crates/full_auto_ui/src/issue31_delivery.rs
+++ b/crates/full_auto_ui/src/issue31_delivery.rs
@@ -35,18 +35,37 @@ use crate::issue31_adjunct::{
3535 };
3636 use crate::provider_roster::parse_provider_accounts;
3737 
38+/// This host's clock, in epoch milliseconds. The single reading every
39+/// observation is stamped from.
40+fn unix_millis() -> u64 {
41+    std::time::SystemTime::now()
42+        .duration_since(std::time::UNIX_EPOCH)
43+        .map(|elapsed| u64::try_from(elapsed.as_millis()).unwrap_or(u64::MAX))
44+        .unwrap_or_default()
45+}
46+
3847 /// One complete reading of the live Full Auto surface, owned rather than
3948 /// borrowed so it can be cached between the panel that reads it and the pump
4049 /// that publishes it.
4150 ///
4251 /// `host_ref` is absent on purpose: the host reference belongs to the device's
4352 /// grant, not to the run registry, so it is supplied per delivery.
44-#[derive(Clone, Debug, Default)]
53+///
54+/// `Default` is deliberately not derived. A default reading is one nobody
55+/// measured, and it would carry `generated_at_ms: 0` — a stamp that decodes,
56+/// projects, and reads on the phone as a host observation taken in 1970. The
57+/// only readings that exist are ones a daemon answered.
58+#[derive(Clone, Debug)]
4559 pub struct Issue31FullAutoReading {
46-    /// When the daemon was actually read, in epoch milliseconds. Never "now":
47-    /// a snapshot stamped with the publish time would claim a freshness the
48-    /// host does not have.
49-    pub generated_at_ms: u64,
60+    /// When the daemon was actually read, in epoch milliseconds.
61+    ///
62+    /// Private, and there is no production path that sets it (omega#97). The
63+    /// host's own clock stamps it once inside `observed`, so a caller cannot
64+    /// supply one: a reading whose stamp its author chose is exactly how a
65+    /// recorded fixture becomes host authority, and omega#49's exit forbids
66+    /// that in as many words. Making it unwritable is stronger than forbidding
67+    /// it in a comment, because a comment does not survive a rebase.
68+    generated_at_ms: u64,
5069     pub host_generation: u64,
5170     /// One `get_run` record per run the daemon listed.
5271     pub run_details: Vec<Value>,
@@ -57,6 +76,62 @@ pub struct Issue31FullAutoReading {
5776 }
5877 
5978 impl Issue31FullAutoReading {
79+    /// The only constructor a production path can reach.
80+    ///
81+    /// `generated_at_ms` is one reading of this host's clock, taken here and
82+    /// stamped once. There is no parameter for it, so a client-supplied,
83+    /// fixture-supplied, or replayed value cannot enter — it is discarded by
84+    /// being inexpressible rather than by being checked.
85+    ///
86+    /// Taken *after* the daemon answered, on purpose. Stamping before the reads
87+    /// would put the stamp behind a run that began during them, and the
88+    /// contract refuses a run whose start is newer than the snapshot; stamping
89+    /// after can only over-state freshness by the length of one poll, which
90+    /// under-states no run's unattended duration.
91+    pub fn observed(
92+        host_generation: u64,
93+        run_details: Vec<Value>,
94+        capacity: Value,
95+        evidence: Vec<(Value, Value)>,
96+    ) -> Self {
97+        Self {
98+            generated_at_ms: unix_millis(),
99+            host_generation,
100+            run_details,
101+            capacity,
102+            evidence,
103+        }
104+    }
105+
106+    /// When this host read its daemon.
107+    pub fn generated_at_ms(&self) -> u64 {
108+        self.generated_at_ms
109+    }
110+
111+    /// A reading stamped at a recorded instant, for tests over captured daemon
112+    /// bytes only.
113+    ///
114+    /// `#[cfg(test)]` is the whole point: the projection has to be testable
115+    /// against real recorded `get_run` output at a known instant, and no
116+    /// shipped build may contain a way to state a stamp the host did not
117+    /// measure. This function does not exist in a release binary.
118+    #[cfg(test)]
119+    fn at_recorded_instant(
120+        generated_at_ms: u64,
121+        host_generation: u64,
122+        run_details: Vec<Value>,
123+        capacity: Value,
124+        evidence: Vec<(Value, Value)>,
125+    ) -> Self {
126+        Self {
127+            generated_at_ms,
128+            host_generation,
129+            run_details,
130+            capacity,
131+            evidence,
132+        }
133+    }
134+
60135     /// A stable identifier for exactly this reading.
61136     ///
62137     /// The mobile contract binds the detail projection to the snapshot that
@@ -101,12 +176,12 @@ pub fn issue31_host_projection_documents(
101176     // against the pump's own clock. A handoff opened after this reading was
102177     // taken belongs to the next snapshot, and the contract says so: a row whose
103178     // request time is newer than `generatedAtMs` is refused (omega#91).
104-    let handoffs = request.handoffs.projected(reading.generated_at_ms).rows;
179+    let handoffs = request.handoffs.projected(reading.generated_at_ms()).rows;
105180     let snapshot_ref = reading.snapshot_ref(&handoffs);
106181     let sources = Issue31FullAutoLiveSources {
107182         host_ref: request.host_ref,
108183         snapshot_ref: &snapshot_ref,
109-        generated_at_ms: reading.generated_at_ms,
184+        generated_at_ms: reading.generated_at_ms(),
110185         host_generation: reading.host_generation,
111186         run_details: &reading.run_details,
112187         capacity: &reading.capacity,
@@ -118,7 +193,7 @@ pub fn issue31_host_projection_documents(
118193     // contract's way of saying "this host cannot presently vouch for you".
119194     let identity = Issue31HostIdentitySource {
120195         source_ref: "source.omega.issue31-pairing",
121-        observed_at_ms: reading.generated_at_ms,
196+        observed_at_ms: reading.generated_at_ms(),
122197         owner_grant_ref: Some(request.grant_ref),
123198         record_refs: &["record.omega.host-announcement", "record.omega.owner-grant"],
124199         permitted_action_refs: &[
@@ -258,13 +333,13 @@ mod tests {
258333     }
259334 
260335     fn reading() -> Issue31FullAutoReading {
261-        Issue31FullAutoReading {
262-            generated_at_ms: LIVE_GENERATED_AT_MS,
263-            host_generation: 19,
264-            run_details: vec![live("get_run", LIVE_RUN)],
265-            capacity: live("get_capacity", LIVE_CAPACITY),
266-            evidence: Vec::new(),
267-        }
336+        Issue31FullAutoReading::at_recorded_instant(
337+            LIVE_GENERATED_AT_MS,
338+            19,
339+            vec![live("get_run", LIVE_RUN)],
340+            live("get_capacity", LIVE_CAPACITY),
341+            Vec::new(),
342+        )
268343     }
269344 
270345     #[test]
@@ -532,13 +607,13 @@ mod tests {
532607             omega_effectd::SarahConversationClient::with_relay(config, Box::new(relay), signer);
533608         client.attach_issue31_host_controller(controller);
534609         // The host's real, currently-empty reading of its own Full Auto state.
535-        set_issue31_live_reading(Issue31FullAutoReading {
536-            generated_at_ms: unix_seconds().saturating_mul(1_000),
537-            host_generation: 1,
538-            run_details: Vec::new(),
539-            capacity: json!({ "accounts": [] }),
540-            evidence: Vec::new(),
541-        });
610+        set_issue31_live_reading(Issue31FullAutoReading::at_recorded_instant(
611+            unix_seconds().saturating_mul(1_000),
612+            1,
613+            Vec::new(),
614+            json!({ "accounts": [] }),
615+            Vec::new(),
616+        ));
542617         client.set_issue31_host_projection_source(issue31_host_projection_source());
543618 
544619         client
@@ -653,13 +728,13 @@ mod tests {
653728         // the snapshot's `generatedAtMs` is when the host last looked, and a
654729         // handoff stamped after that reading belongs to the next snapshot.
655730         let observe = || {
656-            set_issue31_live_reading(Issue31FullAutoReading {
657-                generated_at_ms: unix_seconds().saturating_mul(1_000) + 1_000,
658-                host_generation: 1,
659-                run_details: Vec::new(),
660-                capacity: capacity_with_accounts(),
661-                evidence: Vec::new(),
662-            });
731+            set_issue31_live_reading(Issue31FullAutoReading::at_recorded_instant(
732+                unix_seconds().saturating_mul(1_000) + 1_000,
733+                1,
734+                Vec::new(),
735+                capacity_with_accounts(),
736+                Vec::new(),
737+            ));
663738         };
664739         observe();
665740         client.set_issue31_host_projection_source(issue31_host_projection_source());
@@ -929,11 +1004,11 @@ mod tests {
9291004         // is what makes the terminal outcome a refusal rather than a
9301005         // connection.
9311006         let observe = || {
932-            set_issue31_live_reading(Issue31FullAutoReading {
933-                generated_at_ms: unix_seconds().saturating_mul(1_000) + 1_000,
934-                host_generation: 1,
935-                run_details: Vec::new(),
936-                capacity: json!({
1007+            set_issue31_live_reading(Issue31FullAutoReading::at_recorded_instant(
1008+                unix_seconds().saturating_mul(1_000) + 1_000,
1009+                1,
1010+                Vec::new(),
1011+                json!({
9371012                     "lanes": [{"lane": "claude-local", "state": "available", "activeRuns": 0}],
9381013                     "accounts": [{
9391014                         "accountRef": "account.claude.1",
@@ -944,8 +1019,8 @@ mod tests {
9441019                         "lane": "lane.claude-local",
9451020                     }],
9461021                 }),
947-                evidence: Vec::new(),
948-            });
1022+                Vec::new(),
1023+            ));
9491024         };
9501025         observe();
9511026         client.set_issue31_host_projection_source(issue31_host_projection_source());
@@ -1241,13 +1316,13 @@ mod tests {
12411316 
12421317     #[test]
12431318     fn a_host_running_nothing_publishes_an_empty_view_rather_than_silence() {
1244-        let empty = Issue31FullAutoReading {
1245-            generated_at_ms: LIVE_GENERATED_AT_MS,
1246-            host_generation: 19,
1247-            run_details: Vec::new(),
1248-            capacity: json!({ "accounts": [] }),
1249-            evidence: Vec::new(),
1250-        };
1319+        let empty = Issue31FullAutoReading::at_recorded_instant(
1320+            LIVE_GENERATED_AT_MS,
1321+            19,
1322+            Vec::new(),
1323+            json!({ "accounts": [] }),
1324+            Vec::new(),
1325+        );
12511326         let documents = issue31_host_projection_documents(
12521327             &empty,
12531328             &request("omega.host.local", "grant.omega.device_1", &no_handoffs()),
@@ -1262,4 +1337,268 @@ mod tests {
12621337             Some("omega.host.local"),
12631338         );
12641339     }
1340+
1341+    /// The omega#97 exit, against a REAL running daemon and the DEPLOYED relay.
1342+    ///
1343+    /// omega#49 and omega#91 both closed carrying the same named substitution:
1344+    ///
1345+    /// > the Full Auto reading. No `omega-effectd` daemon is attached to this
1346+    /// > process, so the host's roster reading is supplied here rather than
1347+    /// > polled.
1348+    ///
1349+    /// This test is that substitution being removed. It spawns the packaged
1350+    /// `omega-effectd` under a data root of its own, reads it through the same
1351+    /// `observe_issue31_full_auto` the desktop panel calls, hands the reading to
1352+    /// the shipped projection source, and lets the shipped `sync_issue31_host`
1353+    /// pump publish it to a paired device over a real relay.
1354+    ///
1355+    /// Nothing recorded is on the wire. `fixtures/live-omega-effectd.get_run.json`
1356+    /// is not read here — replaying it would make a recorded fixture the host
1357+    /// authority for a device proof, which omega#49's exit forbids in as many
1358+    /// words, and which the private stamp on `Issue31FullAutoReading` now makes
1359+    /// unexpressible from a production path rather than merely discouraged.
1360+    ///
1361+    /// ## What this run does NOT start
1362+    ///
1363+    /// It never calls `start`, `pause`, `resume`, `retry`, or `stop`. Full Auto
1364+    /// authority does not begin on a path a model can reach, so a freshly
1365+    /// spawned daemon holds no runs and this asserts a host that looked and
1366+    /// found none — which is a real observation, and on the wire is a different
1367+    /// document from a host that never looked. What a real *run* projects is
1368+    /// covered from captured daemon bytes by
1369+    /// `a_live_host_run_projects_its_exact_unattended_duration`; what this adds
1370+    /// is that the bytes reaching the phone came from a daemon that answered.
1371+    ///
1372+    /// ## Named substitutions
1373+    ///
1374+    /// - identity custody. The owner key is a keypair rather than
1375+    ///   `omega_identity::IdentityService`, because custody needs the GPUI app
1376+    ///   and this harness runs headless. A run proves the host protocol and the
1377+    ///   relay, not owner key custody.
1378+    /// - `lane_readiness`. The shipped answer lives in `agent_ui` and needs a
1379+    ///   GPUI workspace. This process genuinely holds no workspace and no
1380+    ///   admitted agent authority, so it answers `unavailable` for every lane —
1381+    ///   a true statement about this host, and one that can only under-claim
1382+    ///   its capacity.
1383+    ///
1384+    /// ```sh
1385+    /// OMEGA_LIVE_RELAY_URL=wss://relay.openagents.com \
1386+    /// OMEGA_EFFECTD_BIN=/Applications/Omega.app/Contents/Resources/omega-effectd/bin/omega-effectd \
1387+    ///   cargo test -p full_auto_ui --lib \
1388+    ///   a_running_daemon_supplies_the_reading_a_paired_device_reads_on_a_live_relay \
1389+    ///   -- --ignored --nocapture
1390+    /// ```
1391+    #[test]
1392+    #[ignore = "requires a live relay and a packaged omega-effectd; set OMEGA_LIVE_RELAY_URL and OMEGA_EFFECTD_BIN"]
1393+    fn a_running_daemon_supplies_the_reading_a_paired_device_reads_on_a_live_relay() {
1394+        let Ok(relay_url) = std::env::var("OMEGA_LIVE_RELAY_URL") else {
1395+            eprintln!("OMEGA_LIVE_RELAY_URL unset; skipping");
1396+            return;
1397+        };
1398+        let Ok(effectd_bin) = std::env::var("OMEGA_EFFECTD_BIN") else {
1399+            eprintln!("OMEGA_EFFECTD_BIN unset; skipping");
1400+            return;
1401+        };
1402+        let daemon = std::path::PathBuf::from(&effectd_bin);
1403+        assert!(
1404+            daemon.is_file(),
1405+            "OMEGA_EFFECTD_BIN must name the packaged omega-effectd executable: {effectd_bin}",
1406+        );
1407+
1408+        // Its own data root. The owner's running Omega keeps its runs under
1409+        // `paths::data_dir()`, and a second writer there would be a second
1410+        // durable run authority over the same registry.
1411+        let temporary = tempfile::tempdir().expect("tempdir");
1412+        let data_root = temporary.path().join("effectd");
1413+        let supervisor = std::rc::Rc::new(smol::lock::Mutex::new(
1414+            omega_effectd::OmegaEffectdSupervisor::new(omega_effectd::default_options(
1415+                data_root.clone(),
1416+                omega_effectd::OmegaEffectdCommand {
1417+                    program: daemon,
1418+                    args: Vec::new(),
1419+                },
1420+            )),
1421+        ));
1422+        {
1423+            // See the named substitution above: no workspace, no admitted agent
1424+            // authority, so every lane is honestly unavailable.
1425+            let mut guard = smol::block_on(supervisor.lock());
1426+            guard.set_host_handler(std::rc::Rc::new(|request: omega_effectd::HostRequestFrame| {
1427+                Box::pin(async move {
1428+                    match request.method {
1429+                        omega_effectd::HostMethod::LaneReadiness => Ok(json!({
1430+                            "known": false,
1431+                            "admitted": false,
1432+                            "fullAuto": false,
1433+                            "state": "unavailable",
1434+                        })),
1435+                        _ => Err(omega_effectd::HostResponseError::unavailable(
1436+                            "This headless host answers only lane_readiness.",
1437+                        )),
1438+                    }
1439+                }) as omega_effectd::OmegaEffectdHostFuture
1440+            }));
1441+        }
1442+
1443+        let initialized = smol::block_on(async {
1444+            let mut guard = supervisor.lock().await;
1445+            guard.start().await
1446+        })
1447+        .expect("the packaged omega-effectd must start");
1448+        eprintln!(
1449+            "omega#97: omega-effectd {} answered initialize at generation {} under {}",
1450+            initialized.service_version,
1451+            initialized.generation,
1452+            data_root.display(),
1453+        );
1454+
1455+        // The reading. Measured, through the exact function `panel.rs` calls.
1456+        let before_ms = unix_seconds().saturating_mul(1_000);
1457+        let reading = smol::block_on(crate::issue31_observation::observe_issue31_full_auto(
1458+            &supervisor,
1459+        ))
1460+        .expect("a running daemon must let this host state a reading");
1461+        let after_ms = unix_seconds().saturating_mul(1_000) + 1_000;
1462+
1463+        // Provenance: the stamp is this host's clock at the moment it read the
1464+        // daemon, not a value anything handed it. A replayed fixture cannot
1465+        // land in this window, and there is no parameter through which one
1466+        // could try.
1467+        assert!(
1468+            reading.generated_at_ms() >= before_ms && reading.generated_at_ms() <= after_ms,
1469+            "the reading must be stamped by the host that took it: \
1470+             {before_ms} <= {} <= {after_ms}",
1471+            reading.generated_at_ms(),
1472+        );
1473+        // The daemon's own capacity record, not a constructed one. Its lane
1474+        // list is the thing `parse_provider_accounts` reads the account-to-lane
1475+        // mapping out of, so this is where the roster comes from.
1476+        let lanes = reading
1477+            .capacity
1478+            .get("lanes")
1479+            .and_then(Value::as_array)
1480+            .expect("a live get_capacity carries the host's lanes");
1481+        assert!(
1482+            !lanes.is_empty(),
1483+            "the daemon answered get_capacity with no lanes at all: {}",
1484+            reading.capacity,
1485+        );
1486+        eprintln!(
1487+            "omega#97: measured reading · generation {} · {} run(s) · {} lane(s) · {} account(s) · stamped {}",
1488+            reading.host_generation,
1489+            reading.run_details.len(),
1490+            lanes.len(),
1491+            parse_provider_accounts(&reading.capacity).len(),
1492+            reading.generated_at_ms(),
1493+        );
1494+
1495+        // Publish it, through the shipped pump, to a real paired device.
1496+        let host_keys = nostr::Keys::generate();
1497+        let sarah_keys = nostr::Keys::generate();
1498+        let device_public_key_hex = nostr::Keys::generate().public_key().to_hex();
1499+        let signer = omega_effectd::SigningIdentity::from_keys(host_keys.clone());
1500+        let owner_public_key_hex = signer.public_key_hex.clone();
1501+
1502+        let mut config = omega_effectd::SarahConversationConfig::mock_fixture();
1503+        config.identity.owner_public_key_hex = owner_public_key_hex.clone();
1504+        config.identity.sarah_public_key_hex = sarah_keys.public_key().to_hex();
1505+        config.conversation_digest = owner_public_key_hex[..24].to_string();
1506+        config.relay_url = Some(relay_url.clone());
1507+        let conversation_ref = config.conversation_ref();
1508+
1509+        let relay = omega_effectd::WebSocketRelayAdapter::new_for_keys_with_policy(
1510+            vec![relay_url.clone()],
1511+            host_keys,
1512+            sarah_keys.public_key().to_hex(),
1513+            Vec::new(),
1514+            Vec::new(),
1515+        )
1516+        .expect("host relay adapter");
1517+        let controller = live_paired_controller(
1518+            &owner_public_key_hex,
1519+            &sarah_keys.public_key().to_hex(),
1520+            &conversation_ref,
1521+            &relay_url,
1522+            &device_public_key_hex,
1523+        );
1524+        let grant = controller
1525+            .active_grants(unix_seconds())
1526+            .expect("grants")
1527+            .first()
1528+            .cloned()
1529+            .expect("the paired device holds an active grant");
1530+
1531+        let mut client =
1532+            omega_effectd::SarahConversationClient::with_relay(config, Box::new(relay), signer);
1533+        client.attach_issue31_host_controller(controller);
1534+        set_issue31_live_reading(reading.clone());
1535+        client.set_issue31_host_projection_source(issue31_host_projection_source());
1536+        client.set_issue31_provider_roster_source(issue31_provider_roster_source());
1537+
1538+        client
1539+            .sync_issue31_host()
1540+            .expect("the shipped host pump runs against the live relay");
1541+
1542+        assert!(
1543+            client
1544+                .issue31_published_host_adjunct_grants()
1545+                .contains(&format!("{}:{}", grant.grant_ref, grant.generation)),
1546+            "the pump must record the omega#47 publication it made for this grant",
1547+        );
1548+        // The outbox drains only when every configured relay acknowledged every
1549+        // gift wrap, so an empty backlog is the live relay's own receipt.
1550+        assert!(
1551+            client.issue31_pending_private_publish_refs().is_empty(),
1552+            "the live relay must acknowledge every owner-private record: {:?}",
1553+            client.issue31_pending_private_publish_refs(),
1554+        );
1555+
1556+        // And the document the phone decodes carries the daemon's stamp — the
1557+        // proof that what crossed the relay is what the daemon said, rather
1558+        // than anything this test could have authored.
1559+        let documents = issue31_host_projection_documents(
1560+            &reading,
1561+            &Issue31HostProjectionRequest {
1562+                host_ref: "omega.host.local",
1563+                host_public_key_hex: &owner_public_key_hex,
1564+                device_public_key_hex: &device_public_key_hex,
1565+                grant_ref: &grant.grant_ref,
1566+                expected_generation: grant.generation,
1567+                observed_at_ms: reading.generated_at_ms(),
1568+                handoffs: &no_handoffs(),
1569+            },
1570+        )
1571+        .expect("a measured reading projects");
1572+        assert_eq!(
1573+            documents
1574+                .detail
1575+                .get("generatedAtMs")
1576+                .and_then(Value::as_u64),
1577+            Some(reading.generated_at_ms()),
1578+            "the detail the phone reads must carry the instant the daemon was read",
1579+        );
1580+        let decoded = workroom_receipts::decode_issue31_full_auto_adjunct(
1581+            &serde_json::to_string(&documents.detail).expect("serialize detail"),
1582+        )
1583+        .expect("the emitter must not produce a detail the phone would refuse");
1584+        assert_eq!(
1585+            decoded.runs.len(),
1586+            reading.run_details.len(),
1587+            "the phone must read exactly the runs the daemon reported",
1588+        );
1589+
1590+        eprintln!(
1591+            "omega#97 live relay OK: {relay_url} stored the omega#47 snapshot and detail for \
1592+             grant {} addressed to device {device_public_key_hex}, built from a reading this \
1593+             host measured from a running omega-effectd ({} run(s), {} lane(s))",
1594+            grant.grant_ref,
1595+            reading.run_details.len(),
1596+            lanes.len(),
1597+        );
1598+
1599+        smol::block_on(async {
1600+            let mut guard = supervisor.lock().await;
1601+            let _ = guard.stop().await;
1602+        });
1603+    }
12651604 }
diff --git a/crates/full_auto_ui/src/issue31_observation.rs b/crates/full_auto_ui/src/issue31_observation.rs
new file mode 100644
index 0000000000..a13ee60449
--- /dev/null
+++ b/crates/full_auto_ui/src/issue31_observation.rs
@@ -0,0 +1,372 @@
1+//! Read this host's own Full Auto state off a running `omega-effectd` (omega#97).
2+//!
3+//! omega#49 and omega#91 both shipped with the same named substitution:
4+//!
5+//! > the Full Auto reading. No `omega-effectd` daemon is attached to this
6+//! > process, so the host's roster reading is supplied here rather than polled.
7+//!
8+//! That substitution is the whole of omega#97. A phone paired to a host that is
9+//! genuinely running work still rendered `no host projection`, because nothing
10+//! outside the desktop Full Auto panel ever handed the pump a reading, and the
11+//! obvious way to fill the rows — replaying
12+//! `fixtures/live-omega-effectd.get_run.json` — would make a recorded fixture
13+//! the host authority for a device proof, which omega#49's exit forbids in as
14+//! many words.
15+//!
16+//! So the reading is taken here instead, from a daemon that answered.
17+//!
18+//! ## One reading path, not two
19+//!
20+//! The polling this module does was previously inlined in `panel.rs`, reachable
21+//! only from a GPUI view. That is the wrong owner twice over: a headless proof
22+//! cannot reach it, and a host publishes its Full Auto state because it is a
23+//! host, not because someone opened a panel. `panel.rs` now calls
24+//! `observe_issue31_full_auto` like everyone else, so the desktop and the phone
25+//! cannot end up looking at two different readings of one daemon.
26+//!
27+//! ## Refused rather than shortened
28+//!
29+//! If the daemon lists runs and then fails to describe one of them, this
30+//! returns an error and no reading at all. A reading that dropped the run would
31+//! be published as a shorter run list than the host actually has, and a run
32+//! that vanishes from a snapshot reads on the phone as a run that ended. The
33+//! host has no way to distinguish those after the fact, so the incomplete
34+//! reading is never built.
35+//!
36+//! Evidence is the one exception, and deliberately: a run with no report or
37+//! receipt yet is a run whose evidence chain has not been produced, which is an
38+//! observation rather than a failure. It contributes no pair and the reading
39+//! stands.
40+//!
41+//! ## What this module does not do
42+//!
43+//! It never starts, pauses, resumes, retries, or stops a run. Reading a run
44+//! registry is not the same act as directing one, and Full Auto authority does
45+//! not begin on a path a model can reach. Every method called here is one of
46+//! `list_runs`, `get_run`, `get_capacity`, `get_report`, `get_receipt`.
47+
48+use omega_effectd::{SharedOmegaEffectdSupervisor, SupervisorError};
49+use serde_json::Value;
50+
51+use crate::issue31_delivery::Issue31FullAutoReading;
52+
53+/// The omega#47 contract carries at most this many runs in one projection.
54+pub const MAX_ISSUE31_PROJECTED_RUNS: usize = 16;
55+
56+/// Why this host could not state a reading of its own Full Auto surface.
57+///
58+/// Every variant means "no reading", never "an empty reading". The distinction
59+/// omega#49 turns on is that a host which could not look is not a host that
60+/// looked and found nothing: the first publishes silence and the phone says
61+/// `no host projection`, the second publishes zero runs and the phone says the
62+/// host is running nothing.
63+#[derive(Debug)]
64+pub enum Issue31ObservationError {
65+    /// `list_runs` did not answer. The host does not know what it is running.
66+    RunsUnavailable(String),
67+    /// `get_capacity` did not answer. Without it there is no account-to-lane
68+    /// mapping, and a snapshot carrying runs but no roster would report this
69+    /// host as holding no provider accounts.
70+    CapacityUnavailable(String),
71+    /// A run the daemon listed could not be described. See the module note:
72+    /// this refuses the whole reading rather than publishing a shorter one.
73+    RunDetailUnavailable { run_ref: String, error: String },
74+}
75+
76+impl std::fmt::Display for Issue31ObservationError {
77+    fn fmt(&self, formatter: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
78+        match self {
79+            Self::RunsUnavailable(error) => {
80+                write!(formatter, "omega-effectd did not list its runs: {error}")
81+            }
82+            Self::CapacityUnavailable(error) => {
83+                write!(formatter, "omega-effectd did not report capacity: {error}")
84+            }
85+            Self::RunDetailUnavailable { run_ref, error } => write!(
86+                formatter,
87+                "omega-effectd listed run {run_ref} and then could not describe it: {error}",
88+            ),
89+        }
90+    }
91+}
92+
93+impl std::error::Error for Issue31ObservationError {}
94+
95+impl Issue31ObservationError {
96+    /// A stable token for logs and proof output, so a failed observation is
97+    /// reported as the specific thing that failed rather than as a sentence.
98+    pub fn token(&self) -> &'static str {
99+        match self {
100+            Self::RunsUnavailable(_) => "observation.omega.runs_unavailable",
101+            Self::CapacityUnavailable(_) => "observation.omega.capacity_unavailable",
102+            Self::RunDetailUnavailable { .. } => "observation.omega.run_detail_unavailable",
103+        }
104+    }
105+}
106+
107+fn describe(error: SupervisorError) -> String {
108+    error.to_string()
109+}
110+
111+/// Take one complete reading of this host's Full Auto surface.
112+///
113+/// The supervisor is the same one the desktop drives, so this is the daemon
114+/// the owner's Omega is actually running, answering the same five read
115+/// methods. Nothing here is recorded, replayed, or defaulted: if the daemon
116+/// does not answer, the host states no reading.
117+pub async fn observe_issue31_full_auto(
118+    supervisor: &SharedOmegaEffectdSupervisor,
119+) -> Result<Issue31FullAutoReading, Issue31ObservationError> {
120+    let listed = {
121+        let mut guard = supervisor.lock().await;
122+        guard
123+            .list_runs()
124+            .await
125+            .map_err(|error| Issue31ObservationError::RunsUnavailable(describe(error)))?
126+    };
127+    let capacity = {
128+        let mut guard = supervisor.lock().await;
129+        guard
130+            .get_capacity()
131+            .await
132+            .map_err(|error| Issue31ObservationError::CapacityUnavailable(describe(error)))?
133+    };
134+    let host_generation = {
135+        let guard = supervisor.lock().await;
136+        guard.generation()
137+    };
138+
139+    let mut run_details: Vec<Value> = Vec::new();
140+    let mut evidence: Vec<(Value, Value)> = Vec::new();
141+    for run in listed.iter().take(MAX_ISSUE31_PROJECTED_RUNS) {
142+        let mut guard = supervisor.lock().await;
143+        let detail = guard.get_run(&run.run_ref).await.map_err(|error| {
144+            Issue31ObservationError::RunDetailUnavailable {
145+                run_ref: run.run_ref.clone(),
146+                error: describe(error),
147+            }
148+        })?;
149+        run_details.push(detail);
150+        // A run whose evidence chain has not been produced yet contributes no
151+        // pair. Both halves or neither: a report without its receipt is half a
152+        // chain, and omega#43's chain is refused hop by hop rather than shown
153+        // with a hop missing.
154+        if let (Ok(report), Ok(receipt)) = (
155+            guard.get_report(&run.run_ref).await,
156+            guard.get_receipt(&run.run_ref).await,
157+        ) {
158+            evidence.push((report, receipt));
159+        }
160+    }
161+
162+    // Stamped inside the constructor, from one reading of this host's clock,
163+    // with no parameter for a caller to supply. See `Issue31FullAutoReading`.
164+    Ok(Issue31FullAutoReading::observed(
165+        host_generation,
166+        run_details,
167+        capacity,
168+        evidence,
169+    ))
170+}
171+
172+#[cfg(test)]
173+mod tests {
174+    use super::*;
175+
176+    use serde_json::json;
177+
178+    /// Stand up the observation stub against a data root of its own.
179+    ///
180+    /// The stub is a test double for this module's error handling, never host
181+    /// authority — see the header of `fixtures/observation_stub_effectd.mjs`.
182+    /// Nothing built here is published to a relay by any test.
183+    fn stub(scenario: serde_json::Value) -> (tempfile::TempDir, SharedOmegaEffectdSupervisor) {
184+        let temporary = tempfile::tempdir().expect("tempdir");
185+        let data_root = temporary.path().join("effectd");
186+        std::fs::create_dir_all(&data_root).expect("data root");
187+        std::fs::write(
188+            data_root.join("scenario.json"),
189+            serde_json::to_string(&scenario).expect("scenario"),
190+        )
191+        .expect("write scenario");
192+        let fixture = std::path::Path::new(env!("CARGO_MANIFEST_DIR"))
193+            .join("fixtures")
194+            .join("observation_stub_effectd.mjs");
195+        let supervisor = std::rc::Rc::new(smol::lock::Mutex::new(
196+            omega_effectd::OmegaEffectdSupervisor::new(omega_effectd::default_options(
197+                data_root,
198+                omega_effectd::fixture_command(&fixture),
199+            )),
200+        ));
201+        smol::block_on(async {
202+            let mut guard = supervisor.lock().await;
203+            guard.start().await
204+        })
205+        .expect("the observation stub must start");
206+        (temporary, supervisor)
207+    }
208+
209+    fn one_run_scenario(refuse_get_run: bool) -> serde_json::Value {
210+        json!({
211+            "runs": [{
212+                "runRef": "run.omega.1",
213+                "threadRef": null,
214+                "state": "running",
215+                "title": "Observed run",
216+                "updatedAt": "2026-07-26T12:00:00Z",
217+                "startedAtMs": 1_785_000_000_000u64,
218+            }],
219+            "details": {
220+                "run.omega.1": {
221+                    "runRef": "run.omega.1",
222+                    "state": "running",
223+                    "title": "Observed run",
224+                    "objective": "Prove the host measured this.",
225+                    "doneCondition": "The phone reads it.",
226+                    "lane": "codex-local",
227+                    "turnCap": 40,
228+                    "successfulAttempts": 1,
229+                    "failedAttempts": 0,
230+                    "recoveryAction": "none",
231+                    "startedAtMs": 1_785_000_000_000u64,
232+                    "updatedAt": "2026-07-26T12:00:00Z",
233+                    "turns": [],
234+                },
235+            },
236+            "capacity": {
237+                "activeRunLimit": 8,
238+                "activeRunCount": 1,
239+                "lanes": [{ "lane": "codex-local", "state": "available", "activeRuns": 1 }],
240+            },
241+            "refuseGetRun": if refuse_get_run { vec!["run.omega.1"] } else { Vec::<&str>::new() },
242+        })
243+    }
244+
245+    /// The happy path, against a daemon that answered.
246+    #[test]
247+    fn a_daemon_that_answers_yields_the_runs_it_reported() {
248+        let (_temporary, supervisor) = stub(one_run_scenario(false));
249+        let reading =
250+            smol::block_on(observe_issue31_full_auto(&supervisor)).expect("the daemon answered");
251+        assert_eq!(reading.run_details.len(), 1);
252+        assert_eq!(
253+            reading.run_details[0].get("runRef").and_then(|v| v.as_str()),
254+            Some("run.omega.1"),
255+        );
256+        assert_eq!(
257+            reading.capacity.get("activeRunCount").and_then(|v| v.as_u64()),
258+            Some(1),
259+            "the capacity record must be the daemon's own, not a constructed one",
260+        );
261+        // No report or receipt exists yet, so no evidence pair is invented.
262+        assert!(reading.evidence.is_empty());
263+    }
264+
265+    /// A run the daemon listed and then would not describe refuses the whole
266+    /// reading.
267+    ///
268+    /// Publishing the shorter list instead would drop a run the host is
269+    /// actually holding, and a run that vanishes from a snapshot reads on the
270+    /// phone as a run that ended. The host cannot tell those apart afterwards,
271+    /// so the incomplete reading is never built.
272+    #[test]
273+    fn a_run_the_daemon_will_not_describe_refuses_the_whole_reading() {
274+        let (_temporary, supervisor) = stub(one_run_scenario(true));
275+        let error = smol::block_on(observe_issue31_full_auto(&supervisor))
276+            .expect_err("an undescribable run must refuse the reading");
277+        assert_eq!(error.token(), "observation.omega.run_detail_unavailable");
278+        assert!(
279+            matches!(
280+                &error,
281+                Issue31ObservationError::RunDetailUnavailable { run_ref, .. }
282+                    if run_ref == "run.omega.1"
283+            ),
284+            "the refusal must name the run it could not describe: {error}",
285+        );
286+    }
287+
288+    /// A host that could not look is not a host that looked and found nothing.
289+    #[test]
290+    fn a_daemon_that_will_not_list_its_runs_yields_no_reading_rather_than_an_empty_one() {
291+        let (_temporary, supervisor) = stub(json!({ "refuseListRuns": true }));
292+        let error = smol::block_on(observe_issue31_full_auto(&supervisor))
293+            .expect_err("a host that could not look states no reading");
294+        assert_eq!(error.token(), "observation.omega.runs_unavailable");
295+    }
296+
297+    /// Capacity is the account-to-lane mapping's only source. A reading that
298+    /// carried runs and no capacity would report this host as holding no
299+    /// provider accounts, which is a claim rather than an observation.
300+    #[test]
301+    fn a_daemon_that_will_not_report_capacity_yields_no_reading() {
302+        let (_temporary, supervisor) = stub(json!({ "runs": [], "refuseCapacity": true }));
303+        let error = smol::block_on(observe_issue31_full_auto(&supervisor))
304+            .expect_err("no capacity means no reading");
305+        assert_eq!(error.token(), "observation.omega.capacity_unavailable");
306+    }
307+
308+    /// A daemon with nothing running is a real observation, and a different one
309+    /// from a daemon that was never asked.
310+    #[test]
311+    fn a_daemon_running_nothing_yields_an_empty_reading_rather_than_a_refusal() {
312+        let (_temporary, supervisor) = stub(json!({
313+            "runs": [],
314+            "capacity": { "activeRunLimit": 8, "activeRunCount": 0, "lanes": [] },
315+        }));
316+        let reading = smol::block_on(observe_issue31_full_auto(&supervisor))
317+            .expect("a host that looked and found nothing still has a reading");
318+        assert!(reading.run_details.is_empty());
319+    }
320+
321+    /// The reading a host takes of a daemon it did not read is not an empty
322+    /// reading — there is no way to express one.
323+    ///
324+    /// This is a compile-shaped assertion written as a runtime one: the only
325+    /// constructor reachable from outside `issue31_delivery` is `observed`,
326+    /// which takes no stamp. If someone adds a public setter or restores
327+    /// `Default`, the omega#97 property is gone, and the tests that name it in
328+    /// `issue31_delivery` go red.
329+    #[test]
330+    fn an_observed_reading_is_stamped_by_the_host_that_took_it() {
331+        let before = std::time::SystemTime::now()
332+            .duration_since(std::time::UNIX_EPOCH)
333+            .expect("clock")
334+            .as_millis() as u64;
335+        let reading = Issue31FullAutoReading::observed(
336+            7,
337+            Vec::new(),
338+            serde_json::json!({ "accounts": [] }),
339+            Vec::new(),
340+        );
341+        let after = std::time::SystemTime::now()
342+            .duration_since(std::time::UNIX_EPOCH)
343+            .expect("clock")
344+            .as_millis() as u64;
345+        assert!(
346+            reading.generated_at_ms() >= before && reading.generated_at_ms() <= after,
347+            "the stamp must be this host's own clock reading, not a supplied value: \
348+             {before} <= {} <= {after}",
349+            reading.generated_at_ms(),
350+        );
351+        assert_eq!(reading.host_generation, 7);
352+    }
353+
354+    #[test]
355+    fn every_observation_failure_is_a_distinct_token() {
356+        let tokens = [
357+            Issue31ObservationError::RunsUnavailable("x".into()).token(),
358+            Issue31ObservationError::CapacityUnavailable("x".into()).token(),
359+            Issue31ObservationError::RunDetailUnavailable {
360+                run_ref: "run.1".into(),
361+                error: "x".into(),
362+            }
363+            .token(),
364+        ];
365+        let unique: std::collections::BTreeSet<&str> = tokens.iter().copied().collect();
366+        assert_eq!(
367+            unique.len(),
368+            tokens.len(),
369+            "a host that could not look must say which look failed",
370+        );
371+    }
372+}
diff --git a/crates/full_auto_ui/src/panel.rs b/crates/full_auto_ui/src/panel.rs
index 8f487f8cf6..10ab429601 100644
--- a/crates/full_auto_ui/src/panel.rs
+++ b/crates/full_auto_ui/src/panel.rs
@@ -23,17 +23,8 @@ use omega_effectd::{
2323     openagents_session, shared_supervisor,
2424 };
2525 
26-use crate::issue31_delivery::{Issue31FullAutoReading, set_issue31_live_reading};
27-
28-/// The omega#47 contract carries at most this many runs in one projection.
29-const MAX_ISSUE31_PROJECTED_RUNS: usize = 16;
30-
31-fn unix_millis() -> u64 {
32-    std::time::SystemTime::now()
33-        .duration_since(std::time::UNIX_EPOCH)
34-        .map(|elapsed| u64::try_from(elapsed.as_millis()).unwrap_or(u64::MAX))
35-        .unwrap_or_default()
36-}
26+use crate::issue31_delivery::set_issue31_live_reading;
27+use crate::issue31_observation::observe_issue31_full_auto;
3728 use omega_front_door::LaunchOrigin;
3829 use serde_json::Value;
3930 use settings::{NotifyWhenAgentWaiting, Settings as _};
@@ -335,46 +326,26 @@ impl FullAutoPanel {
335326             // omega#49: the same poll feeds the phone. The Sarah host pump
336327             // publishes whatever reading is recorded here, so a Full Auto view
337328             // the desktop can see is one the owner's paired device can see too.
338-            // The reading is stored only when the daemon actually answered:
329+            //
330+            // omega#97: the polling itself moved to `issue31_observation`, so
331+            // this view and a headless host take one reading path rather than
332+            // two. It refuses an incomplete reading rather than shortening it —
339333             // recording a partial one would publish a shorter run list than the
340-            // host has, which reads on the phone as runs that ended.
341-            if let (Ok(runs), Some(capacity)) = (&listed, &capacity) {
342-                let generation = {
343-                    let guard = supervisor.lock().await;
344-                    guard.generation()
345-                };
346-                let mut run_details = Vec::new();
347-                let mut evidence = Vec::new();
348-                let mut complete = true;
349-                for run in runs.iter().take(MAX_ISSUE31_PROJECTED_RUNS) {
350-                    let mut guard = supervisor.lock().await;
351-                    match guard.get_run(&run.run_ref).await {
352-                        Ok(detail) => run_details.push(detail),
353-                        Err(_) => {
354-                            complete = false;
355-                            break;
356-                        }
357-                    }
358-                    if let (Ok(report), Ok(receipt)) = (
359-                        guard.get_report(&run.run_ref).await,
360-                        guard.get_receipt(&run.run_ref).await,
361-                    ) {
362-                        evidence.push((report, receipt));
363-                    }
364-                }
365-                if complete {
366-                    set_issue31_live_reading(Issue31FullAutoReading {
367-                        generated_at_ms: unix_millis(),
368-                        host_generation: generation,
369-                        run_details,
370-                        capacity: capacity.clone(),
371-                        // Provider connection handoffs are not part of this
372-                        // reading. They are durable host records owned by the
373-                        // Sarah pump's ledger (omega#91) and survive a restart
374-                        // that this poll does not; carrying them here as well
375-                        // would give one fact two sources.
376-                        evidence,
377-                    });
334+            // host has, which reads on the phone as runs that ended — and the
335+            // stamp is the observer's own clock reading, which no caller here
336+            // can supply.
337+            //
338+            // Provider connection handoffs are deliberately not part of this
339+            // reading. They are durable host records owned by the Sarah pump's
340+            // ledger (omega#91) and survive a restart that this poll does not;
341+            // carrying them here as well would give one fact two sources.
342+            match observe_issue31_full_auto(&supervisor).await {
343+                Ok(reading) => set_issue31_live_reading(reading),
344+                Err(error) => {
345+                    // Silence, not an empty view. The pump keeps publishing the
346+                    // last reading the host actually took, and a host that has
347+                    // never taken one publishes nothing at all.
348+                    log::debug!("omega#97: Full Auto observation skipped ({})", error.token());
378349                 }
379350             }
380351             this.update(cx, |this, cx| {
Served at tenant.openagents/omega Member data and write actions are omitted.