Context
Measured on the local Agent OS plane, 2026-08-19T12:44Z, while rebuilding it from a7b58e6a (2026-08-17) to dev's head 1bc70b56e1.
After docker compose -f docker-compose.yml -f docker-compose.local-agent-os.yml up -d --force-recreate --wait exited 0 and printed Healthy for everything it touched:
| service |
revision running |
|
kb-server |
1bc70b56e134 |
advanced |
mc-server |
1bc70b56e134 |
advanced |
fleet-server |
a7b58e6a9409 |
untouched |
orchestrator |
a7b58e6a9409 |
untouched |
All four reported running healthy throughout. Half the plane stayed two days stale behind an exit-0 recreate, and the only reason it was noticed is that the revision was read per container from the image labels instead of trusted from the exit code.
Separating observation from inference: the revisions, the split, and the exit code are observed; the cause is read from the compose files (profiles) rather than inferred from behaviour.
The Problem
fleet-server declares profiles: [fleet] (ai/deploy/docker-compose.yml:632) and orchestrator declares profiles: [cloud] (:518). docker compose up without --profile omits profiled services from the set it operates on, and leaves already-running containers of those services alone. So the command completes successfully having recreated a strict subset of the plane, with nothing in its output naming what it skipped.
The working invocation needed the profiles of whatever happens to be running:
docker compose --profile cloud --profile fleet … up -d --force-recreate --wait fleet-server orchestrator
Nothing at the point of use says so, and the required set is not a constant — it is a function of the live stack, which the operator has to discover by listing containers and cross-referencing profile declarations across two compose files.
Why this is worse than a documentation gap. The failure returns success and the post-conditions look right: every service healthy, every image freshly built. A rebuild is exactly the moment someone believes the plane is current, so a half-rebuild converts an ordinary staleness problem into a false belief that it was already handled. The plane in question had been stale for two days and the substrate already records a prior instance of the same class — ai/deploy/docker-compose.local-agent-os.yml, above the auth secret: "every container running a revision 103 commits behind dev while the fixes sat merged."
What this ticket is NOT, because both were checked and discarded rather than assumed:
- Not the mutable-ref cache hazard. That is
#16635, closed COMPLETED — ai/deploy/Dockerfile:20/:51 now refuse a non-SHA NEO_REF unless NEO_ALLOW_MUTABLE_REF=1. An unpinned build fails loudly instead of silently reusing the cached source layer. Verified present in the tree; the guard is working.
- Not a reporting gap.
healthcheck publishes deployedRevision (observed 1bc70b56e13488… post-rebuild), and runtimeFreshness is honest about its scope — it emitted "Runtime config/schema identity matches the current checkout", and RuntimeFreshnessService excludes gitHead from status-driving fields deliberately, with a guard preventing a current verdict from claiming a source dimension it never measured. A draft of this ticket targeted that field; reading the module removed the claim.
The residual is narrow and specific: no single command rebuilds the whole running plane, and the one that looks like it does reports success after covering part of it.
The Architectural Reality
ai/deploy/docker-compose.yml:518 — orchestrator, profiles: [cloud].
ai/deploy/docker-compose.yml:632 — fleet-server, profiles: [fleet].
ai/deploy/docker-compose.yml:666 / :711 — ingress (profiles: [ingress]) and local-model, the same shape; ingress was running during this incident and was likewise skipped, though it is caddy:2-alpine and carries no neo revision.
ai/deploy/docker-compose.local-agent-os.yml:8 — name: "${NEO_LOCAL_AGENT_OS_PROJECT_NAME:-neo-local-agent-os}". The stable project name is why a rebuild from any checkout targets the same stack, and therefore why a whole-plane entrypoint is expressible at all.
- Image labels
org.neomjs.image.requested-ref / org.opencontainers.image.revision — stamped at build, and the authority a per-container check reads. Emitted by #15775.
The Fix
Direction rather than prescription; the cheapest correct option depends on how much the deploy path should own.
Option A — a documented whole-plane entrypoint that derives the profile set from the containers currently running in the project, so the invocation cannot drift from the live stack. Removes the discovery step entirely.
Option B — a post-recreate assertion. After a rebuild, compare each running neo service's stamped revision against the revision just built and fail if any differs. Catches this class regardless of why a service was missed, including future profiles nobody has added yet, and matches the fail-closed posture #16635 established one layer down.
B is the stronger backstop because it does not enumerate the failure; A is the better ergonomics. They compose.
Contract Ledger Matrix
| Target Surface |
Source of Authority |
Proposed Behavior |
Fallback |
Docs |
Evidence |
| plane rebuild path |
the profile declarations at docker-compose.yml:518 / :632 (verified present) |
covers every running neo service, or refuses to report success |
existing ad-hoc commands keep working |
deploy docs |
2 of 4 services stale after an exit-0 --force-recreate --wait, all healthy |
Decision Record impact
none.
Acceptance Criteria
Out of Scope
#16635's mutable-ref cache hazard — closed COMPLETED and verified fixed in the tree.
runtimeFreshness and the deliberate gitHead exclusion — correct as designed.
- Publishing a revision in the deployment-state snapshot —
healthcheck already carries deployedRevision, and #16791 covered that surface.
- Automatic rebuilds or deploy-on-merge. This makes a partial rebuild impossible to mistake for a whole one; deciding when to deploy stays human.
inspect_deployment returning 104 KB, over the tool-result cap — a real adjacent defect, separately ticketable.
Avoided Traps
- Trusting an exit code over a per-container read. Both this and
#16635 return success. An AC written against exit status measures the wrong thing.
- Filing the cache hazard again. It is
#16635, closed COMPLETED, and the guard is in the tree. My rebuild succeeded because I passed an explicit SHA; unpinned would have failed loudly, which is the fix working.
- Filing against a field name. A draft targeted
runtimeFreshness for reporting current on a stale plane. It reported current for config/schema, which is what it measures and what it said.
- Enumerating the profiles as the fix. Naming
cloud and fleet in a command fixes today's two and silently excludes the third profile someone adds later. Option B is preferred because it asserts the property rather than the list.
Related
#16635 — the mutable-ref cache hazard, closed COMPLETED; the sibling failure mode in the same rebuild path, and the precedent for failing closed.
#15774 / #15775 — made the packaged revision observable; this consumes those labels.
#15787 — the open deploy-provenance holder.
#16791 — the deployment-state snapshot's revision surface, closed.
Origin Session ID: fb387768-e68f-4a71-9b6a-3cf9ad4a9e7e
Retrieval Hint: query_raw_memories("docker compose up skips profiled services fleet cloud profile rebuild reports success half plane stale")
Context
Measured on the local Agent OS plane, 2026-08-19T12:44Z, while rebuilding it from
a7b58e6a(2026-08-17) todev's head1bc70b56e1.After
docker compose -f docker-compose.yml -f docker-compose.local-agent-os.yml up -d --force-recreate --waitexited 0 and printedHealthyfor everything it touched:kb-server1bc70b56e134mc-server1bc70b56e134fleet-servera7b58e6a9409orchestratora7b58e6a9409All four reported
running healthythroughout. Half the plane stayed two days stale behind an exit-0 recreate, and the only reason it was noticed is that the revision was read per container from the image labels instead of trusted from the exit code.Separating observation from inference: the revisions, the split, and the exit code are observed; the cause is read from the compose files (profiles) rather than inferred from behaviour.
The Problem
fleet-serverdeclaresprofiles: [fleet](ai/deploy/docker-compose.yml:632) andorchestratordeclaresprofiles: [cloud](:518).docker compose upwithout--profileomits profiled services from the set it operates on, and leaves already-running containers of those services alone. So the command completes successfully having recreated a strict subset of the plane, with nothing in its output naming what it skipped.The working invocation needed the profiles of whatever happens to be running:
Nothing at the point of use says so, and the required set is not a constant — it is a function of the live stack, which the operator has to discover by listing containers and cross-referencing profile declarations across two compose files.
Why this is worse than a documentation gap. The failure returns success and the post-conditions look right: every service healthy, every image freshly built. A rebuild is exactly the moment someone believes the plane is current, so a half-rebuild converts an ordinary staleness problem into a false belief that it was already handled. The plane in question had been stale for two days and the substrate already records a prior instance of the same class —
ai/deploy/docker-compose.local-agent-os.yml, above the auth secret: "every container running a revision 103 commits behinddevwhile the fixes sat merged."What this ticket is NOT, because both were checked and discarded rather than assumed:
#16635, closed COMPLETED —ai/deploy/Dockerfile:20/:51now refuse a non-SHANEO_REFunlessNEO_ALLOW_MUTABLE_REF=1. An unpinned build fails loudly instead of silently reusing the cached source layer. Verified present in the tree; the guard is working.healthcheckpublishesdeployedRevision(observed1bc70b56e13488…post-rebuild), andruntimeFreshnessis honest about its scope — it emitted "Runtime config/schema identity matches the current checkout", andRuntimeFreshnessServiceexcludesgitHeadfrom status-driving fields deliberately, with a guard preventing acurrentverdict from claiming asourcedimension it never measured. A draft of this ticket targeted that field; reading the module removed the claim.The residual is narrow and specific: no single command rebuilds the whole running plane, and the one that looks like it does reports success after covering part of it.
The Architectural Reality
ai/deploy/docker-compose.yml:518—orchestrator,profiles: [cloud].ai/deploy/docker-compose.yml:632—fleet-server,profiles: [fleet].ai/deploy/docker-compose.yml:666/:711—ingress(profiles: [ingress]) andlocal-model, the same shape;ingresswas running during this incident and was likewise skipped, though it iscaddy:2-alpineand carries no neo revision.ai/deploy/docker-compose.local-agent-os.yml:8—name: "${NEO_LOCAL_AGENT_OS_PROJECT_NAME:-neo-local-agent-os}". The stable project name is why a rebuild from any checkout targets the same stack, and therefore why a whole-plane entrypoint is expressible at all.org.neomjs.image.requested-ref/org.opencontainers.image.revision— stamped at build, and the authority a per-container check reads. Emitted by#15775.The Fix
Direction rather than prescription; the cheapest correct option depends on how much the deploy path should own.
Option A — a documented whole-plane entrypoint that derives the profile set from the containers currently running in the project, so the invocation cannot drift from the live stack. Removes the discovery step entirely.
Option B — a post-recreate assertion. After a rebuild, compare each running neo service's stamped revision against the revision just built and fail if any differs. Catches this class regardless of why a service was missed, including future profiles nobody has added yet, and matches the fail-closed posture
#16635established one layer down.B is the stronger backstop because it does not enumerate the failure; A is the better ergonomics. They compose.
Contract Ledger Matrix
docker-compose.yml:518/:632(verified present)--force-recreate --wait, all healthyDecision Record impact
none.Acceptance Criteria
main, it is not exercising the skip.#16635returned success.upomits profiled services, at the point where the rebuild is described.Out of Scope
#16635's mutable-ref cache hazard — closed COMPLETED and verified fixed in the tree.runtimeFreshnessand the deliberategitHeadexclusion — correct as designed.healthcheckalready carriesdeployedRevision, and#16791covered that surface.inspect_deploymentreturning 104 KB, over the tool-result cap — a real adjacent defect, separately ticketable.Avoided Traps
#16635return success. An AC written against exit status measures the wrong thing.#16635, closed COMPLETED, and the guard is in the tree. My rebuild succeeded because I passed an explicit SHA; unpinned would have failed loudly, which is the fix working.runtimeFreshnessfor reportingcurrenton a stale plane. It reportedcurrentfor config/schema, which is what it measures and what it said.cloudandfleetin a command fixes today's two and silently excludes the third profile someone adds later. Option B is preferred because it asserts the property rather than the list.Related
#16635— the mutable-ref cache hazard, closed COMPLETED; the sibling failure mode in the same rebuild path, and the precedent for failing closed.#15774/#15775— made the packaged revision observable; this consumes those labels.#15787— the open deploy-provenance holder.#16791— the deployment-state snapshot's revision surface, closed.Origin Session ID: fb387768-e68f-4a71-9b6a-3cf9ad4a9e7e
Retrieval Hint:
query_raw_memories("docker compose up skips profiled services fleet cloud profile rebuild reports success half plane stale")