Skip to content

[Bug]: Hard leases treat case-only write-scope aliases as non-overlapping #7243

Open

Description

@JasonBuildAI

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.

  1. Create a promoted hard_lease Goal containing two open Todos with the same task_repository (git:github.com/example/project).
  2. Give agent-a an exclusive lease for src/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
  1. Give agent-b an exclusive lease for src/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.ts

Actual 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

  1. The exclusive write scope is the concurrency safety boundary for multi-agent execution, and this path silently disables that boundary rather than failing visibly.
  2. 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.
  3. 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.
  4. 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.
  5. 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 main and 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.ts and src/./main.ts but 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

  1. Dante-dan commented on Oct 24, 2026

    @Dante-dan
    Contributor

    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.ts and src/main.ts can 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.

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Metadata

Metadata

Assignees

No one assigned

    Labels

    No labels
    No labels

    Type

    No type

    Projects

    No projects

      Milestone

      No milestone

      Relationships

      None yet

      Development

      No branches or pull requests

      Issue actions