Repository navigation
[Bug]: Hard leases treat case-only write-scope aliases as non-overlapping #7243
Description
Impact
Hard-lease admission can grant two active exclusive leases for the same repository file when their write scopes differ only by case, such as src/Main.ts and src/main.ts. On the default case-insensitive Windows and macOS filesystems those spellings name one physical file, so two agents can write concurrently while each believes it holds exclusive authority. The second acquisition should fail with write_scope_conflict before any provider mutation.
This is not limited to one acquisition command or provider. The shared decision is used by both File and SQLite goals, and by standalone task-lease acquire and atomic todo claim. New unconfigured goals default to SQLite with hard_lease (provider-selection contract), so the gap covers the default multi-agent path on supported Windows/macOS hosts.
Problem
The canonical lease contract states: "Host/path case aliases remain overlapping." The adjacent repository identity comparator also deliberately case-folds identities so that "case aliases must not create a second lock on case-insensitive hosts."
The write-scope comparison does neither. comparisonWriteScopes preserves case, and scopePairOverlaps compares the resulting strings and glob patterns case-sensitively. A case-only alias therefore bypasses the exclusive lease check.
Minimal reproduction
Tested at upstream commit 4191b7d966da09a7b0e136353f331e8d5e677dec on native Windows with Python 3.14.3 and Node.js 26.11.1. The same result was observed with both File and SQLite canonical authority.
- Create a promoted
hard_leaseGoal containing two open Todos with the sametask_repository(git:github.com/example/project). - Give
agent-aan exclusive lease forsrc/Main.ts:
loopx --registry registry.json --format json task-lease acquire \
--goal-id scope-contract --todo-id todo_scope_a --owner agent-a \
--idempotency-key case-a --expected-version 0 --ttl-seconds 600 \
--write-scope src/Main.ts- Give
agent-ban exclusive lease forsrc/main.ts:
loopx --registry registry.json --format json task-lease acquire \
--goal-id scope-contract --todo-id todo_scope_b --owner agent-b \
--idempotency-key case-b --expected-version 0 --ttl-seconds 600 \
--write-scope src/main.tsActual result: both commands exit 0. The second response contains status: "applied", action: "acquire", acquired: true, and lease.write_scopes: ["src/main.ts"]; both leases remain active.
Expected result: the second command exits non-zero with error_code: "write_scope_conflict" and the canonical provider is unchanged.
The exported decision reproduces the same behavior directly: evaluateTaskLeaseWriteScopesOverlap({left: ["src/Main.ts"], right: ["src/main.ts"]}) returns {"overlap": false}.
Why this is particularly important
- The exclusive write scope is the concurrency safety boundary for multi-agent execution, and this path silently disables that boundary rather than failing visibly.
- The affected population is the default collaboration configuration: unconfigured Goals create SQLite with
hard_lease, the shared decision also serves File authority, and Windows and macOS use case-insensitive filesystems by default. - The consequence is not a cosmetic false positive. Two agents can concurrently mutate one file under what appears to be valid exclusive authority, creating lost updates or corrupted output.
- The public contract explicitly promises that case aliases overlap; the implementation violates that accepted rule, so callers cannot detect the gap from readback or documentation.
- This is the remaining case-alias member of the same exclusive-scope bypass class addressed for lexical aliases in fix(leases): canonicalize equivalent write scopes #7204. The bug is present on current
mainand has no warning, migration, or conservative fallback that limits exposure.
Additional context
- Adjacent merged fix: fix(leases): canonicalize equivalent write scopes #7204 canonicalized
src//main.tsandsrc/./main.tsbut did not change case sensitivity. - No existing issue or open PR covering case aliases was found.
- A bounded fix can fold case only in the conflict-comparison view; stored scopes and immutable request identity must retain their original spelling.
Activity
Dante-dan commentedon Oct 24, 2026
I’ll work on a bounded fix in the shared write-scope conflict comparison. The current comparison canonicalizes lexical aliases but retains case, so
src/Main.tsandsrc/main.tscan remain disjoint despite the documented alias rule. I’ll keep stored scope spelling and immutable request identity unchanged, and cover case aliases across literal, subtree and glob comparisons plus the shared acquisition paths. I’ll distinguish repository test results from the native Windows reproduction reported here.