lumbda/factory/bend-emit-pool.sh
russell@unturf.com 3e47814cf3
factory: heal_orphan_bins nullglob defect — use compgen for inflight check + reducer
shopt -s nullglob (set so outer for-loop tolerates empty *.bin) made
an unmatched .inflight-* glob expand to nothing. Bare "ls >/dev/null"
then succeeded by listing CWD, and our if-branch incorrectly skipped
every orphan whose .inflight-* did not match. Net effect: medium-sized
orphan bins (100 MiB+, valid QECCOPS1 magic, no markers) accumulated
in our queue indefinitely across pool restarts.

Replace with compgen -G which returns success only when our pattern
matches, immune to nullglob.

Adds tests/integration/test-heal-orphan-bins.sh as TCRAUDT reducer
covering our contract: medium-size orphan promotes to .ready,
sub-threshold truncated to .done, bad-magic DLQs to dlq/.

Incident 2026-06-14: 8 vecC-tri-*-w8c.bin orphans (493 MiB each, Jun 12
emit) survived multiple foxhop pool restarts. Live factory queue
visible to operator as a persistent ~8-bin floor that never drained.
2026-06-14 11:47:03 -04:00

507 lines
23 KiB
Bash
Executable file

#!/usr/bin/env bash
# bend-emit-pool.sh — bounded parallel emit pool for circuit-emission cells.
#
# Reads work items from $LUMBDA_QUEUE_DIR/*.lsp. For each item <tag>:
# - skips if <tag>.ready or <tag>.done exists
# - skips if <tag>.emitting marker exists (in-flight)
# - throttles to MAX concurrent emits
# - launches $LUMBDA_EMITTER_CMD <tag>.lsp, expects it to write <tag>.bin
# - touches <tag>.ready when bin size > MIN_BIN_SIZE
# - touches <tag>.done if emit fails or bin stays too small
#
# Exits when no .lsp without a marker remains AND no .emitting markers active.
# Touch $LUMBDA_QUEUE_DIR/STOP to halt new launches gracefully.
#
# Liveness:
# - Writes $LUMBDA_QUEUE_DIR/pool.heartbeat (pid + timestamp) at top of every
# loop iter via lib-heartbeat.sh::heartbeat_touch.
# - On every exit path (STOP, drain timeout, signal trap, EXIT trap)
# writes $LUMBDA_QUEUE_DIR/pool.exit-cause with reason + removes heartbeat.
# - Supervisor reads pool.heartbeat to detect silent death.
#
# Usage: bend-emit-pool.sh [MAX [QUEUE_DIR [MIN_BIN_SIZE_BYTES]]]
# MAX default 6
# QUEUE_DIR default $LUMBDA_QUEUE_DIR
# MIN_BIN_SIZE_BYTES default 268435456 (256 MB — sanity check only).
#
# Self-identifies as "bend-emit-pool" in cmdline for precise pkill targeting.
#
# Env vars consumed: see $LUMBDA_FACTORY_DIR/CONTRACT.md (LUMBDA_QUEUE_DIR,
# LUMBDA_DOMAIN_DIR, LUMBDA_REPO_DIR, LUMBDA_EMITTER_CMD, AUTO_PULL_MASTER,
# AUTO_PULL_TIMEOUT, DISK_MIN_FREE_GB, MEM_MIN_FREE_GB, POOL_MAX_IDLE_LOOPS,
# SLOW_MAX).
set -u
MAX="${1:-6}"
QUEUE_DIR="${2:-${LUMBDA_QUEUE_DIR:-/tmp/lumbda-queue}}"
MIN_BIN_SIZE="${3:-268435456}"
LUMBDA_DOMAIN_DIR="${LUMBDA_DOMAIN_DIR:-}"
LUMBDA_REPO_DIR="${LUMBDA_REPO_DIR:-$HOME/git/lumbda}"
LUMBDA_FACTORY_DIR="${LUMBDA_FACTORY_DIR:-$(dirname "$(readlink -f "$0")")}"
LUMBDA_EMITTER_CMD="${LUMBDA_EMITTER_CMD:?LUMBDA_EMITTER_CMD must be set by consumer (e.g. \"lumbda --fast \$LUMBDA_DOMAIN_DIR/lumbda/emit-stream.lsp\")}"
LIB_HEARTBEAT="${LIB_HEARTBEAT:-$LUMBDA_FACTORY_DIR/lib-heartbeat.sh}"
LIB_TIER="${LIB_TIER:-$LUMBDA_FACTORY_DIR/lib-tier.sh}"
mkdir -p "$QUEUE_DIR"
echo $$ > "$QUEUE_DIR/pool.pid"
trap 'rm -f "$QUEUE_DIR/pool.pid"' EXIT
# Self-tag in cmdline so pkill -f bend-emit-pool only matches THIS script,
# not other bash sessions that happen to mention factory paths.
exec -a bend-emit-pool bash -c '
QUEUE_DIR="'"$QUEUE_DIR"'"
MAX="'"$MAX"'"
MIN_BIN_SIZE="'"$MIN_BIN_SIZE"'"
LUMBDA_DOMAIN_DIR="'"$LUMBDA_DOMAIN_DIR"'"
LUMBDA_REPO_DIR="'"$LUMBDA_REPO_DIR"'"
LUMBDA_EMITTER_CMD="'"$LUMBDA_EMITTER_CMD"'"
LIB_HEARTBEAT="'"$LIB_HEARTBEAT"'"
LIB_TIER="'"$LIB_TIER"'"
# Source liveness library — provides heartbeat_touch / heartbeat_is_alive /
# heartbeat_exit_cause. Path resolved by parent shell before exec so inner
# shell does not need to derive its own dirname.
# shellcheck source=/dev/null
. "$LIB_HEARTBEAT"
# Source bin-size tier classifier — provides tier_of_size / tier_label_dlq.
# Pool uses these to route oversize bins (above TIER_LARGE_MAX) to DLQ at
# finalize time instead of letting them flow downstream to crush the
# dispatcher fleet.
# shellcheck source=/dev/null
. "$LIB_TIER"
echo "[$(date +%H:%M:%S)] bend-emit-pool start MAX=$MAX queue=$QUEUE_DIR"
# Exit-cause writers. Every exit path (drain, STOP, signal) records its
# reason via the library before falling off the loop. EXIT trap acts as
# safety net for unforeseen crashes; the loop body sets a more specific
# reason just before break + EXIT trap then forwards it.
POOL_EXIT_REASON=""
on_exit() {
local r="${POOL_EXIT_REASON:-unknown-exit}"
heartbeat_exit_cause "$QUEUE_DIR" "pool" "$r"
echo "[$(date +%H:%M:%S)] bend-emit-pool exit reason=$r"
}
trap on_exit EXIT
trap "POOL_EXIT_REASON=signal-int; exit 130" INT
trap "POOL_EXIT_REASON=signal-term; exit 143" TERM
# Pull origin/master before consuming a cell. Fail-soft: log a warning
# and return non-zero on failure (caller decides whether to keep emitting
# against checkout we have). Cheap (~1s fetch over LAN). Locked by
# .git/index.lock so concurrent pool restarts serialize cleanly.
pull_master_or_log() {
[ "${AUTO_PULL_MASTER:-1}" = "1" ] || return 0
local out rc
out=$(timeout "${AUTO_PULL_TIMEOUT:-30}" git -C "$LUMBDA_REPO_DIR" pull --ff-only origin master 2>&1)
rc=$?
if [ "$rc" -ne 0 ]; then
echo "[$(date +%H:%M:%S)] PULL-FAIL rc=$rc (substrate may be stale): $(echo "$out" | tr "\n" "|" | cut -c-200)"
return "$rc"
fi
if ! echo "$out" | grep -q "Already up to date"; then
echo "[$(date +%H:%M:%S)] PULL ok: $(echo "$out" | tr "\n" "|" | cut -c-200)"
fi
return 0
}
# Self-healing janitor: clear .emitting markers whose emitter PID is dead.
# Called before every slot-count check. Stale markers can accrue when emits
# get SIGKILLed (OOM, manual cleanup) before they can clean up their own
# marker.
#
# Subshell wrapper around emitter touches .ready (or .done) BEFORE removing
# its own .emitting marker. A brief window exists where emitter has exited
# (PID dead) but parent subshell has not finished tail cleanup. If this
# janitor fires in that window, it must NOT treat emit as failed —
# .ready/.done file proves completion. Likewise if a .bin already exists
# at full size, emit was real and .ready marker may already have been
# consumed by dispatcher.
#
# NOTE: comments inside bash -c body must NOT contain apostrophes — each
# closes outer single-quoted argument early + corrupts parse.
heal_stale_markers() {
for em in "$QUEUE_DIR"/*.emitting; do
[ ! -f "$em" ] && continue
pid=$(cat "$em" 2>/dev/null)
if [ -z "$pid" ] || ! kill -0 "$pid" 2>/dev/null; then
tag=$(basename "$em" .emitting)
# If ANY completion artifact already exists, the emit completed
# before this janitor fired. Silently clear stale .emitting marker
# — no HEAL log, no .done touch, no relaunch.
if [ -f "$QUEUE_DIR/${tag}.ready" ] \
|| [ -f "$QUEUE_DIR/${tag}.done" ]; then
rm -f "$em"
continue
fi
# Orphan-bin recovery: if .bin exists at full size but no .ready/.done,
# subshell wrote .bin then died (SIGTERM mid-cleanup) before touching
# .ready. Promote: bin is real, dispatcher should consume it. Truncated
# bins (< MIN_BIN_SIZE) treated as failure — rm + .done so cell does
# not re-emit endlessly.
if [ -f "$QUEUE_DIR/${tag}.bin" ]; then
sz=$(stat -c %s "$QUEUE_DIR/${tag}.bin" 2>/dev/null || echo 0)
magic=$(head -c 8 "$QUEUE_DIR/${tag}.bin" 2>/dev/null)
if [ "$magic" != "QECCOPS1" ]; then
# Header-rewrite never fired. See finalize() comment for the
# SIGKILL/OOM mechanism.
mkdir -p "$QUEUE_DIR/dlq"
echo 1 > "$QUEUE_DIR/dlq/${tag}.retries"
{
echo "tier=zero-magic bin_bytes=$sz"
echo "reason: emit-interrupted (heal-stale path) — bin header magic missing"
echo "first-8-bytes-hex: $(head -c 8 $QUEUE_DIR/${tag}.bin 2>/dev/null | xxd -p -c 8)"
} > "$QUEUE_DIR/dlq/${tag}.reason"
echo "[$(date +%H:%M:%S)] DLQ orphan-bin-zero-magic $tag sz=$sz"
rm -f "$QUEUE_DIR/${tag}.bin"
touch "$QUEUE_DIR/${tag}.done.fail"
touch "$QUEUE_DIR/${tag}.done"
elif [ "$sz" -ge "$MIN_BIN_SIZE" ]; then
touch "$QUEUE_DIR/${tag}.ready"
echo "[$(date +%H:%M:%S)] RECOVER orphan-bin $tag ($(du -h $QUEUE_DIR/${tag}.bin | cut -f1))"
else
rm -f "$QUEUE_DIR/${tag}.bin"
touch "$QUEUE_DIR/${tag}.done"
echo "[$(date +%H:%M:%S)] HEAL orphan-bin-truncated $tag sz=$sz"
fi
rm -f "$em"
continue
fi
echo "[$(date +%H:%M:%S)] HEAL stale $tag (pid=$pid dead)"
rm -f "$em"
touch "$QUEUE_DIR/${tag}.done"
fi
done
}
# Belt-and-suspenders: standalone scan for orphan .bin files whose .emitting
# marker is already gone (different death path than heal_stale_markers).
# Catches:
# - pool subshell died AFTER .emitting was racy-cleaned but
# BEFORE it touched .ready
# - pool process murdered between unrelated janitor pass + .ready touch
# - any .bin left behind by a manual operator intervention
#
# Same recovery rule as .emitting branch: full-size → .ready, truncated →
# rm + .done so cell does not re-emit forever. Skips bins already in flight
# (.inflight-N) or already complete (.ready / .done / .dispatch.log).
# Idempotent — safe to run every loop iter alongside heal_stale_markers.
heal_orphan_bins() {
shopt -s nullglob
for bin in "$QUEUE_DIR"/*.bin; do
tag=$(basename "$bin" .bin)
# Any in-progress or completion artifact = NOT orphan
[ -f "$QUEUE_DIR/${tag}.ready" ] && continue
[ -f "$QUEUE_DIR/${tag}.done" ] && continue
[ -f "$QUEUE_DIR/${tag}.emitting" ] && continue
[ -f "$QUEUE_DIR/${tag}.dispatch.log" ] && continue
# 2026-06-14 defect fix: with shopt -s nullglob (set at function top
# so the outer for loop tolerates empty *.bin), an unmatched
# .inflight-* glob expands to NOTHING. Bare "ls >/dev/null" then
# succeeds (lists CWD) + the if-branch incorrectly skips the orphan.
# Compgen returns success only when pattern matches, immune to nullglob.
if compgen -G "$QUEUE_DIR/${tag}.inflight-*" >/dev/null; then
continue
fi
sz=$(stat -c %s "$bin" 2>/dev/null || echo 0)
magic=$(head -c 8 "$bin" 2>/dev/null)
if [ "$magic" != "QECCOPS1" ]; then
# Same emit-interrupted class as finalize() + heal_stale_markers.
mkdir -p "$QUEUE_DIR/dlq"
echo 1 > "$QUEUE_DIR/dlq/${tag}.retries"
{
echo "tier=zero-magic bin_bytes=$sz"
echo "reason: emit-interrupted (orphan-bin-scan path) — bin header magic missing"
echo "first-8-bytes-hex: $(head -c 8 $bin 2>/dev/null | xxd -p -c 8)"
} > "$QUEUE_DIR/dlq/${tag}.reason"
echo "[$(date +%H:%M:%S)] DLQ orphan-bin-scan-zero-magic $tag sz=$sz"
rm -f "$bin"
touch "$QUEUE_DIR/${tag}.done.fail"
touch "$QUEUE_DIR/${tag}.done"
elif [ "$sz" -ge "$MIN_BIN_SIZE" ]; then
touch "$QUEUE_DIR/${tag}.ready"
echo "[$(date +%H:%M:%S)] RECOVER orphan-bin-scan $tag ($(du -h $bin | cut -f1))"
else
rm -f "$bin"
touch "$QUEUE_DIR/${tag}.done"
echo "[$(date +%H:%M:%S)] HEAL orphan-bin-scan-truncated $tag sz=$sz"
fi
done
shopt -u nullglob
}
DISK_MIN_FREE_GB="${DISK_MIN_FREE_GB:-30}"
# Memory-free guard. Each emit RSS sits ~3-4 GiB at peak. With MAX=6 emits
# in flight + backend + dispatchers, total memory pressure can climb fast.
# Hold launches when free RAM falls below MEM_MIN_FREE_GB (default 8 GiB).
# Mirrors disk-free pattern: pool HOLDs rather than spawning an
# overcommit-OOM-kill cascade.
MEM_MIN_FREE_GB="${MEM_MIN_FREE_GB:-8}"
# Idle tolerance — pool stays alive while feeder may push more .lsp cells.
# Default 600 (x 5s = 50 min) — long enough to outlast feeder push cadence
# in normal operation. Exit only on STOP marker OR sustained idle past this
# bound.
POOL_MAX_IDLE_LOOPS="${POOL_MAX_IDLE_LOOPS:-600}"
# Substrate freshness — pull origin/master before emitting each cell so a
# backend never consumes a .lsp against stale substrate. Fail-soft: if pull
# fails (local mods, network), log + continue with checkout we have. Cell
# still emits — log entry tells operator "this host ran against potentially
# stale substrate".
AUTO_PULL_MASTER="${AUTO_PULL_MASTER:-1}"
AUTO_PULL_TIMEOUT="${AUTO_PULL_TIMEOUT:-30}"
# Startup orphan sweep — runs BEFORE main loop so any leftover bins from a
# SIGKILL-d previous pool (whose subshell wrappers died before touching
# .ready) get promoted immediately. In-loop heal also runs, but only after
# a full poll cycle (~5s + admission checks), during which the new pool
# could start spawning fresh emits + pushing past orphans. Catching them
# at startup closes that window.
echo "[$(date +%H:%M:%S)] startup orphan scan"
heal_stale_markers
heal_orphan_bins
idle_loops=0
while true; do
# Heartbeat UNCONDITIONALLY at top of loop. Records both "I am alive"
# mtime + "my pid is X" content. Supervisor on this host reads it every
# poll cycle to decide whether to restart us.
heartbeat_touch "$QUEUE_DIR" "pool"
if [ -f "$QUEUE_DIR/STOP" ]; then
echo "[$(date +%H:%M:%S)] STOP seen"
POOL_EXIT_REASON="stop-marker"
break
fi
pull_master_or_log || true
heal_stale_markers
heal_orphan_bins
# Disk-free guard. Emits land 4-6 GiB bins; running near zero free disk
# makes emitter spin on write retries instead of erroring out. Hold
# launches until at least DISK_MIN_FREE_GB available.
free_gb=$(df -BG --output=avail / 2>/dev/null | tail -1 | tr -dc 0-9)
if [ "${free_gb:-0}" -lt "$DISK_MIN_FREE_GB" ]; then
echo "[$(date +%H:%M:%S)] HOLD disk-free=${free_gb}G < ${DISK_MIN_FREE_GB}G, sleeping"
sleep 30
continue
fi
# Memory-free guard. MAX=6 concurrent emits can each climb to ~3-4 GiB
# RSS; with backend + dispatchers also resident, sustained pressure
# approaches host limits. Hold new launches until MemAvailable >=
# MEM_MIN_FREE_GB so an OOM killer never picks a long-running emit
# at the worst moment. Same backoff sleep as disk guard.
free_mem_kb=$(grep ^MemAvailable: /proc/meminfo 2>/dev/null | tr -dc 0-9)
free_mem_gb=$(( ${free_mem_kb:-0} / 1048576 ))
if [ "$free_mem_gb" -lt "$MEM_MIN_FREE_GB" ]; then
echo "[$(date +%H:%M:%S)] HOLD mem-free=${free_mem_gb}G < ${MEM_MIN_FREE_GB}G, sleeping"
sleep 30
continue
fi
# Read autoscaler advisory + apply dynamic MAX cap when RAM/swap pressure
# detected. If advisory says slow_emit=1, cap concurrency at SLOW_MAX
# (default = MAX/2). When pressure clears the advisory flips back to 0
# and the cap restores. MEM_MIN_FREE_GB hold (above) is still the hard
# floor — advisory acts as a graceful intermediate signal.
effective_max="$MAX"
if [ -f "$QUEUE_DIR/emit.advisory" ]; then
slow=$(grep -E '^slow_emit=' "$QUEUE_DIR/emit.advisory" 2>/dev/null | tail -1 | cut -d= -f2)
if [ "${slow:-0}" = "1" ]; then
effective_max="${SLOW_MAX:-$(( MAX / 2 ))}"
[ "$effective_max" -lt 1 ] && effective_max=1
fi
fi
# Find next pending work item
next=""
for f in "$QUEUE_DIR"/*.lsp; do
[ ! -f "$f" ] && continue
tag=$(basename "$f" .lsp)
[ -f "$QUEUE_DIR/${tag}.ready" ] && continue
[ -f "$QUEUE_DIR/${tag}.done" ] && continue
# .done.fail = operator/caller marked cell as skip (incompatible config,
# poison cell, retracted batch). Without this check pool re-emits the
# cell every iter even after the operator explicitly excluded it.
[ -f "$QUEUE_DIR/${tag}.done.fail" ] && continue
[ -f "$QUEUE_DIR/${tag}.emitting" ] && continue
# Dispatcher worker claims .ready by renaming to .inflight-N (atomic).
# If worker dies mid-dispatch (backend crash, kill, OOM) .inflight-N
# stays + no .done written. Treat .inflight-* as downstream-owned and
# do not re-emit. Dispatcher reclaim path runs at bend-dispatcher
# startup + heals stale .inflight-N back to .ready or .done.
# NOTE: comments here must NOT contain apostrophes — each closes outer
# single-quoted bash -c argument early + corrupts parse.
if ls "$QUEUE_DIR/${tag}.inflight-"* >/dev/null 2>&1; then
continue
fi
# Belt-and-suspenders against HEAL race: if a .bin exists at any size,
# an emit has run. Dispatcher consumes .bin + may delete .ready when
# done, leaving no other markers — without this check main loop would
# relaunch the cell after dispatcher consumed it.
[ -f "$QUEUE_DIR/${tag}.bin" ] && continue
next="$f"
break
done
if [ -z "$next" ]; then
# No new items — wait for in-flight or for feeder to push more.
running=$(ls "$QUEUE_DIR"/*.emitting 2>/dev/null | wc -l)
if [ "$running" = "0" ]; then
idle_loops=$((idle_loops + 1))
if [ "$idle_loops" -ge "$POOL_MAX_IDLE_LOOPS" ]; then
echo "[$(date +%H:%M:%S)] pool drained + idle past $POOL_MAX_IDLE_LOOPS loops"
POOL_EXIT_REASON="drain-timeout"
break
fi
fi
sleep 5
continue
fi
idle_loops=0
# Throttle to MAX concurrent (self-healing each check). Heartbeat again
# inside throttle wait — long emits can hold us here for minutes; without
# re-touching heartbeat supervisor would see us as dead.
while true; do
heartbeat_touch "$QUEUE_DIR" "pool"
heal_stale_markers
heal_orphan_bins
running=$(ls "$QUEUE_DIR"/*.emitting 2>/dev/null | wc -l)
# Compare against effective_max (advisory-aware) rather than raw MAX
# so RAM-pressure slow-down actually reduces concurrency. effective_max
# computed once per main-loop iter against latest advisory.
[ "$running" -lt "$effective_max" ] && break
sleep 3
done
tag=$(basename "$next" .lsp)
echo "[$(date +%H:%M:%S)] launch $tag"
# Subshell: traps EXIT/HUP/TERM so partial emit still finalizes (.ready
# or .done) when parent pool dies during hot-reload. SIGHUP explicitly
# ignored — propagated from a dying parent must NOT kill the wrapper
# mid-cleanup. SIGKILL on us still leaks (no trap fires), but startup
# heal_orphan_bins promotes those bins on next pool start.
(
trap "" HUP
bin="$QUEUE_DIR/${tag}.bin"
finalize() {
if [ -f "$bin" ]; then
sz=$(stat -c %s "$bin" 2>/dev/null || echo 0)
# Bin-magic check FIRST. Interrupted emits (OOM-killed mid-write,
# SIGKILL on emitter hang) leave the 16-byte header reserve as
# zero bytes — emit-stream writes 16 zeros, streams ops, then
# seeks back to byte 0 to rewrite QECCOPS1 + n_ops at the end.
# SIGKILL between those steps strands a file with zero magic.
# Downstream backend chokes with opaque "bad magic" → bin-load-fail
# → DLQ with no useful forensic info. Route here with actual cause
# named so operators can trace back to upstream cell hang instead
# of guessing at load_ops_bin internals.
magic=$(head -c 8 "$bin" 2>/dev/null)
if [ "$magic" != "QECCOPS1" ]; then
mkdir -p "$QUEUE_DIR/dlq"
echo 1 > "$QUEUE_DIR/dlq/${tag}.retries"
{
echo "tier=zero-magic bin_bytes=$sz"
echo "reason: emit-interrupted — bin header magic missing or non-QECCOPS1 (likely SIGKILL or OOM mid-emit; emit-stream header rewrite never fired)"
echo "first-8-bytes-hex: $(head -c 8 "$bin" 2>/dev/null | xxd -p -c 8)"
echo "--- last 10 lines of emit.log ---"
tail -10 "$QUEUE_DIR/${tag}.emit.log" 2>/dev/null
} > "$QUEUE_DIR/dlq/${tag}.reason"
echo "[$(date +%H:%M:%S)] DLQ $tag zero-magic sz=$sz ($(printf %.0f $((sz/1024/1024)))MiB) — emit-interrupted"
rm -f "$bin"
touch "$QUEUE_DIR/${tag}.done.fail"
touch "$QUEUE_DIR/${tag}.done"
rm -f "$QUEUE_DIR/${tag}.emitting"
return 0
fi
# 2nd sub-class: GOOD magic + n_ops=0 — emit wrote QECCOPS1 magic +
# streamed ops but SIGKILL hit between stream-end + finalize seek-
# back-to-byte-8 that rewrites n_ops. Detect via n_ops=0
# implausibility check: any in-tier bin should have n_ops > 0;
# if not, finalize never fired.
n_ops_le=$(dd if="$bin" bs=1 count=8 skip=8 2>/dev/null | xxd -p -c 8)
if [ "$n_ops_le" = "0000000000000000" ]; then
mkdir -p "$QUEUE_DIR/dlq"
echo 1 > "$QUEUE_DIR/dlq/${tag}.retries"
{
echo "tier=zero-n_ops bin_bytes=$sz"
echo "reason: emit-finalize-never-fired — QECCOPS1 magic present but n_ops field at byte 8 is zero (SIGKILL between stream-end + emit-stream seek-back; downstream load_ops_bin would fail length-mismatch)"
echo "n_ops_le_hex: $n_ops_le"
echo "--- last 10 lines of emit.log ---"
tail -10 "$QUEUE_DIR/${tag}.emit.log" 2>/dev/null
} > "$QUEUE_DIR/dlq/${tag}.reason"
echo "[$(date +%H:%M:%S)] DLQ $tag zero-n_ops sz=$sz ($(printf %.0f $((sz/1024/1024)))MiB) — emit-finalize-never-fired"
rm -f "$bin"
touch "$QUEUE_DIR/${tag}.done.fail"
touch "$QUEUE_DIR/${tag}.done"
rm -f "$QUEUE_DIR/${tag}.emitting"
return 0
fi
# Tier-classify the bin. Tier "below-min" / "above-max" → DLQ with
# a typed reason. In-band tiers (micro/small/medium/large) get
# promoted to .ready + dispatcher routes through tier-specific
# admission. lib-tier.sh owns boundary policy.
tier=$(tier_of_size "$sz")
dlq_reason=$(tier_label_dlq "$tier")
if [ -n "$dlq_reason" ]; then
mkdir -p "$QUEUE_DIR/dlq"
echo 1 > "$QUEUE_DIR/dlq/${tag}.retries"
{
echo "tier=$tier bin_bytes=$sz"
echo "reason: $dlq_reason"
echo "--- last 10 lines of emit.log ---"
tail -10 "$QUEUE_DIR/${tag}.emit.log" 2>/dev/null
} > "$QUEUE_DIR/dlq/${tag}.reason"
echo "[$(date +%H:%M:%S)] DLQ $tag tier=$tier sz=$sz ($(printf %.0f $((sz/1024/1024)))MiB) — $dlq_reason"
rm -f "$bin"
touch "$QUEUE_DIR/${tag}.done.fail"
touch "$QUEUE_DIR/${tag}.done"
else
# In-tier bin → optional domain-specific sidecar (vorticity, etc),
# then mark .ready. Sidecar lands next to .bin. Read-only
# post-process; emit substrate untouched. Fail-soft: sidecar
# script errors log + continue — bin still promotes to .ready.
# Dispatcher reads .bin, ignores sidecar; operators + analysis
# tooling consume sidecar opportunistically.
vort="$QUEUE_DIR/${tag}.vorticity.tsv"
if [ -n "$LUMBDA_DOMAIN_DIR" ] && [ -x "$LUMBDA_DOMAIN_DIR/scripts/qubit-vorticity.py" ]; then
if "$LUMBDA_DOMAIN_DIR/scripts/qubit-vorticity.py" "$bin" --out "$vort" \
> "$QUEUE_DIR/${tag}.vorticity.log" 2>&1; then
:
else
echo "[$(date +%H:%M:%S)] vorticity $tag FAILED (see ${tag}.vorticity.log) — promoting bin anyway"
fi
fi
touch "$QUEUE_DIR/${tag}.ready"
echo "[$(date +%H:%M:%S)] ready $tag tier=$tier ($(du -h $bin | cut -f1))"
fi
else
# No bin — emitter errored before writing (undefined flag, syntax
# error, load failure, etc). Emit failures stay DETERMINISTIC —
# retry will fail the same way. Route to DLQ for forensics, mark
# .done.fail + .done so pool skips on next iter (no infinite
# relaunch loop).
mkdir -p "$QUEUE_DIR/dlq"
echo 1 > "$QUEUE_DIR/dlq/${tag}.retries"
tail -10 "$QUEUE_DIR/${tag}.emit.log" 2>/dev/null > "$QUEUE_DIR/dlq/${tag}.reason"
echo "[$(date +%H:%M:%S)] DLQ $tag (emit error — see dlq/${tag}.reason)"
touch "$QUEUE_DIR/${tag}.done.fail"
touch "$QUEUE_DIR/${tag}.done"
fi
rm -f "$QUEUE_DIR/${tag}.emitting"
}
trap finalize EXIT
trap "kill \$lpid 2>/dev/null; finalize; exit 130" INT
trap "kill \$lpid 2>/dev/null; finalize; exit 143" TERM
if [ -n "$LUMBDA_DOMAIN_DIR" ]; then
cd "$LUMBDA_DOMAIN_DIR"
fi
PYTHONUNBUFFERED=1 ECDSA_OUT_BIN="$QUEUE_DIR/${tag}.bin" \
$LUMBDA_EMITTER_CMD "$next" \
> "$QUEUE_DIR/${tag}.emit.log" 2>&1 &
lpid=$!
echo "$lpid" > "$QUEUE_DIR/${tag}.emitting"
wait "$lpid"
) &
done
wait
echo "[$(date +%H:%M:%S)] all emits done"
# Loop exited cleanly via STOP / drain; POOL_EXIT_REASON already set.
# EXIT trap calls heartbeat_exit_cause.
'