mcpbeat Sign in

Filter Chain Two Owners One Writer Agent Skill

Two independent features want entries in one engine property that holds the WHOLE chain, so writing it replaces everything — keep each feature's entries in its own field, compose them in a single writer, and let "clear" drop only its own tier. Use when a second effect is added beside an existing one, or when one of two effects works and then randomly stops working after a transition.

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 filter-chain-two-owners-one-writer

The instruction itself

3 sections, as written by the author

One property, two owners, one writer

Media engines that expose their processing graph as a string property — a comma-separated

list of stages — hand you an all-or-nothing setter. There is no "append a stage" and no "remove

the stage I added": every write is the entire chain. The moment a second feature wants a stage in

there, direct writes stop being viable.

The shape that survives is one field per owner and exactly one function that touches the property:

// adapted — the engine's filter-chain property is called `chainProperty` here, and the stage
// strings are shortened to `"eq(...)"` / `"sweep(...)"`; the real grammar is engine-specific.

@Volatile private var eqEntry: String? = null          // owner A: null while flat
@Volatile private var transitionEntries: List<String> = emptyList()   // owner B

/**
 * The only place the property is ever written. Owner A first: it shapes what B then acts on.
 * Read-modify-write over two fields, so callers must reach this on the thread that owns the
 * handle — name where that is enforced, because nothing here enforces it.
 */
private fun applyChain(): Boolean {
    val entries = listOfNotNull(eqEntry) + transitionEntries
    return setStringProperty(chainProperty, entries.joinToString(","))
}

fun setEqualizer(bands: List<Float>, preampDb: Float): Boolean {
    eqEntry = if (isFlat(bands, preampDb)) null else "eq(${bands.joinToString(",")};$preampDb)"
    return applyChain()
}

/** Drops owner B's tier only. Blanking the property here is what used to take A down with it. */
fun clearTransitionFilters() {
    transitionEntries = emptyList()
    applyChain()
}

Traps

The bug is silent, intermittent, and blames the wrong feature. The feature that writes the

property directly usually ships first and looks fine forever; the *second* feature is the one that

"works, then randomly doesn't". It stops precisely when the first feature next writes — at the start

or the end of a transition — which from the outside is a timing that correlates with nothing the

user did to the second feature. Anyone bisecting will land on the second feature's commit.

"Clear" is the half everybody forgets. Installing through the composer and then clearing with a

blank write is the same bug wearing a different hat, and it is *worse*: install happens once per

transition, clear happens at the end of every transition, including the ones that were cancelled.

See guard-on-every-trigger-path — a chain owner has the same two-entry-point problem, and covering

only the install path leaves the wipe on the path that runs more often.

An absent tier must contribute no entry, not an empty one. "$a,$b" with a empty yields a

leading comma, and a comma-separated graph parser reads that as a stage with an empty name — which

fails the whole string, so the tier that *did* have entries goes down too:

$ ffmpeg -v error -f f32le -ar 48000 -ac 1 -i imp.raw -af ",volume=2" -f null -
[AVFilterGraph] No such filter: ''
[aost#0:0] Error initializing a simple filtergraph

listOfNotNull(a) + b then joinToString(",") is not tidiness; it is the defense.

Order between tiers is a decision, and it belongs to the writer. Two linear stages compose to

the same response either way, so nothing sounds wrong — but order decides *which stage a boost can

clip in*, and the trim that makes room for a boost has to travel with it. Write the reason down at

the composer, because it is the one place a reader can see both tiers at once.

Label your entries if the engine lets you address them later. Per-stage updates that retune a

live filter in place are far cheaper than rebuilding the graph (the engine typically drains and

re-creates it on every property write), but they need a handle on the stage. Labelling also stops

the two owners from addressing each other's stages by position, which silently breaks the moment

the other tier's length changes.

@Volatile on both fields does not make the composer atomic. A settings screen writes eqEntry

while a transition animation writes transitionEntries — hence @Volatile on both. But

applyChain() *reads both fields and then writes the property*, a read-modify-write: volatile stops

a torn read of either field, not two threads interleaving inside the composer and publishing owner

A's new entry beside owner B's already-cleared list. Confine applyChain() itself to the thread

that owns the handle and the whole read-modify-write becomes atomic — which also closes the

use-after-free where a handle is released between the liveness check and the write. The sample above

calls the composer straight from the caller's thread, so the confinement is the *caller's* contract:

name where it is enforced, at the composer, or the next reader copies the race.

Every freshly created handle starts with an empty chain. The composer covers "two owners on one

handle"; it does nothing for "the same setting on a handle created later". That is the separate

problem in sampled-supplier-vs-per-handle-reapply, and on an engine of this shape you need both.

Guard the composer's return value where a tier is optional. If a stage can be rejected by the

engine (an optional build-time dependency, a codec that is not there), a single boolean "did the

write land" is not enough to know *which* tier survived — see

optional-engine-feature-degrade-in-tiers.

Verifying it

The invariant is "exactly one call site writes the property", so check it as a grep, not as a test:

# every write of the chain property — expect exactly ONE hit, and it must be the composer's own
# line. Resist filtering the composer out: against a block body the filter removes nothing, and
# against a single-expression `fun applyChain() = setStringProperty(…)` it hides the one real hit.
grep -rn --include='*.kt' 'setStringProperty(chainProperty' .

Then drive the interleaving that produced the original bug, in this order, asserting the property

after each step: set owner A → start a transition → cancel it → read the property. A test that

only runs a transition to completion passes against the blank-write version too, because completion

and cancellation reach the clear by different paths.

How to use it

Copy the folder

Take maxrave-dev/filter-chain-two-owners-one-writer 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.