Task: Carry the host on the heartbeat
Table of Contents
This page documents a task in the Implement the operations journeys in ores.web story. It captures the goal, current status, acceptance, and any notes or results.
1. Goal
Every service heartbeat carries the service name, the instance id and the
release, and no host. The compute side is the mirror: each node sample
carries the host id, and no instance id. Nothing joins an instance to the
machine it runs on, or a compute wrapper to its node
(The service heartbeat carries no host).
This task adds host_id to service_heartbeat_message and stores it on
service_sample beside instance_id, so the grid's wrapper panel and the
services roster can name the machine behind an instance.
2. Status
| Field | Value |
|---|---|
| State | DONE |
| Parent story | Implement the operations journeys in ores.web |
| Now | Nothing. |
| Waiting on | Nothing. |
| Next | Nothing. |
| Last touched | 2026-10-04 |
3. Acceptance
service_heartbeat_messagecarries ahost_id, andservice_samplestores it besideinstance_id.- The publishing side fills it wherever it already holds the id, the compute wrapper first: the wrapper already stamps its node samples with it.
- The capture The service heartbeat carries no host moves out of the inbox with this task's evidence.
- The change regenerates cleanly, and the telemetry and compute tests pass.
4. Plan
The field travels one way: a publisher stamps it, the telemetry service
copies it, and the database stores it. The work follows that path. Both
messages keep one field order — service_name, instance_id, host_id,
version — so the model, the entity and the stored row agree.
- Add
host_idtoservice_heartbeat_messageandservice_sampleinprojects/ores.telemetry/modeling/ores.telemetry.service_samples.org. The field sits besideinstance_id, and it is empty when the publisher does not know the host. - Regenerate the protocol from the model, and check the C++ and TypeScript diffs carry the new field and nothing else.
- Fill the field where the value exists.
heartbeat_publishertakeshost_idas a trailing defaulted constructor argument and stamps every message with it, and the compute wrapper passescfg.host_id— the value it already stamps on its node samples. Every other service holds no host id, so its beats carry the empty string. - Copy the field at the receiving end:
telemetry_handler::service_heartbeatwriteshb->host_idonto theservice_sampleit builds. - Store it.
service_sample_entitygainsstd::string host_id;, the mapper carries it both ways, and the create script addshost_id text not null default ''. - Prove it. Add a repository test that inserts a sample with a host id and
reads it back through
list_service_samples, then build and run the telemetry and compute tests against a recreated database. - Close the loop. Move capture A1D7FC12 out of the inbox with the evidence, and record the measurements on this task.
5. Measurements
Every number this task claims about a tree, with the command that produced it
and the commit it was measured at. A number without both is not a measurement,
because a clean-up record describes a tree and trunk moves under a branch: a
consumer count, a gate's component count and a suite's size all change with no
code changing, and the reader of a bare number cannot tell which tree it came
from. Where the raw output matters, put it in an evidence/ file beside the
task and name that file in the Command column.
| What | Value | Command | Commit |
|---|---|---|---|
| Telemetry suites | 3/3 pass: core 0.90 s, database 1.84 s, service 0.81 s | compass test run -- -R 'ores\.telemetry\.' |
fd05c11807 |
| Compute suites | 3/3 pass: api 0.20 s, core 3.63 s, service 0.74 s | compass test run -- -R 'ores\.compute\.' |
fd05c11807 |
| Database recreate | fingerprint 348f2fc9f8d6d570, 0 errors, 0 m 42 s |
compass db recreate -y -k |
fd05c11807 |
| Drift sweep | no file this branch touched would change | check_component_drift.py --sweep |
fd05c11807 |
| ER diagram check | 430 tables, 0 warnings, puml up to date | plantuml_er_generate.sh --check |
fd05c11807 |
| TypeScript typecheck | no errors | npm run typecheck (wire-protocol) |
fd05c11807 |
| Live demo | wrapper beats carry the host: 17/17; every other service sends the empty string | compass db sql (queries in PR #2626 comment) |
3a8ca6538e |
6. Notes
The design is settled on The service heartbeat carries no host: one field, not a new entity. The alternative — a release and an instance id on the node sample — answers the grid's join only, and leaves the services screen blind.
7. Test Scenarios
Manual QA scenarios (scaffolded via compass add test_scenario)
that verify this task. Link
new ones here as they're created; the scenario doc itself links back
via its "Verifies task" field.
| Scenario | State | Notes |
|---|---|---|
8. PRs
| PR | Title |
|---|---|
| #2626 | [telemetry] Carry the host on the service heartbeat |
9. Review
| # | Comment summary | File | Decision | Notes |
|---|---|---|---|---|
| 1 | Existing databases: create table if not exists does not add the column |
projects/ores.sql/create/telemetry/service_samples_create.sql |
Accepted | The project has no migration mechanism: environments are created by compass db recreate, and the fleet starts only on a matching fingerprint. No long-lived database exists to alter. |
| 2 | Mixed-version rollout: a missing field may fail to decode | build/.../rfl/parsing/NamedTupleParser.hpp (rfl 0.25.0) |
Accepted | Verified in the vendored source: a plain std::string field is required (line 289), so an old publisher's beat fails to decode and telemetry logs a warning and drops it (telemetry_handler.hpp:136). An unknown field is ignored, so a new publisher is safe on an old telemetry. The fleet starts from one build, so no mixed window exists; the failure is bounded and self-heals on publisher restart. |
| 3 | Primary key: host_id is not in the key |
projects/ores.sql/create/telemetry/service_samples_create.sql:35 |
Accepted | The key stays (sampled_at, service_name, instance_id); instance ids are unique per process. |
| 4 | Bundled merge in the six-commit history | — | Accepted | House flow merges main into the branch before review; the review surface is the implementation commit. |
10. Result
service_heartbeat_message and service_sample carry host_id, and the one
field answers both joins the capture named: a wrapper's row can name its node,
and an instance's row its host.
The field travels the whole path. The model carries it on both messages, the
regenerated C++ and TypeScript protocols carry it, and heartbeat_publisher
takes it as a trailing defaulted argument and stamps every beat. The compute
wrapper passes cfg.host_id, the value it already stamps on its node samples;
every other service sends the empty string. telemetry_handler copies it onto
the sample, and the entity, the mapper and the host_id text not null default
'' column store it.
Verification. The repository test create_and_list_service_sample writes a
sample with a host id and reads it back through list_service_samples. The
telemetry suites (3/3) and the compute suites (3/3) pass against a recreated
database. See the measurements above.