Skip to content

Commit a613ebe

Browse files
committed
fix: preserve original lease fence recovery after writer cutoff
Signed-off-by: LoopX Agent <337587101+loopx-agent@users.noreply.github.com>
1 parent 66de915 commit a613ebe

14 files changed

Lines changed: 1661 additions & 1866 deletions

‎loopx/control_plane/work_items/task_lease_lifecycle.ts‎

Lines changed: 22 additions & 62 deletions
Original file line numberDiff line numberDiff line change
@@ -50,15 +50,13 @@ import {
5050
taskLeaseDirectory,
5151
taskLeaseLockPath,
5252
taskLeasePath,
53-
utcIsoformat,
5453
type AuthorityFacts,
5554
type LeaseRecord,
5655
type TodoFact,
5756
type TodoFactField,
5857
} from "./task_lease_acquire.ts";
5958
import {
6059
decideTaskLeaseLifecycle,
61-
materializeTaskLeaseLifecycle,
6260
releasedTaskLeaseRecord as releasedLease,
6361
type TaskLeaseLifecycleDecision,
6462
type TaskLeaseLifecycleDecisionInput,
@@ -411,10 +409,10 @@ async function persistFenceReceipt(
411409
): T | null => value !== undefined ? value : fallback ?? null;
412410
const verifyResponse = options.verifyResponse !== undefined
413411
? options.verifyResponse
414-
: base?.verify_response ?? (
415-
state === "held"
416-
? options.response
417-
: state === "closed" &&
412+
: state === "held"
413+
? options.response
414+
: base?.verify_response ?? (
415+
state === "closed" &&
418416
base?.response !== null &&
419417
base?.response !== undefined &&
420418
base.response.action !== "fence_close"
@@ -1469,11 +1467,11 @@ async function fenceVerify(
14691467
// safe to retry the verification under a newly acquired lock only
14701468
// when the same verified generation is still present. A different
14711469
// generation must fail closed and force a fresh fence identity.
1472-
if (currentLease !== null &&
1473-
currentLease.status === "active" &&
1474-
tupleMatches) {
1475-
receipt = null;
1476-
} else {
1470+
// Retain the original receipt as recovery authority; the locked
1471+
// re-read below revalidates this exact active generation.
1472+
if (currentLease === null ||
1473+
currentLease.status !== "active" ||
1474+
!tupleMatches) {
14771475
throw new TaskLeaseLifecycleError(
14781476
"closed task lease fence receipt is stale or has no retryable lease generation",
14791477
"fence_state_invalid",
@@ -1499,7 +1497,10 @@ async function fenceVerify(
14991497
);
15001498
}
15011499
}
1502-
if (receipt === null) rejectLegacyCoordinationExecution(request.goal_id);
1500+
if (receipt === null) {
1501+
await requireLegacyCoordinationPrimaryWriteAllowed(request.runtime_root, request.goal_id);
1502+
rejectLegacyCoordinationExecution(request.goal_id);
1503+
}
15031504
const adoptedLock = await adoptFenceReceiptLock(request, receipt);
15041505
const acquiredFreshLock = adoptedLock === null;
15051506
lock = adoptedLock ?? await acquireFileMutationLock(
@@ -1707,6 +1708,11 @@ async function fenceVerify(
17071708
}
17081709
return replayResponse;
17091710
}
1711+
throw new TaskLeaseLifecycleError(
1712+
"held task lease fence receipt does not match the persisted lease",
1713+
"fence_state_invalid",
1714+
{ goal_id: request.goal_id, todo_id: request.todo_id, lease_path: leasePath },
1715+
);
17101716
}
17111717
if (request.operation === "holder_verify") {
17121718
if (request.authority.handoff_mode !== "hard_lease") {
@@ -1757,60 +1763,14 @@ async function fenceVerify(
17571763

17581764
const explicitFence = request.idempotency_key !== null || request.expected_version !== null;
17591765
const autoAcquire = allowsUserGateAutoAcquire(request, todo);
1760-
if (autoAcquire && !effective && !active) {
1761-
if (!request.owner) {
1762-
throw new TaskLeaseLifecycleError("hard_lease handoff mode auto-acquire requires an attributed actor; provide --agent-id", "handoff_mode_requires_lease", { goal_id: request.goal_id, todo_id: request.todo_id, reason: "missing_actor", lease_path: leasePath });
1763-
}
1764-
const rejection = ownerRejection(todo, request.owner, request.authority.registered_agents);
1765-
if (rejection) throw ownerError(request, rejection, todo);
1766-
const now = utcIsoformat(at);
1767-
lease = {
1768-
schema_version: "task_lease_v0",
1769-
goal_id: request.goal_id,
1770-
todo_id: request.todo_id,
1771-
owner: request.owner,
1772-
idempotency_key: request.idempotency_key ?? `auto-${request.todo_id}`,
1773-
write_scopes: [],
1774-
acquire_ttl_seconds: 2700,
1775-
version: leaseVersion(lease) + 1,
1776-
lease_epoch: leaseEpoch(lease) + 1,
1777-
acquired_at: now,
1778-
updated_at: now,
1779-
expires_at: utcIsoformat(new Date(at.valueOf() + 2700 * 1_000)),
1780-
status: "active",
1781-
};
1782-
await dependencies.beforeWrite?.(lease);
1783-
await revalidateAuthoritySources(request.authority.source_receipts);
1784-
const shadowCapture = await captureLeaseWrite(
1785-
request, rawLease, lease, "task_lease_auto_acquire",
1786-
);
1787-
await atomicWriteJson(leasePath, lease);
1788-
await shadowCapture?.commit();
1789-
const response = fencePayload(
1790-
{ ...request, owner: request.owner },
1791-
lease,
1792-
lock.token,
1793-
true,
1794-
);
1795-
await persistFenceReceipt(request, "held", lock.token, {
1796-
operationId,
1797-
lease,
1798-
response,
1799-
ownerPid: request.owner_pid ?? process.pid,
1800-
fenceOwner: request.owner,
1801-
fenceIdempotencyKey: lease.idempotency_key as string,
1802-
fenceExpectedVersion: leaseVersion(lease),
1803-
fenceExpectedLeaseEpoch: leaseEpoch(lease),
1804-
base: receipt,
1805-
});
1806-
keep = true;
1807-
return attachRuntimeShadowCapture(response, shadowCapture);
1808-
}
1766+
// An acquired receipt records intent, not a committed grant. After the
1767+
// cutoff it can adopt an existing effect, but cannot create a missing one.
1768+
if (autoAcquire && !effective && !active) rejectLegacyCoordinationExecution(request.goal_id);
18091769
// A response can be lost after an auto-acquire write but before the
18101770
// receipt reaches the caller. The durable fence receipt proves that the
18111771
// same execution instance already minted this lease, so recover it under
18121772
// the adopted lock instead of treating the active lease as a divergence.
1813-
const recoveredAutoAcquire = request.operation === "terminal_verify" &&
1773+
const recoveredAutoAcquire = request.operation === "terminal_verify" && effective &&
18141774
receipt?.state !== "closed" &&
18151775
receipt?.fence_idempotency_key === `auto-${request.todo_id}` &&
18161776
receipt?.fence_owner !== null &&

0 commit comments

Comments
 (0)