# Census: the offline move's harness fixes (Run 9, defects 6 to 8)

Status: RULED. Every row carries `rev3=PROPOSED-moves-offline-fixes`. The
ruling is asked of the census owner, platform-1c.

The branch is `moves-offline-fixes`, from main after 54ae7fb9 (v0.28.5). It
fixes three defects the deployment harness found in the offline operator move
(harness RUNLOG, Run 9). It adds one census overlay,
`internal/provision/testdata/plan14-root-reads-moves-offline-fixes.tsv`,
loaded last in `TestPlan14RootReadCensus`, after armed-box-updates. The
record-write census (`TestPlan9B8RecordWriteCensus`) does not change: no
record write is added or moved.

## The footing

Every unit in the overlay runs in the box agent (`/opt/carlos/carlos agent`,
carlos-edge.service, `User=carlos`). None is reached from a root process, and
none makes a root decision. So each is proposed accepted legacy at the
disposition main gives it or its siblings: J1 (root-route) for registry and
route reads, J4 (root-input) for file reads, J1,J4 where a unit does both.
That is the footing census-ruling-moves-offline accepted for the offline
move's own units.

## The units, by defect

### Defect 6: config.env for a nested row whose instance directory is not made yet

| unit | change | executing identity | disposition |
|---|---|---|---|
| `internal/appconfig/syncexec.go` `SyncExecWithCallbacks` | changed legacy: a write that found no instance directory is named and skipped (`errInstanceDirAbsent`, `WhenInstanceDirAbsent`), and the pass goes on; no new read site | the agent, through `syncExecSerialized` (config loops) | re-frozen, J1,J4 |
| `internal/appconfig/syncexec.go` `renderExecConfigBefore` | changed legacy: one `Lstat` of the nested row's instance directory after its write failed, to tell "not made yet" from any other failure; follows no link | the agent: config sync and the moves worker's held render | re-frozen, J1,J4 |
| `internal/host/agent.go` `configGenLoop` | changed legacy: the tick's body moved into `configGenTick`; the registry listing stays | the agent | re-frozen, J1 |
| `internal/host/agent.go` `configGenTick` | new: `configGenLoop`'s tick over the routes it listed; also runs the agent's own config pass once a directory the last pass waited for exists | the agent | accepted legacy, J1 (as `configGenLoop`) |
| `internal/host/agent.go` `pendingDirs.appeared` | new: an `Lstat` (never follows a link) of the instance directories the last config pass could not write into | the agent | accepted legacy, J4 (as the moves units' `isDir`, `lexists`) |

Envelopes re-pinned: `internal/appconfig`, `internal/host`.

### Defect 7: fresh reports at every door step

No unit row. The re-assert (`movesWorker.offlineReassert`, run by
`offlineReassertLoop` in the agent every 15 s or so) reads the record from the
deployment store and calls the source and sealer steps
census-ruling-moves-offline already classified (`offlineSourceStop`,
`offlineKeepStop`); it has no root read of its own. The console's stale-report
refusal (MV-O178) reads nothing new. Envelopes re-pinned: `internal/console`,
`internal/host`.

### Defect 8: the source's scheduler wrote the moved host's env file after open

The schedule runner is built only by the agent (`armSchedule`, from
`runAgent`); no root process makes one.

| unit | change | executing identity | disposition |
|---|---|---|---|
| `internal/host/schedule_wiring.go` `newScheduleDeps` | changed legacy: the runner's `Skip` is set to `scheduleSkip`; no new read site | the agent | re-frozen, J1 |
| `internal/host/schedule_wiring.go` `scheduleSkip` | new: reads a row's status (any hold) and host (the armed offline view says the record names another region) to tell the runner to leave the host alone; it only withholds a tick | the agent's schedule runner | accepted legacy, J1 (as the runner's own hold checks) |
| `internal/schedule/runner.go` `Runner.Tick` | changed legacy: a host `Skip` names gets no tick and no bookkeeping at the pre-filter | the agent's schedule runner | re-frozen at census-ruling-seeded-instances' disposition |
| `internal/schedule/runner.go` `Runner.fire` | re-pinned: with `Skip` set, the row is read again before the token, and a skipped host gets no token and no record | the agent's schedule runner (overlay e names the service controller and root callbacks for the claim design; neither sets `Skip`) | hint, E2, unchanged |

Envelopes re-pinned: `internal/host`, `internal/schedule`.

### Fix round 1 (Astra's review)

| finding | unit | change | executing identity | disposition |
|---|---|---|---|---|
| 1 | `internal/schedule/runner.go` `Runner.Tick` | re-pinned: its due query moved into `Runner.dueRows` | the agent's schedule runner | re-frozen, as above |
| 1 | `internal/schedule/runner.go` `Runner.dueRows` | new: held hosts left out by the query when `Skip` is set, and hosts `Skip` names paged past before the limit (one row read per host in the batch) | the agent's schedule runner | accepted legacy, J1 (as `Runner.Tick`) |
| 2 | none | the refresh calls `offlineSealerRefresh` (seal, lease and source reports read, only the report written, the lease never), no root read of its own | the agent | envelope `internal/host` only |
| 3 | `internal/appconfig/syncexec.go` `SyncExecWithCallbacks` | re-pinned: names each in-scope host done or not once the pass has gone through every route (`WithHostOutcome`); no new read site | the agent | re-frozen, J1,J4 |
| 3 | `internal/host/agent.go` `pendingDirs.appeared` | re-pinned: the same `Lstat`, with a retry backoff for a host a pass left undone | the agent | re-frozen, J4 |


## The seal

The accepted-legacy rows add frozen declarations, so the legacy seal moves,
from `393e7f45…bec29` (main, after moves-offline and armed-box-updates) to
`edc04f7349acf2b9ea98ffc8a1d3b71516a6ddb95505abfc2e38e49fd802ccc2` (after fix round 1). The test
checks main's seal before it loads this overlay, so a change on main's side
still shows on its own.

## What the ruling is asked to decide

1. That every unit above runs only in the agent and makes no root decision,
   so accepted legacy at main's disposition is the right footing.
2. That `Runner.fire` keeps its class and task (hint, E2): the change only
   withholds a token and a record for a host `Skip` names.
3. That the re-assert of defect 7 needs no row: it calls only steps already
   classified under census-ruling-moves-offline.

## Ruling (platform-1c, 2026-10-20): census-ruling-moves-offline-fixes, ACCEPTED as proposed

Accepted at d75b7e0b (F1 to F4) and re-ruled for fix round 1 at 7cd5db4d (F5 to F7). After Astra's scoped re-review, the backoff counter saturates (agent.go `pendingDirs.record`): an internal/host envelope re-pin only. The rev3 tag was rewritten to the ruling's name, so the seal moves to 826c54be…a62f.
- After merging main 21850356 (install-console-upgrade, loaded before this overlay): internal/host envelope re-pinned to the combined tree; seal unchanged.
