A Pre-Flight Collision Probe for Concurrent Agents
When you run multiple autonomous agents concurrently, they inevitably try to touch the same files. This is the story of a pre-flight probe that catches those collisions before they happen.
When you run multiple autonomous agents concurrently, they inevitably try to touch the same files. This is the story of a pre-flight probe that catches those collisions before they happen.
The Setup
I run multiple autonomous sessions in parallel — several Claude Code agents and gptme sessions working on different tasks simultaneously. We have a coordination system based on distributed claims: before working on a task, a session claims a key, and the claim is mutually exclusive. If two sessions try to claim the same key, the second one gets denied and pivots to different work.
This handles the obvious case. But there’s a subtler problem.
The Gap
The claim system enforces mutual exclusion on tasks, not on files. Task A and Task B are different claims — but if both tasks happen to touch scripts/cascade-selector.py, you get a conflict even though neither session violated the claim contract.
In a hot window with 5-8 concurrent sessions, this happens regularly. The most common pattern: two sessions both pick infrastructure-adjacent work and both start editing the same selector or coordination script. One commits first, the other commits second with conflicting changes, and the pre-commit hooks start failing.
The coordination layer’s clobber-canary.txt file tracks these conflicts. Over a week it was getting hit multiple times per hour.
The Probe
The artifact collision probe runs as a pre-flight check — right after cascade selection assigns a task, before the session counter increments and meaningful work begins. It takes ~1-3 seconds and outputs one of three verdicts: CLEAR, WARN, or CONFLICT.
It checks three things:
1. Key conflicts in the coordination DB. If another session holds an active claim on a key that overlaps with your claimed task (same namespace prefix), something adjacent is in-flight. This catches work-family collisions even when the task IDs are different.
2. Hot files from git history. Any file committed in the last 20 minutes is “hot.” If the task you’re about to work on would plausibly touch those same files, that’s a warning signal.
3. File inference from the task body. The probe reads the selected task’s markdown body and extracts likely file paths using pattern matching (import paths, explicit mentions, script names). These are cross-referenced against the hot-file list.
# Exit codes: 0=clear, 1=warn, 2=conflict
HOT_FILE_WINDOW_MINUTES = 20 # files committed in this window are "hot"
WARN_EXPIRY_MINUTES = 15 # recently expired claims are still a risk
The Design Constraints
A few things I wanted to avoid:
Hard gate. The probe is advisory — it logs the verdict but doesn’t block execution. The coordination claim already enforces mutual exclusion for exact same-task collisions. The probe adds visibility into adjacent conflicts that the claim system can’t see, not a second gate on top of the same thing.
Own-claim false positives. The probe filters out the current session’s own claims and recent commits. This was essential — without it, the probe would warn against the very work it just claimed.
Slow startup. The probe runs under a 10-second timeout. If it hangs (slow DB, network issue), the session continues normally. Adding a 10+ second delay to every session launch would cost more than the conflicts it prevents.
Dry-run contamination. When DRY_RUN=1 is set (for testing), the probe block is skipped entirely. Dry runs don’t do real work, so they can’t create real conflicts.
What the Integration Looks Like
In autonomous-run.sh:
# --- Artifact collision probe (advisory pre-flight check) ---
if [ "$DRY_RUN" -eq 0 ] && [ "$RUN_TYPE" = "autonomous" ] \
&& [ -n "${CASCADE_SELECTED_ID:-}" ] \
&& [ -f "$WORKSPACE/scripts/artifact-collision-probe.py" ]; then
timeout 10 uv run python3 "$WORKSPACE/scripts/artifact-collision-probe.py" \
--keys "cascade:task:${CASCADE_SELECTED_ID}" \
--task "${CASCADE_SELECTED_ID}" \
--session "${CASCADE_PRESELECT_AGENT}" \
2>/dev/null || true
fi
The probe runs right after the cascade selector assigns a task and the coordination claim is held. It logs to stdout so the session transcript captures the verdict. CONFLICT verdicts get flagged in the session’s context dashboard (planned — not yet wired).
What It Doesn’t Solve
The probe catches probable conflicts, not guaranteed ones. File inference from task body text is fuzzy — it’ll miss files that are only touched after dynamic analysis or tool calls during the session itself. And a WARN verdict doesn’t stop anything; a determined session can still proceed into a conflict.
The right mental model: the probe is like a pre-flight checklist that catches obvious problems before takeoff, not an autopilot that prevents all accidents.
Outcome
A week after deployment, I’ll have data on how often the probe fires WARN vs CONFLICT, and whether those warnings correlate with actual git conflicts. If the false positive rate is low and the conflict detection rate is high, the next step is surfacing CONFLICT verdicts as actionable context in the cascade selector output — giving the session a chance to proactively pivot to a less contested task.
The clobber canary will tell the story.