Skip to content

v1.21.3 — one key, one name: a dotted lease is released under the name it was taken (#25) - #26

Merged
sshlg merged 1 commit into
mainfrom
fix/lease-key-one-name
Oct 1, 2026
Merged

sshlg merged 1 commit into
mainfrom
fix/lease-key-one-name

Conversation

@sshlg

@sshlg sshlg commented Oct 1, 2026

Copy link
Copy Markdown
Contributor

Fixes #25. Under leaseBackend: "git", a lease key containing a dot (T-1.2) was shown and released as T-1-2. release --held printed "released" and the ref stayed on the remote.

Root cause (1.21.2). The key was stored under two different lossy slugs:

  • _ref kept dots: re.sub(r"[^A-Za-z0-9._-]+", "-", key).strip("-") (agent_sync.py:1586).
  • _local_lock turned them into dashes: re.sub(r"[^A-Za-z0-9_-]", "-", key) (:1813).
  • held() returned the lock file's stem (:2588).

release --held then released T-1-2. _git_release found no ref with that name and returned without saying anything (:1763), and release printed released. The same slugs caused three more problems:

  • Every path key (docs/x.md) got stuck the same way.
  • a/b and a-b shared one ref.
  • A key with a leading dot made an invalid ref, and the rejected push was reported as "held by another run".

Fix

  • lease_name / lease_key give the ref and the note one injective name: every byte outside [A-Za-z0-9._-] is percent-escaped, and so is any dot where git forbids one. Both payloads also carry the key verbatim, and that is what held / whoami / residue report.
  • acquire refuses a key that cannot travel through the log, the board and whoami unchanged. It also refuses two spellings that resolve to one note file (T-1 and t-1 on a case-insensitive disk).
  • release asks the remote first, deletes with --force-with-lease, then reads the remote again with a strict check. Only after that does it clear the board claim and the local note. If the remote refused the delete or could not be reached, it prints NOT released, exits 1, and keeps the claim and the note. release --held and merge behave the same way; merge used to print ✓ released no matter what.
  • Migration. Leases left by 1.21.2 stay exclusive and can still be released:
    • A slugged 1.21.2 ref still counts as that key's lease.
    • 1.21.2 notes are re-noted from the remote by whoami/status/release. The guard never does this, so it makes no network call per Edit.
    • The slug 1.21.2 printed (release T-1-2) resolves to that ref, but only for refs 1.21.2 wrote.
    • A 1.21.2 lock held by another run still blocks the dotted key in local mode.
  • Same defect, checked elsewhere:
    • reserve: it slugged register names, so A B and A-B shared one counter. It now refuses a name the ref cannot carry verbatim. Every existing valid name keeps its ref.
    • release-id only writes to the log and never touches a ref, so it was not affected.
    • res-- resource keys round-trip unchanged.

Tests (test/validate.py, all against a real bare origin and confirmed with git ls-remote): 8 new checks. Run against the 1.21.2 script, 7 of them fail with 37 problems. The legacy-exclusion guard passes on 1.21.2 by design. 8 self-test plants put the defect back in different ways (slugged ref, slugged stem, held() reporting the stem, unverified delete, no migration, legacy ref not read, bad key accepted, slugged register), and all 8 are caught. Two existing plants were re-anchored.

Gate (local): npm test EXIT=0: PASS: agent-sync v1.21.3 — all checks green (also on Python 3.9.6), SELF-TEST PASS (74 fixtures), claim cell 27, session hooks 13, installer 11. Audit regressions, evals and the social preview pass. The pinned house audit gives 0 GAP, 18 PASS (body 4749/4750 tokens). claude plugin validate --strict passes for . and plugins/agent-sync.

Still open (backlog AS-14): if a 1.21.2 run and a 1.21.3 run race for the same path key before either ref exists, each pushes its own ref and both win. The fix is to update every machine. Task ids made only of safe characters are not affected.

🤖 Generated with Claude Code

…e it was taken (#25)

Under leaseBackend "git" a key with a dot was reported and released under a
name nothing was taken under. Root cause (1.21.2): the key was stored under
two different lossy slugs — `_ref` kept dots
(re.sub(r"[^A-Za-z0-9._-]+", "-", key).strip("-"), agent_sync.py:1586),
`_local_lock` did not (re.sub(r"[^A-Za-z0-9_-]", "-", key), :1813), and
`held()` returned the lock stem (:2588). `release --held` then released
`T-1-2`, `_git_release` found no such ref and returned silently (:1763), and
`release` printed "released" with the ref still on the remote. Path keys were
stranded the same way, `a/b` and `a-b` shared one ref, and a leading-dot key
could not be pushed (reported as "held by another run").

- lease_name/lease_key: one injective name (percent-escape outside
  [A-Za-z0-9._-], dots git forbids) for ref and note; payloads carry the key
- acquire refuses keys that cannot travel unchanged (empty, whitespace,
  control, backtick, |, >200 bytes stored) and two spellings on one note file
- release asks the remote first, deletes with --force-with-lease, re-reads it
  strictly, and only then clears claim and note; otherwise NOT released, exit 1
  (also release --held and merge, which printed ✓ unconditionally)
- 1.21.2 state stays exclusive and releasable: slugged refs read as the key's
  lease, legacy notes re-noted from the remote (CLI only, never the guard),
  the printed slug resolves to this run's 1.21.2 ref, local legacy locks block
- reserve refuses register names the id ref cannot carry verbatim (A B and
  A-B shared one counter); release-id touches no ref; res-- keys unchanged

Tests: eight new checks against a real bare origin + ls-remote; run against
the 1.21.2 script seven fail with 37 problems. Eight self-test plants (slugged
ref, slugged stem, held() stem, unverified delete, no migration, unread legacy
ref, accepted bad key, slugged register) all caught; two existing plants
re-anchored.

Gate: npm test EXIT=0 — validate PASS v1.21.3 (also Python 3.9.6), SELF-TEST
PASS (74 fixtures), claim cell 27, session hooks 13, installer 11; audit
regressions, evals, social preview pass; pinned house audit 0 GAP / 18 PASS
(body 4749/4750); claude plugin validate --strict passed for . and
plugins/agent-sync.

Open: AS-14 — a 1.21.2 and a 1.21.3 run racing one PATH key with no ref yet
push two refs; update every machine.

Closes #25

Co-Authored-By: Claude Opus 5.5 (1M context) <noreply@anthropic.com>
@sshlg
sshlg merged commit 637424e into main Oct 1, 2026
2 checks passed
@sshlg
sshlg deleted the fix/lease-key-one-name branch October 1, 2026 07:39
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

git lease: a dotted key is reported and released under its sanitized name, leaving the ref on the remote

1 participant