mcpbeat Sign in

Intent Flag Not Observed State Agent Skill

A component that is committed to running but not yet running reports "not running", so anything that means intent must read the intent flag, not the observed one — and at a transition the intent flag must be waited for with a timeout rather than sampled inline. Use when one client's buffering hiccup stops a whole synchronised group, when appending to a queue in the background silences the track that was about to start, when a resume command is issued on every tick, or when a state read is wrong on exactly the transitions it exists for.

2k tokens
context cost
the whole folder, loaded on every use
1
files
instructions only
0
copies elsewhere
how many repositories repackaged it
100
stars on the repo
on the repository, not the skill itself

Install

one command, takes just this skill from the repository
npx skills add https://github.com/maxrave-dev/kotlin-footguns --skill intent-flag-not-observed-state

The instruction itself

3 sections, as written by the author

Two flags, and only one of them is a decision

Most playing/running components expose both, and they are not two names for one thing:

  • observed (isPlaying, isActive, isRunning) — output is actually happening *right now*.
  • intent (playWhenReady, shouldRun, startRequested) — someone asked for it to happen.

Between the two sits a third state nobody names: committed, not yet producing. Buffering,

resolving a stream, opening a device. Observed is false there, and it is false for a user pause

too — so a comparison against observed cannot tell "the network stuttered" from "the user pressed

pause". In a shared session that difference is the whole feature: publish the first as if it were

the second and one member's hiccup stops everybody.

// adapted — names generalized; the collector's role checks and the send call elided
.map { it.isPlaying }             // collect the observed one: it is the one with a flow
.distinctUntilChanged()
.collect { observed ->
    val intent = component.playWhenReady   // publish the intent one
    if (observed != intent) return@collect // a dip where they disagree is not news
    publish(if (intent) PLAY else PAUSE)
}

Note the shape: you *collect* observed because it is what changes, and you *act on* intent, dropping

the emission entirely whenever the two disagree. Applying works the same way in reverse — compare

the incoming command against the local intent, not against local output, or a still-buffering item

gets a fresh play command on every tick and a stale pause can land on an item that was about to

start.

Traps

Do not sample the intent flag inline at a transition — wait for it. This is the same read, and

it is wrong at exactly the moments it exists for. The intent flag is false while the next item

buffers, and false again for the instant the component is rebuilt for an item picked out of a list.

Both are transitions, and both are when something needs publishing:

// adapted — names generalized
val started = withTimeoutOrNull(SETTLE_TIMEOUT_MS) {
    while (!component.playWhenReady && !component.isPlaying) delay(SETTLE_POLL_MS)
} != null
if (started) publish(PLAY)

The timeout is the negative answer, not an error path. A source that is genuinely paused simply

never satisfies the wait, the publish never fires, and the group stays paused — which is correct.

Treating the timeout as a failure to log or retry inverts that.

Wait for *either* flag, not for the intent alone. Which one flips first depends on the path: on

one the intent commits before any output exists, on another output starts before the intent field is

written. Requiring both is a wait that sometimes never ends.

Do not wait for the observed flag alone. Output waits on stream resolution, which is unbounded —

longer than any timeout you would be willing to hold a transition for. Waiting for audio here is

what leaves followers paused after the source picks a new item.

Poll it; there is usually no flow behind a plain property. Which makes the timeout mandatory

rather than optional: a bare while around a property read is a coroutine parked for the life of

the process, and it pairs with whatever gave up on the other side. retry-needs-backoff-and-cap

states the pairing rule — a bounded retry policy and an unbounded waiter is a hang.

Dedupe the observed stream before you act on it. Components re-emit the same running value

freely — on every buffer, on every internal state change — and without a distinctUntilChanged the

publisher fires on ticks where nothing changed, which is traffic the whole session pays for. Map to

the single boolean first, dedupe, then read the intent inside the collector.

Keep the poll step and the timeout as named constants next to each other. They are one decision:

a 50 ms step under a 2 s ceiling is forty chances to observe a flag that flips once. Tuning either

without seeing the other is how a settle wait becomes either a busy loop or a stall.

The apply side needs the same rule, and it is easier to get wrong. `if (remoteWantsPlay &&

!component.isPlaying) play()` re-issues play on every tick of a buffering item, because the item

stays observed-false the whole time. Compare against the local *intent* and the command fires once.

The same misread happens locally, with no synced group in sight. A guard meant to keep an

already-paused player paused has to read the intent flag too, not just the apply-side command above:

// wrong: also fires while the next item is still preparing
if (!player.isPlaying && isAddToQueue) player.playWhenReady = false

isPlaying is false both while genuinely paused and while the next track is still buffering, so

appending to the queue in the background during that ordinary buffering window reads as "paused" and

writes playWhenReady = false onto a player that was committed to playing — the incoming track then

loads silent. if (!player.playWhenReady && isAddToQueue) is both correct and idempotent:

already-false stays false, and a player mid-buffer with real intent is left alone.

The intent flag is a property, not a stream, and that shapes every use of it. Nothing tells you

when it changes, so there are only two legitimate reads: a sample taken at a moment you know is

settled, and a bounded wait. Anything else — a periodic poll that publishes, a read in a hot path —

is inventing an event source the component does not offer.

The wait stalls the collector it sits in. It is inside a collect block, so for as long as it

runs, later emissions from that same stream queue up behind it. That makes the timeout a bound on

*the whole publish path*, not just on this transition: set it generously and a burst of item changes

arrives at the other members long after the fact. Keep it in the low seconds, and keep the wait as

the last thing the collector does.

"Both flags mean the same thing here" is a claim with a shelf life. Some components genuinely

keep them in step at the moment you check. They stop doing so the first time a slow network, a

device change or a rebuild appears — none of which are in the tests. Read the intent because it is

the question you are asking, not because you measured a divergence.

Verifying it

Find every settle wait and every unbounded poll in one pass — the paired while/delay census:

grep -rn -A3 --include="*.kt" -E "while *\(" . | grep -v "/build/" \
  | awk '/while *\(/{w=$0} /delay\(/{if(w!=""){print w; w=""}}'

Read the enclosing call of each: a poll on a component property with no withTimeout* around it is

the hang, and a while (someDuration <= 0) shape is the same bug wearing a different condition.

Then check that the waits which *do* exist accept either flag:

grep -rn -B2 -A6 "withTimeoutOrNull" --include="*.kt" . | grep -v "/build/" \
  | grep -E "playWhenReady|isPlaying|isActive|shouldRun"

Finally, census the two vocabularies and read each observed-state hit for whether it is answering an

intent question:

grep -rnE "\.(isPlaying|isActive|isRunning)\b" --include="*.kt" . | grep -v "/build/"
grep -rnE "playWhenReady|shouldPlay|startRequested|playbackIntent" --include="*.kt" . | grep -v "/build/"

Behaviourally, the two flags only diverge under conditions you have to create: throttle the network

on the source and confirm the group does *not* pause; then pause on the source and confirm it does.

A build that fails the first and passes the second is reading observed state.

How to use it

Copy the folder

Take maxrave-dev/intent-flag-not-observed-state from the repository into ~/.claude/skills for personal use, or into .claude/skills inside a project.

Check the name does not clash

The agent identifies a skill by the name field in its header. Two skills with the same name cannot sit side by side — one of them will be ignored.