Known issues and carry-overs for later milestones¶
This file carries only problems that still exist. An entry that gets fixed is
deleted, and the account of what it was and how it was found lives in the
commit that removed it — git log -p docs/reference/known-issues.md is where to look
for one. A closed entry left standing with a note saying it is closed costs a
reader the same attention as a live one, which is the whole reason for the
rule.
Things that are not open problems live elsewhere, and on 2026-08-27 four of
them moved out of this file to where they belong.
upgrading.md carries what strands an object or rolls a fleet
when an installation crosses a release — real work for whoever is upgrading
one, and nothing at all for anyone else.
ca-rotation.md carries the CA rotation procedure, which is
a thing a human drives rather than a thing that is wrong — including that
nothing schedules one, which is a decision and not an omission, and where the
clock is published so that nobody has to remember.
persistent-storage.md carries what an operator owns
about a persistent group's claims — that this operator never deletes one, that
deleting one deletes a world, and how long a group whose storage is broken
takes to say so.
network-boundaries.md carries what the
NetworkPolicy objects buy and what they do not, and what bounds the number of
agents that may reach the operator — measured scope rather than a list of
faults.
charts/spawnery/README.md carries the manual
grant a chart cannot make for a namespace that does not exist yet, and why the
digest checked in at any tag describes the release before it.
Two of the things this file used to carry were facts about one cluster rather
than about this code, and they now live where that cluster is described — the
GitOps repository, beside the HelmRelease whose arguments they are about.
Anything here should be a claim about this repository; a claim about paulwtf
belongs to paulwtf.
Older documents in docs/ name sections of this file that no longer exist.
That is the rule above working, not rot: a handover or a runbook records what
was open at its milestone, and rewriting one to match today would falsify a
record. git log -p docs/reference/known-issues.md is where a named section went.
The design decisions live in
superpowers/specs/2026-08-07-minecraft-cloud-operator-design.md, in
superpowers/specs/2026-08-08-agent-channel-design.md, in
superpowers/specs/2026-08-09-paper-agent-design.md, in
superpowers/specs/2026-08-10-proxy-channel-design.md, in
superpowers/specs/2026-08-10-velocity-image-design.md and in
superpowers/specs/2026-08-11-velocity-agent-design.md.
A spec.mounts entry under /data is a fourth writer into it¶
extraFiles reasons about three things writing into a server's working
directory on a start — the renderer, the extraFiles copy and the
extraPlugins copy — and makes their paths disjoint by refusing a claim that
carries a path one of the others owns. A claim-backed or ConfigMap-backed
spec.mounts entry nested under /data is a fourth, and no scan knows about
it.
A group with a ConfigMap mounted at /data/mods and an extraFiles claim
carrying a top-level mods/ dies on the copy, because every mount this
operator renders is read-only:
cp: can't create 'mods/pack.jar': Read-only file system
Under set -eu that ends the start, and the message names neither the mount
nor the claim.
Documented rather than fixed, because it mirrors an accepted risk this code
already carries. The chmod in image/entrypoint.sh narrows itself to the
entries it just copied, rather than running chmod -R u+w ., for exactly this
reason: a read-only mount somewhere else under /data would make the wider
version die the same way — and since 0.2.34 it stops at filesystem boundaries
(find -xdev), so a mount nested inside a copied directory no longer kills
the start on the chmod either. What remains is the cp itself: the
entrypoint cannot tell a read-only mount from a read-only file without probing
every destination before copying, and the operator cannot know what a claim
holds when it admits the group. What it could do is refuse a spec.mounts
path under /data when the group also names extraFiles — which would refuse
the many groups where the two do not overlap at all, to catch the few where
they do.
The remedy is the ordinary one: a mount and an extraFiles claim should not
aim at the same directory. Found by reading the design against the collision
check, not by a failure.
A capacity edit still clears a group's failure streak¶
ofGeneration (internal/controller/servergroup_controller.go) narrows an
ephemeral group's views to the current metadata.generation before
CountFailures runs, so raising minReplicas resets
status.consecutiveFailures and clears a Degraded condition. Scaling a group
up is not a fix for whatever its servers were failing on.
Milestone 7a moved every other staleness comparison onto
podspec.DesiredServerHash and deliberately did not move this one. Failure
counting runs unconditionally and early, before the hash is computed, and that
ordering is itself a decision: the hash is gated on the group's Network being
usable, and a group whose Network was deleted is exactly the one that piles
failures up. Replacing a value that is always present with one that is
sometimes empty, on that path, is the wrong trade.
This entry once said the /cloud milestone would inherit the hazard, and it
will not. The claim was that /cloud start lobby would be a spec edit an
admin types, and so would clear a CrashLoopBackoff and start hammering a
broken image from a fresh window. That command turned out not to edit the
group at all: the operator has no write access to a ServerGroup's spec, and
the one on this project's own cluster is Flux-managed, so a minReplicas it
wrote would be reverted. Extra capacity became its own object instead
(ScaleBoost), which does not move metadata.generation and therefore cannot
touch the streak.
So what remains is the plain fact above and no consequence beyond it: a
person editing minReplicas still clears their group's failure count.
Whether that is worth fixing is a smaller question than it looked, and nothing
currently forces it.
Found by reading, not by a failure: writing 7a's plan against the code showed the ordering, and the task that would have changed this was removed from the plan rather than left to fail.
A corrected configOverlay is the one edit that cannot clear a latched group¶
spec.configOverlay names a ConfigMap, and the operator renders it as a
volume without ever reading what is in it (configOverlayVolume,
internal/podspec/server.go:278). That is deliberate — the merge happens in
spawnery-config inside the image — and it has a consequence at the far end
of the code. The overlay's content reaches neither DesiredServerHash nor
metadata.generation.
An overlay that names a property the CRD does not declare kills every server its group starts. After six rounds the group latches:
not retrying: 6 rounds of server starts failed in a row; change the group's
spec to try again — any edit to it clears the streak, and spec.attributes
shapes no pod, so editing that one retries without replacing a running server
Correcting the overlay is what removes the cause, and it is precisely the edit
that cannot lift the latch. Only group.Generation !=
group.Status.ObservedGeneration resets status.consecutiveFailures
(internal/controller/servergroup_controller.go:315), and a ConfigMap's
content is not the group's generation. The group then sits Degraded with
nothing left wrong with it.
The remedy costs nothing, and since 2026-09-07 the message carries it. Any
spec edit clears the streak, and spec.attributes is the one to reach for: it
shapes no pod and is not in the hash a server is replaced on, so it moves the
generation without replacing anything that is running. The condition used to
stop at "change the group's spec", which reads as already followed to whoever
has just corrected the ConfigMap; it now names the field as well.
That is the half of this entry that was worth fixing. The other half is still here: the operator does not notice that the cause is gone, and somebody has to make an edit that means nothing in order to say so.
This is the sibling of the entry above it, from the other side. There, a capacity edit clears a streak it has not earned; here, the edit that has earned it clears nothing. Both follow from the same decision — that the streak belongs to a generation — and neither is worth undoing on its own.
Measured 2026-09-04, on four ephemeral groups of one installation that stayed
latched after their overlay was corrected. An attributes edit cleared all
four and replaced no running server. Fixing it in the operator would mean
giving the reset a second input — the overlay ConfigMap's resourceVersion
— which means watching an object the reconciler currently only names, for a
case a one-line edit already answers, and which the message now names.
TestARecreatedOrdinalCreatesItsPodOnceThePredecessorIsGone failed once and was never reproduced¶
The test (internal/controller/server_controller_test.go) recreates an
ordinal over a pod that is still terminating, lets the pod finish the way a
kubelet would, and expects the next pass to create the successor. It failed
one make test with status.podName empty, passed in isolation and on a
full rerun, and nothing was captured.
One thing is ruled out rather than assumed: it is not cache lag.
internal/testenv's client is client.New, a direct client with no informer
behind it, so the hypothesis anyone reaches for first with envtest cannot be
the mechanism.
The assertion prints what a second occurrence needs and the first did not
have: the Accepted condition, every pod in the namespace with its deletion
timestamp and node, and whether the pod under the name is still the
predecessor by UID. A lingering predecessor says the force delete did not
take; a Server carrying PodNameTerminating with no such pod present says the
controller decided against a pod that is no longer there; an empty namespace
with a clean condition says something else refused the create. The second
occurrence should be a diagnosis, and this entry leaves with it.