The state
spark_quiet_mode is the dysregulation protocol — the Three S's, and the one
policy rule in pxh.policy that binds both origins (policy.py:117). It is
also a naked boolean with an indefinite lifetime, three writers, asymmetric
transitions and no record of why it is true.
#206 did not create this. #206 made enforcement finally match the declared
state; the state itself is now the weak link. This is the correct direction of
failure — each fix exposes the next hidden assumption instead of pushing the bug
sideways.
Evidence (live robot, 2026-08-18)
spark_quiet_mode: true right now. Its origin is not merely unrecorded, it is
unrecoverable:
spark_quiet_mode : True
history entries : 100 (limit 100)
window covered : 2026-08-17T23:05:20Z -> 2026-08-18T06:34:54Z
event kinds seen : ['emote','mind','perform','remember','status','voice','wake_transcript','weather']
quiet_start/quiet_end/transition_buffer : 0
update_session(history_limit=100) (state.py:259) is a ring buffer sized in
entries, not time. On this robot 100 entries is 7.5 hours. Any latch that
outlives a busy morning becomes unattributable by construction — before you even
ask whether its writer logged anything.
Attributing the current true to Obi, to a transition, or to a dashboard tap
would be fabrication. It should not be attributed at all.
Three writers, two provenance policies, one-way transitions
| Writer |
Sets |
Clears |
Logs a history entry |
bin/tool-quiet start |
✅ true |
— |
✅ quiet_start |
bin/tool-quiet end |
— |
✅ false |
✅ quiet_end |
bin/tool-transition buffer |
✅ true |
never |
✅ transition_buffer |
PATCH /api/v1/session (dashboard toggle) |
✅ either |
✅ either |
❌ none |
patch_session() (api.py:1799) passes fields to update_session() and omits
history_entry entirely, so the dashboard's toggleQuiet() (api.py:2835)
mutates the safety-critical flag and leaves no trace at all. history_entry
being optional is the mechanism: invisibility is the default for any new writer.
tool-transition buffer is the sharper problem. Its own comment calls it a
"20-minute low-demand buffer", but the flag it sets is unbounded and the tool
has no counterpart that clears it. The only exit is tool_quiet end, a
dispatched action — and mind.expression()'s action vocabulary has no quiet
verb, so the autonomous loop can never propose it. A transition buffer creates
a permanent mute that only a human can lift, and nothing in the state says so.
Proposed shape
Stop treating quiet mode as a naked boolean. Store a record; derive the boolean.
{
"enabled": true,
"set_at": "2026-08-18T...",
"set_by": "dashboard|tool-quiet|tool-transition|...",
"reason": "manual|dysregulation|transition|...",
"expires_at": null
}
Deterministic code derives spark_quiet_mode == true for compatibility — every
existing reader (policy.py:117, policy_context, bin/tool-voice,
voice_loop, mind) keeps working unchanged.
That buys, at once:
- every transition is attributable;
- dashboard writes stop being invisible;
- temporary quiet can actually be temporary;
- deliberate indefinite quiet remains possible;
tool-transition cannot silently create a permanent mute;
- the admin UI can explain why SPARK is quiet instead of showing a bare toggle;
- future policy can distinguish "manual mute" from "dysregulation protocol"
without guessing — the thing policy.evaluate() currently cannot see.
Acceptance conditions
Explicitly out of scope
Do not change the current live value. There is no evidence the flag is stale —
only evidence that its origin is unknowable. Preserve it; the migration exists to
make the next one explainable, not to relitigate this one.
Relation to the #174 chain
Ordering matters: #207 widens the set of things quiet mode governs, so quiet mode
should be explainable before it governs more.
The state
spark_quiet_modeis the dysregulation protocol — the Three S's, and the onepolicy rule in
pxh.policythat binds both origins (policy.py:117). It isalso a naked boolean with an indefinite lifetime, three writers, asymmetric
transitions and no record of why it is true.
#206 did not create this. #206 made enforcement finally match the declared
state; the state itself is now the weak link. This is the correct direction of
failure — each fix exposes the next hidden assumption instead of pushing the bug
sideways.
Evidence (live robot, 2026-08-18)
spark_quiet_mode: trueright now. Its origin is not merely unrecorded, it isunrecoverable:
update_session(history_limit=100)(state.py:259) is a ring buffer sized inentries, not time. On this robot 100 entries is 7.5 hours. Any latch that
outlives a busy morning becomes unattributable by construction — before you even
ask whether its writer logged anything.
Attributing the current
trueto Obi, to a transition, or to a dashboard tapwould be fabrication. It should not be attributed at all.
Three writers, two provenance policies, one-way transitions
bin/tool-quietstartquiet_startbin/tool-quietendquiet_endbin/tool-transitionbuffertransition_bufferPATCH /api/v1/session(dashboard toggle)patch_session()(api.py:1799) passesfieldstoupdate_session()and omitshistory_entryentirely, so the dashboard'stoggleQuiet()(api.py:2835)mutates the safety-critical flag and leaves no trace at all.
history_entrybeing optional is the mechanism: invisibility is the default for any new writer.
tool-transition bufferis the sharper problem. Its own comment calls it a"20-minute low-demand buffer", but the flag it sets is unbounded and the tool
has no counterpart that clears it. The only exit is
tool_quiet end, adispatched action — and
mind.expression()'s action vocabulary has no quietverb, so the autonomous loop can never propose it. A transition buffer creates
a permanent mute that only a human can lift, and nothing in the state says so.
Proposed shape
Stop treating quiet mode as a naked boolean. Store a record; derive the boolean.
{ "enabled": true, "set_at": "2026-08-18T...", "set_by": "dashboard|tool-quiet|tool-transition|...", "reason": "manual|dysregulation|transition|...", "expires_at": null }Deterministic code derives
spark_quiet_mode == truefor compatibility — everyexisting reader (
policy.py:117,policy_context,bin/tool-voice,voice_loop,mind) keeps working unchanged.That buys, at once:
tool-transitioncannot silently create a permanent mute;without guessing — the thing
policy.evaluate()currently cannot see.Acceptance conditions
tool-transition bufferbecomes bounded (or is made to state that it is not).
PATCH /api/v1/session) and tools converge on the same writer —the API path must stop being the one that logs nothing.
policy.evaluate()'s contract exactly:session.get("spark_quiet_mode") is Truemust keep meaning what it means today,and
SessionRead.available == Falsemust keep failing closed (fix(policy): enforce #174 at the audio sink, then correct the brain prompt #206, fix(state): a self-healed session is not an authoritative read #208).a pre-existing
truemigrates to enabled withsource: unknown, never to aguessed origin, and never silently to
false.(the record lives in the state object, not only in
history).Explicitly out of scope
Do not change the current live value. There is no evidence the flag is stale —
only evidence that its origin is unknowable. Preserve it; the migration exists to
make the next one explainable, not to relitigate this one.
Relation to the #174 chain
Ordering matters: #207 widens the set of things quiet mode governs, so quiet mode
should be explainable before it governs more.