Appendix B - Lifecycle States and Invariant Reference
A lifecycle reference answers two separate questions: whether an edge is valid, and who is allowed to request it. The server owns desired task state. Assigned agents report actual state through an authorization and transition boundary. Node monitors and policy logic can also change task records for explicit reasons. A state name alone never grants ownership, and a condition such as node_lost is not a replacement state machine.
The tables below were generated by actually invoking the pinned public lifecycle validators in a disposable source copy. They include same-state acceptance separately, making repeated observations visible without pretending they are new transitions. Their confidence is the local helper contract: constructor execution and finite state checks do not prove a distributed execution will converge under every failure.
Allowed transitions
Service
| From | Different allowed destinations | Duplicate |
|---|---|---|
active |
deleting |
accepted |
deleted |
none | accepted |
deleting |
deleted |
accepted |
Task actual
| From | Different allowed destinations | Duplicate |
|---|---|---|
assigned |
pulling, created, running, healthy, unhealthy, stopping, stopped, removed, failed |
accepted |
created |
starting, running, stopping, stopped, removed, failed |
accepted |
failed |
removed |
accepted |
healthy |
running, unhealthy, stopping, stopped, removed, failed |
accepted |
pending |
assigned, stopped, removed, failed |
accepted |
pulling |
created, running, stopping, stopped, removed, failed |
accepted |
removed |
none | accepted |
running |
healthy, unhealthy, stopping, stopped, removed, failed |
accepted |
starting |
running, stopping, stopped, removed, failed |
accepted |
stopped |
removed |
accepted |
stopping |
stopped, removed, failed |
accepted |
unhealthy |
running, healthy, stopping, stopped, removed, failed |
accepted |
Task desired
| From | Different allowed destinations | Duplicate |
|---|---|---|
assigned |
none | accepted |
created |
none | accepted |
failed |
none | accepted |
healthy |
none | accepted |
pending |
none | accepted |
pulling |
none | accepted |
removed |
none | accepted |
running |
stopped, removed |
accepted |
starting |
none | accepted |
stopped |
removed |
accepted |
stopping |
none | accepted |
unhealthy |
none | accepted |
Node
| From | Different allowed destinations | Duplicate |
|---|---|---|
draining |
ready, offline |
accepted |
offline |
ready |
accepted |
ready |
draining, offline |
accepted |
unknown |
ready, offline |
accepted |
Deployment
| From | Different allowed destinations | Duplicate |
|---|---|---|
failed |
none | accepted |
paused |
running, failed |
accepted |
pending |
running, succeeded, failed, rolling_back, rolled_back |
accepted |
rolled_back |
none | accepted |
rolling_back |
paused, failed, rolled_back |
accepted |
running |
paused, succeeded, failed |
accepted |
succeeded |
none | accepted |
Desired-state construction normally uses running, stopped and removed. The generic desired validator first accepts any identical known TaskStatus values; therefore its matrix includes identity edges for other enum values. That is helper behavior, not permission for an agent to introduce those desired states. Starting and stopping are also recognized actual enums, but the ordinary agent report whitelist does not admit them. Their presence reserves lifecycle vocabulary without inventing a runtime progress feature.
Service deleted is terminal: restoring an old deleted ID to active is forbidden. Task failed and stopped are terminal for execution, but the generic actual-state validator permits cleanup to removed. Removed cannot leave itself. Deployment succeeded, failed and rolled_back are immutable terminal histories; rollback creates another deployment with its own ID and rolling_back state. Paused is accepted by the enum/validator but lacks an operator pause/resume CLI feature at this revision.
The agent guard narrows the generic task matrix. An actual failed task with desired running cannot report removed merely because ValidateTaskTransition permits that edge; AgentCanReportTaskStatus accepts only a duplicate terminal report in that branch. Once desired state is stopped or removed, the cleanup branch permits stopped, removed or failed. An already removed task still accepts only removed. This order matters when interpreting a late worker message.
Jobs and cron are separate mechanisms
Jobs have pending, running, succeeded and failed states. They do not call a generic ValidateJobTransition helper: MemoryService derives their state from associated task reports. Running or healthy work marks a job running. A stopped task uses an explicit exit code when available and otherwise zero; zero means succeeded. A failed task defaults its missing exit code to one. Retry budget checks compare Attempts to BackoffLimit before increasing attempts and creating a new pending task identity.
These rules must not be inferred from the service restart state machine. A retry creates another task; it does not resurrect a terminal task in place. Cron records scheduling metadata such as LastScheduleAt and LastJobID rather than a lifecycle enum equivalent to deployment status. Allow, Forbid and Replace govern overlapping jobs; occurrence selection, catch-up and clock behavior belong to the cron controller. A suspended cron entry does not imply its already running job was canceled.
Invariant index and scope
| ID | Invariant | Enforcing boundary | Counterexample outside scope |
|---|---|---|---|
| INV-01 | Replica convergence | reconcileServiceTasksLocked |
Zero capacity or restart never can leave intent unfulfilled; initial immediate placement bypasses richer Plan constraints. |
| INV-02 | Atomic task claim | AssignTask |
One-winner claim does not establish default server persistence or remote process fencing. |
| INV-03 | State-change event ordering | ReportTaskStatus |
Memory mutation and append are not a durable transaction across process failure; do not generalize to every controller append. |
| INV-04 | Assigned node owns reports | ReportTaskStatus |
Credential validity alone cannot authorize a different node’s task; physical old-worker activity is not fenced. |
| INV-05 | Namespace secrecy | GetService |
A global identifier must not be returned before scope validation; inspect every newer workload surface separately. |
| INV-06 | Rollout availability budget | reconcileDeployment |
Running new tasks alone do not satisfy newHealthy; concurrent physical failure can invalidate a snapshot budget. |
| INV-07 | Owned container cleanup | removeAssignedTask |
Server deleting status is not proof removal happened; cleanup can fail or old agent can remain unreachable. |
Each companion invariant records assumptions, a precise source symbol and either a real test name or an explicit test gap. An inspected test is not labeled as a new run. Replica convergence requires an active service, an applicable restart policy, capacity and eventually successful operations. Atomic claim safety establishes one successful assignment transition for one pending record; it does not fence a worker that lost contact after starting a process.
Event emission follows validated state changes in the memory handler, and duplicate terminal reports avoid duplicate state-change events. This is not durable atomicity between a database commit and an external event stream. The store-backed reconciler has different transaction/error windows and requires its own constructor and backing store. Default process restart destroys MemoryService intent even if a PostgreSQL package test previously passed.
Namespace secrecy is a lookup and return-value property, not a naming convention. An implementation must verify context before returning data associated with a guessed identifier. Safe cleanup similarly depends on managed, node and task labels, not just a user-provided container name. Neither invariant should be extended to new handlers or runtime adapters without tracing those checks again.
Three source-derived traces
Trace 1: a repeated healthy report. Preconditions: the task belongs to the authenticated node, DesiredStatus is running, ActualStatus is healthy, and its service restart policy need not act. (1) The agent submits healthy again. (2) Node/task ownership is verified. (3) The guard accepts the identity edge. (4) The control plane retains healthy without creating another state-change event. A new health observation may carry metadata; it must not be counted as a new lifecycle transition. The companion validator harness observed guard acceptance; full handler event behavior is established by the cited source test.
Trace 2: deletion during an active rollout. Preconditions: an active service has an in-progress deployment and running tasks. (1) DeleteService marks the service deleting. (2) Desired task state changes to stopped. (3) A late healthy report is rejected by the cleanup-intent guard. (4) The assigned agent stops and removes its owned container, then reports removed. (5) Finalization requires every associated actual task to be removed before the service is deleted. A previously captured rollout snapshot cannot make that service active again. Offline workers can leave deleting visible indefinitely; failed or stopped alone does not satisfy the finalization predicate. This is a reconstructed path, not a new Docker observation.
Trace 3: an offline stateless versus stateful task. Preconditions: two services differ by the Stateful flag and both have tasks on a node whose nonzero heartbeat is older than the monitor cutoff. (1) The node becomes offline. (2) Tasks receive a node_lost condition independently of their status. (3) Stateless work is failed with desired removed so replacement intent can be created elsewhere. (4) Stateful work retains assignment for manual recovery rather than pretending local data moved. (5) A later authenticated worker response must still satisfy current ownership and cleanup rules. Neither branch physically stops a partitioned process; there is no remote fencing token enforced by Docker. This trace describes source behavior and explicitly leaves split-brain prevention unproven.
Using the matrix in reviews
When adding a state, enumerate its owner, outgoing edges, duplicate behavior, cleanup path and every consumer switch. Start with an invalid representative edge such as failed to healthy, then inspect whether a higher-level policy replaces the task instead. Review stale reports against both current desired state and authenticated assignment. A valid generic edge can remain forbidden at the agent boundary, and a valid store transition can remain unused by the shipped binary.
The matrix is a finite reference for finding missing cases, not a formal proof of distributed correctness. Its most useful review question is concrete: after this message is delayed, retried or delivered to a restarted process, which identity and owner still exist to authorize it?
Source: lifecycle validators and report guard, transition tests, MemoryService transitions, agent ownership/cleanup tests, job bookkeeping, rollout controller, and store claim tests.
Source-backed boundary statements for this appendix are indexed in the appendix claim record. Detailed command, state, exercise and reference tables retain their own source provenance.