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:
@@ -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;
|
||||
|
||||
Reference in New Issue
Block a user