fix(reconcile): safely resolve worktree bindings whose paths are missing (Closes #970)
This commit is contained in:
@@ -1913,6 +1913,88 @@ class ControlPlaneDB:
|
||||
),
|
||||
)
|
||||
|
||||
def retire_lease_worktree_path(
|
||||
self,
|
||||
lease_id: str,
|
||||
*,
|
||||
expected_path: str | None = None,
|
||||
reason: str = "missing_worktree_path_retired",
|
||||
) -> dict[str, Any]:
|
||||
"""Retire a missing worktree_path binding from a control-plane lease (#970).
|
||||
|
||||
Clears worktree_path on the lease row, updates provenance_json with
|
||||
durable retirement audit proof, and writes a worktree_binding_retired event.
|
||||
Fail closed: if expected_path is provided and does not match current
|
||||
worktree_path, the update is refused to prevent racing mutations.
|
||||
"""
|
||||
now_s = _ts()
|
||||
with self._tx() as conn:
|
||||
cols = self._lease_columns(conn)
|
||||
row = conn.execute(
|
||||
"SELECT * FROM leases WHERE lease_id = ?",
|
||||
(lease_id,),
|
||||
).fetchone()
|
||||
if not row:
|
||||
raise ControlPlaneError(f"unknown lease_id {lease_id}")
|
||||
|
||||
current_wt = (dict(row).get("worktree_path") or "").strip()
|
||||
if expected_path and current_wt and os.path.realpath(current_wt) != os.path.realpath(expected_path):
|
||||
raise ControlPlaneError(
|
||||
f"cannot retire lease {lease_id} worktree_path: expected '{expected_path}' "
|
||||
f"does not match current '{current_wt}' (fail closed)"
|
||||
)
|
||||
|
||||
# Parse and update provenance_json
|
||||
raw_prov = dict(row).get("provenance_json") or "{}"
|
||||
try:
|
||||
prov = json.loads(raw_prov) if isinstance(raw_prov, str) else dict(raw_prov)
|
||||
except Exception:
|
||||
prov = {}
|
||||
if not isinstance(prov, dict):
|
||||
prov = {}
|
||||
|
||||
prior_path = current_wt or prov.get("worktree_path")
|
||||
prov.update({
|
||||
"worktree_path_retired": True,
|
||||
"retired_worktree_path": prior_path,
|
||||
"retired_at": now_s,
|
||||
"retirement_reason": reason,
|
||||
"worktree_path": "",
|
||||
})
|
||||
prov_json = json.dumps(prov)
|
||||
|
||||
if "worktree_path" in cols:
|
||||
conn.execute(
|
||||
"UPDATE leases SET worktree_path = '', provenance_json = ? WHERE lease_id = ?",
|
||||
(prov_json, lease_id),
|
||||
)
|
||||
else:
|
||||
conn.execute(
|
||||
"UPDATE leases SET provenance_json = ? WHERE lease_id = ?",
|
||||
(prov_json, lease_id),
|
||||
)
|
||||
|
||||
conn.execute(
|
||||
"""
|
||||
INSERT INTO events(work_item_id, event_type, message, created_at)
|
||||
VALUES (?, 'worktree_binding_retired', ?, ?)
|
||||
""",
|
||||
(
|
||||
row["work_item_id"],
|
||||
f"lease {lease_id} worktree_path '{prior_path}' retired: {reason}",
|
||||
now_s,
|
||||
),
|
||||
)
|
||||
|
||||
return {
|
||||
"lease_id": lease_id,
|
||||
"retired": True,
|
||||
"prior_worktree_path": prior_path,
|
||||
"retired_at": now_s,
|
||||
"reason": reason,
|
||||
}
|
||||
|
||||
|
||||
def abandon_lease(
|
||||
self,
|
||||
*,
|
||||
@@ -3051,3 +3133,32 @@ class ControlPlaneDB:
|
||||
"live_lease_id": None if live_lease_id is None else str(live_lease_id),
|
||||
"reconcile_action": "reconcile_required" if stale else "safe_to_resume",
|
||||
}
|
||||
|
||||
def retire_session_checkpoint_worktree_path(
|
||||
self,
|
||||
session_id: str,
|
||||
*,
|
||||
checkpoint_id: str | None = None,
|
||||
expected_path: str | None = None,
|
||||
reason: str = "missing_worktree_path_retired",
|
||||
) -> dict[str, Any]:
|
||||
"""Retire a missing worktree_path from session_checkpoints (#970)."""
|
||||
now_s = _ts()
|
||||
with self._tx() as conn:
|
||||
if checkpoint_id:
|
||||
conn.execute(
|
||||
"UPDATE session_checkpoints SET worktree_path = '', updated_at = ? WHERE checkpoint_id = ?",
|
||||
(now_s, checkpoint_id),
|
||||
)
|
||||
elif session_id:
|
||||
conn.execute(
|
||||
"UPDATE session_checkpoints SET worktree_path = '', updated_at = ? WHERE session_id = ?",
|
||||
(now_s, session_id),
|
||||
)
|
||||
return {
|
||||
"session_id": session_id,
|
||||
"checkpoint_id": checkpoint_id,
|
||||
"retired": True,
|
||||
"retired_at": now_s,
|
||||
"reason": reason,
|
||||
}
|
||||
|
||||
Reference in New Issue
Block a user