@ssheleg/agent-sync 1.19.0 → 1.19.2
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
package/CHANGELOG.md
CHANGED
|
@@ -1,3 +1,60 @@
|
|
|
1
|
+
## [1.19.2] - 2026-09-04
|
|
2
|
+
|
|
3
|
+
### Fixed
|
|
4
|
+
|
|
5
|
+
- **An expired foreign lease printed at every SessionStart for ever, and the block it sat
|
|
6
|
+
in stopped being read.** Measured on the machine that filed the issue: sixteen expired
|
|
7
|
+
leases, none newer than two days, the oldest **thirteen days old** — six printed in full
|
|
8
|
+
plus *"… and 11 more"*, identically, every session for a fortnight. None of it was
|
|
9
|
+
actionable: a foreign lease is reported and deliberately never touched, so the operator
|
|
10
|
+
could not clear a single line of it. A block reprinted verbatim is a block an operator
|
|
11
|
+
learns to skim, **including the lines that would have mattered**.
|
|
12
|
+
|
|
13
|
+
Age splits the list now. A lease expired inside `STALE_DETAIL_SECONDS` (24h) may still
|
|
14
|
+
mean a run is in trouble and keeps its own line; anything older collapses into **one**
|
|
15
|
+
summary — which still **names the keys**, because a summary that hides which leases are
|
|
16
|
+
stuck replaces noise with a different uselessness. Sixteen lines became one.
|
|
17
|
+
|
|
18
|
+
Watched failing before it shipped: with the split removed the new check reports
|
|
19
|
+
*"6 ancient lease(s) still print a line each"* and *"nine ancient leases produced no
|
|
20
|
+
summary line at all, so they vanished"* — the second assertion exists because collapsing
|
|
21
|
+
and hiding are one edit apart.
|
|
22
|
+
|
|
23
|
+
## v1.19.1 — re-acquiring your own lease now refreshes it
|
|
24
|
+
|
|
25
|
+
**`renew` was fixed for exactly this and `acquire` was not, in the same file.**
|
|
26
|
+
`_refresh_lease`'s docstring records the bug it exists to have fixed: *"`renew`
|
|
27
|
+
appended `op=renew` to the RECORD plane — which has not decided a lease since 1.0.0 —
|
|
28
|
+
and touched a throttle file. The lock's own `ts` was written once, by `acquire`. So a
|
|
29
|
+
run holding a lease lost it at TTL while still working."* The repair landed in `renew`.
|
|
30
|
+
The own-lock branch of `acquire` kept doing precisely what that paragraph describes.
|
|
31
|
+
|
|
32
|
+
**Two harms, and the second is the one that bites.** The lock stayed expired, so
|
|
33
|
+
`classify_lock` read it as spent and another run's `acquire` took it over while this one
|
|
34
|
+
believed it held — measured 2026-09-01: a foreign expired lock IS taken over, timestamp
|
|
35
|
+
and host rewritten. And `_touch_renew` resets the marker `renew()` throttles on, so a
|
|
36
|
+
re-acquire **actively suppressed the next real refresh** while refreshing nothing
|
|
37
|
+
itself. That is the mechanism behind a lease expiring three times in one run against a
|
|
38
|
+
450-step CI job.
|
|
39
|
+
|
|
40
|
+
Measured before and after, on a lock aged past its own TTL:
|
|
41
|
+
|
|
42
|
+
```
|
|
43
|
+
old: won mykey (…) ts after re-acquire: 2026-08-29T20:59:04Z still expired
|
|
44
|
+
new: won mykey (…) ts after re-acquire: 2026-09-01T12:38:48Z
|
|
45
|
+
```
|
|
46
|
+
|
|
47
|
+
`check_acquire_refreshes_its_own_lease` is beside `check_renew_extends_the_lease`,
|
|
48
|
+
watched failing against the previous commit with both of its assertions.
|
|
49
|
+
|
|
50
|
+
**One finding retracted in the same run, and it belongs here.** `reap` refuses to clear
|
|
51
|
+
a lock three days past a forty-five-minute TTL when this run's identity is the shared
|
|
52
|
+
fallback, and that reads like a deadlock. It is not: `acquire` takes over an expired
|
|
53
|
+
lock regardless of who wrote it — measured, timestamp and host rewritten — so the
|
|
54
|
+
expired lock blocks nothing, and `reap`'s stricter bar for a *tidying* operation is
|
|
55
|
+
correct as designed. The claim that expiry alone should license a delete was wrong
|
|
56
|
+
about which operation matters.
|
|
57
|
+
|
|
1
58
|
## v1.19.0 — the skill now promises the finish it has always performed
|
|
2
59
|
|
|
3
60
|
Wave-4 close-out of the 2026-08-29 family audit (ASY-10), filed by this
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@ssheleg/agent-sync",
|
|
3
|
-
"version": "1.19.
|
|
3
|
+
"version": "1.19.2",
|
|
4
4
|
"description": "Let concurrent coding agents share one project without colliding — leases with TTL, race-free id reservation, a run journal and a generated board, over a pluggable knowledge cloud.",
|
|
5
5
|
"bin": {
|
|
6
6
|
"agent-sync": "bin/agent-sync.js"
|
|
@@ -2,7 +2,7 @@
|
|
|
2
2
|
"$schema": "https://json.schemastore.org/claude-code-plugin-manifest.json",
|
|
3
3
|
"name": "agent-sync",
|
|
4
4
|
"displayName": "Agent Sync",
|
|
5
|
-
"version": "1.19.
|
|
5
|
+
"version": "1.19.2",
|
|
6
6
|
"description": "Coordination layer for multi-agent repositories — leases with TTL, race-free ID reservation, a run journal, a cross-repo signal feed and a generated board, over a pluggable knowledge cloud.",
|
|
7
7
|
"author": {
|
|
8
8
|
"name": "ssheleg",
|
|
@@ -4,7 +4,7 @@ description: "Use when several coding agents work one repository at the same tim
|
|
|
4
4
|
compatibility: "Requires the task-pipeline skill for its stages (npx sshlg-skills install). Needs python3 3.9+ (stdlib only, HTTP included - nothing to pip install) and bash for the hooks. The knowledge backend is configured per project; with none configured it degrades to git-file leases. Enforcement hooks are Claude Code only - on other agents the same checks run as a self-check."
|
|
5
5
|
license: MIT
|
|
6
6
|
metadata:
|
|
7
|
-
version: "1.19.
|
|
7
|
+
version: "1.19.2"
|
|
8
8
|
author: ssheleg
|
|
9
9
|
---
|
|
10
10
|
|
|
@@ -33,11 +33,16 @@ from datetime import datetime, timezone
|
|
|
33
33
|
from pathlib import Path
|
|
34
34
|
from typing import Any
|
|
35
35
|
|
|
36
|
-
VERSION = "1.19.
|
|
36
|
+
VERSION = "1.19.2"
|
|
37
37
|
|
|
38
38
|
CONFIG_PATH = Path(".claude/agent-sync.json")
|
|
39
39
|
ENV_FILE = Path(".env.agent-sync")
|
|
40
40
|
STATE_DIR = Path(".agent-sync")
|
|
41
|
+
# How recent an expired lease has to be to earn a line of its own. Anything older
|
|
42
|
+
# collapses into one summary that still names the keys. Twenty-four hours is chosen
|
|
43
|
+
# because it is the span in which an expired lease can still mean "a run is in
|
|
44
|
+
# trouble"; beyond it, it means "somebody's machine was closed".
|
|
45
|
+
STALE_DETAIL_SECONDS = 24 * 60 * 60
|
|
41
46
|
GENERATED_MARKER = "<!-- agent-sync:generated"
|
|
42
47
|
MERGE_LOG_MARKER = "<!-- agent-sync:merge-log -->"
|
|
43
48
|
DEFAULT_MERGE_LOG = "docs/MERGES.md"
|
|
@@ -1646,6 +1651,21 @@ class Sync:
|
|
|
1646
1651
|
except (json.JSONDecodeError, OSError):
|
|
1647
1652
|
held = {}
|
|
1648
1653
|
if held.get("run") == self.rid:
|
|
1654
|
+
# MOVE THE LOCK'S OWN `ts`, not just the throttle marker. This branch did
|
|
1655
|
+
# exactly what `_refresh_lease`'s docstring describes as the bug it exists
|
|
1656
|
+
# to have fixed — touch the throttle file and leave the timestamp the lease
|
|
1657
|
+
# is arbitrated by — and the repair landed in `renew` and not here, in the
|
|
1658
|
+
# same file, four minor versions ago.
|
|
1659
|
+
#
|
|
1660
|
+
# Two harms, and the second is the one that bites. The lock stayed expired,
|
|
1661
|
+
# so `classify_lock` read it as spent and another run's `acquire` took it
|
|
1662
|
+
# over while this one believed it held (measured 2026-09-01: a foreign
|
|
1663
|
+
# expired lock IS taken over, timestamp and host rewritten). And
|
|
1664
|
+
# `_touch_renew` resets the marker `renew()` throttles on — so a re-acquire
|
|
1665
|
+
# actively SUPPRESSED the next real refresh, refreshing nothing itself.
|
|
1666
|
+
# That is the mechanism behind a lease expiring three times in one run
|
|
1667
|
+
# against a 450-step CI job on 2026-09-01.
|
|
1668
|
+
self._refresh_lease(key)
|
|
1649
1669
|
self._touch_renew()
|
|
1650
1670
|
return True, self.rid
|
|
1651
1671
|
if time.time() <= parse_iso(held.get("ts", "")) + int(held.get("ttl", self.ttl)):
|
|
@@ -3377,10 +3397,29 @@ def cmd_status(_args: argparse.Namespace) -> int:
|
|
|
3377
3397
|
f"{len(left_alone)} left alone")
|
|
3378
3398
|
print("\n Expired leases still on disk. Nobody holds these: the TTL ended the "
|
|
3379
3399
|
"lease\n and left the file.")
|
|
3380
|
-
|
|
3400
|
+
# Age splits the list, because age is what decides whether a line is news.
|
|
3401
|
+
# A lease that expired an hour ago may still be a run in trouble; one that
|
|
3402
|
+
# expired thirteen days ago is residue nobody will ever clear, and printing
|
|
3403
|
+
# it in full at every SessionStart is how an operator learns to skim the
|
|
3404
|
+
# whole block — including the recent lines that mattered. Measured on the
|
|
3405
|
+
# machine that filed this: sixteen entries, none newer than two days, the
|
|
3406
|
+
# oldest thirteen days old, printed identically every session for a fortnight.
|
|
3407
|
+
fresh = [e for e in stale
|
|
3408
|
+
if e.get("expired_for") is not None and e["expired_for"] < STALE_DETAIL_SECONDS]
|
|
3409
|
+
old_ones = [e for e in stale if e not in fresh]
|
|
3410
|
+
for e in fresh[:6]:
|
|
3381
3411
|
print(f" · {e['key']} [{e['state']}] {e['why']} ({spent(e)})")
|
|
3382
|
-
if len(
|
|
3383
|
-
print(f" · … and {len(
|
|
3412
|
+
if len(fresh) > 6:
|
|
3413
|
+
print(f" · … and {len(fresh) - 6} more expired in the last "
|
|
3414
|
+
f"{since(STALE_DETAIL_SECONDS)}")
|
|
3415
|
+
if old_ones:
|
|
3416
|
+
# One line, and it still names them: a summary that hides WHICH keys
|
|
3417
|
+
# cannot be acted on, and the point of collapsing is to keep the block
|
|
3418
|
+
# readable rather than to stop reporting.
|
|
3419
|
+
keys = ", ".join(e["key"] for e in old_ones[:8])
|
|
3420
|
+
more = f", +{len(old_ones) - 8} more" if len(old_ones) > 8 else ""
|
|
3421
|
+
print(f" · {len(old_ones)} expired over {since(STALE_DETAIL_SECONDS)} ago "
|
|
3422
|
+
f"— {keys}{more}")
|
|
3384
3423
|
if reapable:
|
|
3385
3424
|
print(" Clear what this run owns: agent_sync.py reap")
|
|
3386
3425
|
if left_alone:
|