feat(FN-1253): implement task checkout leasing end-to-end

- Add checkout lease types and conflict error exports, plus DB schema v20 migration for checkedOutBy/checkedOutAt
- Persist checkout lease fields in TaskStore and add AgentStore checkout/release/force-release/get-holder operations
- Add dashboard checkout API routes for acquire/release/force-release/status with explicit 409 conflict and 403 holder enforcement
- Enforce checkout ownership in heartbeat execution with graceful checkout_conflict exits when another agent holds the lease
- Expand core and dashboard test coverage for schema, store behavior, API routes, and leasing workflows, and document leasing behavior in AGENTS.md
This commit is contained in:
gsxdsm
2026-04-08 17:01:44 -07:00
parent e5dc8373b9
commit 2911fe7d44
12 changed files with 655 additions and 19 deletions

View File

@@ -703,6 +703,25 @@ export class HeartbeatMonitor {
return (await this.store.getRunDetail(agentId, run.id))!;
}
// Checkout enforcement: agent must hold the lease to work on this task.
// The heartbeat only validates existing checkout state — it does NOT attempt
// to acquire a checkout itself. The calling system (scheduler, API trigger)
// is responsible for checking out the task before the heartbeat starts.
if (taskDetail.checkedOutBy && taskDetail.checkedOutBy !== agentId) {
heartbeatLog.warn(
`Agent ${agentId} does not hold checkout for ${taskId} (held by ${taskDetail.checkedOutBy}) — graceful exit`
);
await this.completeRun(agentId, run.id, {
status: "completed",
resultJson: {
reason: "checkout_conflict",
taskId,
checkedOutBy: taskDetail.checkedOutBy,
},
});
return (await this.store.getRunDetail(agentId, run.id))!;
}
// Track usage via callbacks
const STDOUT_EXCERPT_LIMIT = 4000;
let outputLength = 0;