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 coordinate
30617:7649603503856e5148d571eac2766b288a8ff1e9e35d380337a1d2b0015b4f92:omegaMaintainersHidden in public view
References2 branches · 1 tag
Read-only clone
git clone https://openagents.com/git/tenant.openagents/omega.gitRevision diff
83862c2a → 9660afa6diff --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| {