| 1 | # Census: the offline move's harness fixes (Run 9, defects 6 to 8) |
| 2 | |
| 3 | Status: RULED. Every row carries `rev3=PROPOSED-moves-offline-fixes`. The |
| 4 | ruling is asked of the census owner, platform-1c. |
| 5 | |
| 6 | The branch is `moves-offline-fixes`, from main after 54ae7fb9 (v0.28.5). It |
| 7 | fixes three defects the deployment harness found in the offline operator move |
| 8 | (harness RUNLOG, Run 9). It adds one census overlay, |
| 9 | `internal/provision/testdata/plan14-root-reads-moves-offline-fixes.tsv`, |
| 10 | loaded last in `TestPlan14RootReadCensus`, after armed-box-updates. The |
| 11 | record-write census (`TestPlan9B8RecordWriteCensus`) does not change: no |
| 12 | record write is added or moved. |
| 13 | |
| 14 | ## The footing |
| 15 | |
| 16 | Every unit in the overlay runs in the box agent (`/opt/carlos/carlos agent`, |
| 17 | carlos-edge.service, `User=carlos`). None is reached from a root process, and |
| 18 | none makes a root decision. So each is proposed accepted legacy at the |
| 19 | disposition main gives it or its siblings: J1 (root-route) for registry and |
| 20 | route reads, J4 (root-input) for file reads, J1,J4 where a unit does both. |
| 21 | That is the footing census-ruling-moves-offline accepted for the offline |
| 22 | move's own units. |
| 23 | |
| 24 | ## The units, by defect |
| 25 | |
| 26 | ### Defect 6: config.env for a nested row whose instance directory is not made yet |
| 27 | |
| 28 | | unit | change | executing identity | disposition | |
| 29 | |---|---|---|---| |
| 30 | | `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 | |
| 31 | | `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 | |
| 32 | | `internal/host/agent.go` `configGenLoop` | changed legacy: the tick's body moved into `configGenTick`; the registry listing stays | the agent | re-frozen, J1 | |
| 33 | | `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`) | |
| 34 | | `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`) | |
| 35 | |
| 36 | Envelopes re-pinned: `internal/appconfig`, `internal/host`. |
| 37 | |
| 38 | ### Defect 7: fresh reports at every door step |
| 39 | |
| 40 | No unit row. The re-assert (`movesWorker.offlineReassert`, run by |
| 41 | `offlineReassertLoop` in the agent every 15 s or so) reads the record from the |
| 42 | deployment store and calls the source and sealer steps |
| 43 | census-ruling-moves-offline already classified (`offlineSourceStop`, |
| 44 | `offlineKeepStop`); it has no root read of its own. The console's stale-report |
| 45 | refusal (MV-O178) reads nothing new. Envelopes re-pinned: `internal/console`, |
| 46 | `internal/host`. |
| 47 | |
| 48 | ### Defect 8: the source's scheduler wrote the moved host's env file after open |
| 49 | |
| 50 | The schedule runner is built only by the agent (`armSchedule`, from |
| 51 | `runAgent`); no root process makes one. |
| 52 | |
| 53 | | unit | change | executing identity | disposition | |
| 54 | |---|---|---|---| |
| 55 | | `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 | |
| 56 | | `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) | |
| 57 | | `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 | |
| 58 | | `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 | |
| 59 | |
| 60 | Envelopes re-pinned: `internal/host`, `internal/schedule`. |
| 61 | |
| 62 | ### Fix round 1 (Astra's review) |
| 63 | |
| 64 | | finding | unit | change | executing identity | disposition | |
| 65 | |---|---|---|---|---| |
| 66 | | 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 | |
| 67 | | 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`) | |
| 68 | | 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 | |
| 69 | | 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 | |
| 70 | | 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 | |
| 71 | |
| 72 | |
| 73 | ## The seal |
| 74 | |
| 75 | The accepted-legacy rows add frozen declarations, so the legacy seal moves, |
| 76 | from `393e7f45…bec29` (main, after moves-offline and armed-box-updates) to |
| 77 | `edc04f7349acf2b9ea98ffc8a1d3b71516a6ddb95505abfc2e38e49fd802ccc2` (after fix round 1). The test |
| 78 | checks main's seal before it loads this overlay, so a change on main's side |
| 79 | still shows on its own. |
| 80 | |
| 81 | ## What the ruling is asked to decide |
| 82 | |
| 83 | 1. That every unit above runs only in the agent and makes no root decision, |
| 84 | so accepted legacy at main's disposition is the right footing. |
| 85 | 2. That `Runner.fire` keeps its class and task (hint, E2): the change only |
| 86 | withholds a token and a record for a host `Skip` names. |
| 87 | 3. That the re-assert of defect 7 needs no row: it calls only steps already |
| 88 | classified under census-ruling-moves-offline. |
| 89 | |
| 90 | ## Ruling (platform-1c, 2026-10-20): census-ruling-moves-offline-fixes, ACCEPTED as proposed |
| 91 | |
| 92 | 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. |
| 93 | - After merging main 21850356 (install-console-upgrade, loaded before this overlay): internal/host envelope re-pinned to the combined tree; seal unchanged. |
| 94 | |