A daemon restart costs one vault lane 300s: module_warming is unclassified and falls through to FailClosed → Decode → non-transient
Observed on a ck-subc restart (0.14.0 → 0.14.1, daemon commit 49b23913), insula at 35d4e5c, subconscious path-dep pinned at 26d122bd. Timeline from the daemon journal, millisecond-ordered:
13:13:41.207 daemon start
13:13:41.262 aft registered
13:13:41.265559 [ck-quota] warning: anthropic vault credential.get failed (oauth:anthropic): FailClosed
13:13:41.265582 insula registered
13:13:41.266557 vault module registered <- ~1.0 ms AFTER the failed get
Wire cost, from a 30s recorder on the claude lane:
13:13:54Z … 13:18:58Z errorClass=decode_failed "decode error: credential vault rejected the request"
10 consecutive samples, recovered on the next attempt at +300s (= NON_TRANSIENT_BACKOFF)
Only the first lane to fire lost the race; codex and antigravity fetched a few ms later and found the vault. One credential.get failed line in the whole journal.
What the daemon actually returned
The vault module's own audit log has zero rows in that window (positive control: the same query form finds rows in the prior hour), and it writes nothing at request level for refused gets anyway — so the request never reached it. The reply was the daemon's. For a route.open whose target is supervised but not yet registered, subc-core/src/control.rs (at 49b23913, lines ~1573–1584) answers:
control_error_frame(&frame, if warming { "module_warming" } else { "target_unavailable" }, ...)
module_warming has existed since subconscious 01c6337e ("report warming modules distinctly", #53, 2026-08-21) and is exported as subc_protocol::error_codes::MODULE_WARMING.
Why insula turned that into a 300s penalty
crates/quota-module/src/vault_client.rs:
open_route (:539) sees FrameType::Error and calls classify_error_frame — correct.
classify_error_frame (:881–:905) recognises unknown_channel | unknown_module | module_reloading → RouteGone and target_unavailable | module_timeout | backend_error → Transport. module_warming is in neither list.
- It then looks for a vault
class field; a control error body has none; read_error_to_outcome _ => VaultGetError::FailClosed (:874).
crates/quota-core/src/provider.rs:264 maps FailClosed → FetchError::Decode("credential vault rejected the request") → non-transient → flat 300s backoff.
So a "try again in a moment" from the router is treated as a corrupt credential reply. The unknown_module arm one line up prints a warning precisely so the id is "printed nowhere else in this process" — the fallthrough one arm down discards the code it saw, and the only trace is the word FailClosed. Reconstructing this took the daemon journal plus the vault module's audit log as a negative control; from insula's own output it was unattributable.
Suggested fixes, in order of value
- Classify
MODULE_WARMING as RouteGone (transient; a warming target is the textbook retry-next-tick case). MODULE_REMOVED probably wants its own arm too — it is permanent, but it is not a decode failure either.
- Log the discarded code/class at every
_ => FailClosed. The wire carried a distinction the log destroyed. Same argument the unknown_module comment already makes.
- Read the codes from
subc_protocol::error_codes::* rather than string literals — insula links that crate; a new constant would then at least be greppable against the match, and the compiler can't help with strings.
- Separately:
FailClosed → Decode conflates two axes. Refusing to trust an unrecognised reply is right; waiting 300s to ask again is a retry-policy choice, and a 60s retry trusts the reply equally little. Worth deciding whether an unclassifiable route-layer reply should be non-transient at all.
Not the cause here but consistent with it: main.rs:217 spawns the refresher before expect_hello_ack (:275). The vault client is its own client-principal connection, so insula's own registration order is not what raced — the target's was — but a cold-slot tick that can fire before the module's own handshake is a second startup race waiting for a reason.
Base: insula 35d4e5c, subconscious 49b23913 (daemon) / 26d122bd (build pin). Line numbers are against those.
A daemon restart costs one vault lane 300s:
module_warmingis unclassified and falls through toFailClosed→Decode→ non-transientObserved on a
ck-subcrestart (0.14.0 → 0.14.1, daemon commit49b23913), insula at35d4e5c, subconscious path-dep pinned at26d122bd. Timeline from the daemon journal, millisecond-ordered:Wire cost, from a 30s recorder on the
claudelane:Only the first lane to fire lost the race;
codexandantigravityfetched a few ms later and found the vault. Onecredential.get failedline in the whole journal.What the daemon actually returned
The vault module's own audit log has zero rows in that window (positive control: the same query form finds rows in the prior hour), and it writes nothing at request level for refused gets anyway — so the request never reached it. The reply was the daemon's. For a route.open whose target is supervised but not yet registered,
subc-core/src/control.rs(at49b23913, lines ~1573–1584) answers:module_warminghas existed since subconscious01c6337e("report warming modules distinctly", #53, 2026-08-21) and is exported assubc_protocol::error_codes::MODULE_WARMING.Why insula turned that into a 300s penalty
crates/quota-module/src/vault_client.rs:open_route(:539) seesFrameType::Errorand callsclassify_error_frame— correct.classify_error_frame(:881–:905) recognisesunknown_channel | unknown_module | module_reloading→RouteGoneandtarget_unavailable | module_timeout | backend_error→Transport.module_warmingis in neither list.classfield; a control error body has none;read_error_to_outcome_ => VaultGetError::FailClosed(:874).crates/quota-core/src/provider.rs:264mapsFailClosed→FetchError::Decode("credential vault rejected the request")→ non-transient → flat 300s backoff.So a "try again in a moment" from the router is treated as a corrupt credential reply. The
unknown_modulearm one line up prints a warning precisely so the id is "printed nowhere else in this process" — the fallthrough one arm down discards the code it saw, and the only trace is the wordFailClosed. Reconstructing this took the daemon journal plus the vault module's audit log as a negative control; from insula's own output it was unattributable.Suggested fixes, in order of value
MODULE_WARMINGasRouteGone(transient; a warming target is the textbook retry-next-tick case).MODULE_REMOVEDprobably wants its own arm too — it is permanent, but it is not a decode failure either._ => FailClosed. The wire carried a distinction the log destroyed. Same argument theunknown_modulecomment already makes.subc_protocol::error_codes::*rather than string literals — insula links that crate; a new constant would then at least be greppable against the match, and the compiler can't help with strings.FailClosed→Decodeconflates two axes. Refusing to trust an unrecognised reply is right; waiting 300s to ask again is a retry-policy choice, and a 60s retry trusts the reply equally little. Worth deciding whether an unclassifiable route-layer reply should be non-transient at all.Not the cause here but consistent with it:
main.rs:217spawns the refresher beforeexpect_hello_ack(:275). The vault client is its own client-principal connection, so insula's own registration order is not what raced — the target's was — but a cold-slot tick that can fire before the module's own handshake is a second startup race waiting for a reason.Base: insula
35d4e5c, subconscious49b23913(daemon) /26d122bd(build pin). Line numbers are against those.