Two-step debugging session on 2026-06-25 ~19:25Z to clear a stuck EMIT_MAX_FIRES cap on the apps-shortterm-crypto-algo-emit Pattern B container that had been spinning in paused state for hours after the crypto Pattern-A → Pattern-B cutover. The producer’s first hypothesis (JetStream subject-capture failure) was wrong; the real cause was 50 pre-cutover rows in crypto_shortterm.emitted_signals (Pattern A residue) hydrating an in-process guardrail counter at boot, with EMIT_MAX_FIRES=50 as the compile-time default. Companion to crypto-shortterm-emit-guardrails (BLOCKER-3 architecture context) and reinforces [[gcp-terraform-ansible-gotchas|gotcha #38]] (in-process counters only re-read on boot).
For Agents
Two reusable artifacts came out of this session: (a) a recipe for proving NATS stream subject-capture from inside the
natscontainer without admin NKey CLI access, and (b) a confirmation that the BLOCKER-3 hydration semantics in crypto-shortterm-emit-guardrails cut both ways — they make the emitter restart-safe, but they also mean TRUNCATE alone is insufficient to clear the cap. Bouncing the container is mandatory.
Symptom
apps-shortterm-crypto-algo-emit (Pattern B service, image pmv2-crypto-algo:latest @ sha256:9f88f34f, started 2026-06-24T21:17:54Z) had been spinning every ~1s on:
INFO emit: EMIT_MAX_FIRES reached, signal emission paused max_fires=50
for ~22 hours by 2026-06-25 ~19:25Z. Zero JetStream publish attempts in that window. The producer (cryptoshort agent) flagged it via babylon DM #701.
Diagnosis — two steps
Two steps because the producer’s first hypothesis was wrong.
Step 1 (wrong) — JetStream subject-capture hypothesis
Hypothesis: Crypto suspected JetStream subject-capture failure. The thinking was that the NKey publish-permit looked fine but maybe PMV2_ORDERS stream wasn’t actually capturing pmv2.order.crypto_shortterm_latency_test.> (i.e. the publish was succeeding but landing in the void).
Step 2 (right) — falsify subject-capture, find Pattern A residue
Verify subject-capture from inside nats container using the NATS monitoring port (http: 8222, exposed inside the container at /etc/nats/nats.conf):
docker exec apps-nats-nats-1 \
wget -qO- 'http://127.0.0.1:8222/jsz?streams=true&config=true&consumers=false'Result: PMV2_ORDERS stream has subjects: ["pmv2.order.>"] (wildcard). The subject pmv2.order.crypto_shortterm_latency_test.> IS captured by the stream. Hypothesis falsified.
Real cause — three things lined up:
- Pattern-A residue: 50 pre-cutover rows in
crypto_shortterm.emitted_signals(carried over the Pattern A → B cutover; nobody truncated on switchover). - Compile-time default
EMIT_MAX_FIRES=50: the binary DOES read the env var if set, but theapps.ymlshortterm-crypto-algo-emitentry omits it ANDsecrets/crypto-shortterm.envomits it. Default wins. - Boot hydration: per BLOCKER-3, on startup the emitter runs
SELECT … FROM emitted_signalsand seeds atotal_firescounter from the row count. Whentotal_fires >= EMIT_MAX_FIRES, the binary sets paused state and short-circuits BEFORE any JetStream publish.total_fires = 50andEMIT_MAX_FIRES = 50→ paused at boot → never publishes → never increments the row count → never recovers on its own.
Fix — TRUNCATE + container restart
The cap is held in a process-local counter, hydrated from the DB once at boot. So:
Step A — TRUNCATE the table
TRUNCATE TABLE crypto_shortterm.emitted_signals;Producer-DB mutation is outside the standing auth scope
Andras’s #480 standing-auth carve-out covers deploys / env / NATS / redispatch / restart — NOT mutations to producer-owned database state. Even with the producer (
cryptoshort) giving per-task go-ahead via DM, a TRUNCATE needs explicit operator approval. Captured chain:
- Crypto authorized via babylon DM #705: “GO with option A — truncate. Authorized.”
- Operator (Andras) surfaced + approved → executed.
Step B — confirm TRUNCATE alone does NOT clear the cap
After the TRUNCATE landed, we watched the container log for ~40s. It kept spamming EMIT_MAX_FIRES reached, signal emission paused max_fires=50 at ~1Hz. The cap is in-process, not in the DB. The hydration is one-shot at boot.
Step C — bounce the container
sudo systemctl restart apps-shortterm-crypto-algo-emit.serviceAfter restart, the boot log shows:
INFO emit: guardrail hydrated from DB hydrated=0
confirming the reset.
Verification — three signals
Post-restart checks (in priority order):
- DB row growth.
SELECT count(*) FROM crypto_shortterm.emitted_signalsshould advance off 0 on the first signal fire. - JetStream sequence.
PMV2_ORDERSlast_seqfrom/jszshould advance past 155 on the first publish. - Emit log. Confirms publish + PubAck rather than the
EMIT_MAX_FIRES reachedspam.
NATS /jsz recipe — proving stream subject-capture
Recipe — JSM stream config from inside the
natscontainerWhen you need to prove a stream IS (or ISN’T) capturing a subject, and you don’t have admin NKey CLI access, exec into the
natscontainer and hit the monitoring port directly:
docker exec apps-nats-nats-1 wget -qO- \
'http://127.0.0.1:8222/jsz?streams=true&config=true&consumers=false'Returns full JSM stream config including subjects: […]. Looks like:
{
"streams": [{
"name": "PMV2_ORDERS",
"config": {
"subjects": ["pmv2.order.>"]
},
"state": {
"last_seq": 155,
"messages": 155
}
}]
}The subjects array on config is the wildcard set the stream binds to — match your publish subject against it. The state.last_seq is the headline signal for “has anyone published recently” — bump it by triggering a publish and re-query.
Why the in-container monitoring port works without auth
The
http: 8222line in/etc/nats/nats.confexposes the monitoring HTTP endpoints. Inside the container (loopback), no credentials are required. We deliberately do NOT publish:8222outside the container — see [[gcp-terraform-ansible-gotchas|gotcha #37]] for why (-connzleaks client IPs,-subzleaks subject names).
NKey publish-permit check — cohort_publisher
While investigating, verified that the cohort_publisher NKey UCZLZYBWPK4ENZKA7PMFWS6Z7HCYP77NEVGEKZ4QNRH2VCIW4TIOZUSB (the NKey the crypto emitter uses) in /etc/nats/nats.conf has the expected permits — so the path from publish → PubAck delivery is unobstructed and we can rule out [[gcp-terraform-ansible-gotchas|gotcha #56]]-style silent-fail:
| Direction | Subject | Permit |
|---|---|---|
| publish | pmv2.order.crypto_shortterm_latency_test.> | allow |
| publish | $JS.API.> | allow (JetStream API surface) |
| publish | $JS.ACK.> | allow (PubAck delivery) |
| subscribe | _INBOX.> | allow (PubAck reply path) |
This was the second check that ruled out NATS as the cause. With subject-capture verified AND publish permits + ACK reply path verified, the only remaining surface was the emitter’s own guardrail — which is what landed us on the Pattern-A-residue hydration.
Approval chain — for future autopilot tuning
The chain that played out for this task is worth recording because it’s exactly the shape future autopilot/auto-act rules need to handle:
- Producer (
cryptoshort) identifies the issue and proposes Option A (truncate). - Producer per-task go-ahead: babylon DM #705 — “GO with option A — truncate. Authorized.”
- Standing-auth carve-out (Andras #480): covers deploys / env / NATS / redispatch / restart. Producer-DB mutation falls outside this carve-out even with producer’s per-task go-ahead.
- Operator (Andras) approval surfaced + granted → execute.
For future autopilot tuning
If we widen the standing-auth carve-out to include producer-owned database mutations under a producer’s per-task DM authorization, this exact session could have collapsed from 4 steps to 2. The trade-off: producer agents become a vector for unintended DB writes; the deploy/operator agent loses a final review checkpoint. Worth a deliberate decision in a future babylon protocol round.
Related
- crypto-shortterm-emit-guardrails — BLOCKER-3 (the hydration is the fix that made restart-safety possible)
- gcp-terraform-ansible-gotchas — gotcha #38 (process-loaded DB-hydrated counters only re-read on boot)
- crypto-shortterm-phase1-emit — emitter architecture
- babylon-deploy-notes — babylon coordination context (DMs #701 / #705 / standing-auth #480 reference)
- levandor-infra — VM project overview