feat(FN-4822): complete Step 8 — document central claim authority

Fusion-Task-Id: FN-4822
Fusion-Task-Lineage: 08cc29e8-114a-48dc-80de-8d7fd2ce0e69
This commit is contained in:
Fusion (runfusion.ai)
2026-05-16 20:47:30 -07:00
committed by gsxdsm
parent 199f317813
commit a11bd0719e
4 changed files with 12 additions and 7 deletions

View File

@@ -485,6 +485,8 @@ Task ownership supports explicit checkout leases. Agents should be aware of:
- If `task.checkedOutBy` is set to another agent, the run exits with `reason: "checkout_conflict"`
- Heartbeat execution does not auto-checkout — callers are responsible for obtaining checkout before starting work
When a `CentralClaimStore` is wired, the authoritative lease owner is the central `taskClaims` row in `~/.fusion/fusion-central.db`; per-project task lease fields are treated as a synchronization mirror of that central result. Without a claim store configured, checkout behavior remains the existing single-node per-project lease flow.
## Per-Agent Heartbeat Configuration
Each agent can override heartbeat behavior via `runtimeConfig`. Key settings:

View File

@@ -27,10 +27,13 @@ Core tables:
- `nodes`
- `peerNodes`
- `settingsSyncState`
- `taskClaims` (authoritative cross-node task checkout claims keyed by `(projectId, taskId)`)
- `__meta`
Per-project task data remains in each repo’s `.fusion/fusion.db`.
`taskClaims` is the central cross-node lease mutex introduced by FN-4819 §2: claim acquisition/renewal/release happen in `~/.fusion/fusion-central.db`, while per-project lease fields mirror the central winner for local scheduler/runtime consumption.
Peer/mesh coordination spans core + engine, with startup ownership in CLI process entrypoints:
- Topology visibility is now cluster-wide from any connected node: dashboard mesh reads aggregate remote local snapshots and dedupe by `nodeId`, with fallback to last-known local mesh state when a peer is temporarily unreachable.

View File

@@ -1432,7 +1432,7 @@ export class AgentStore extends EventEmitter {
const centralResult = await Promise.resolve(this.claimStore.tryClaimTask({
projectId: this.claimProjectId,
taskId,
nodeId: requestNodeId,
nodeId: requestNodeId!,
agentId,
runId: leaseContext?.runId ?? task.checkoutRunId ?? null,
renewedAt: nextRenewedAt,

View File

@@ -55,19 +55,19 @@ describe("cross-node claim mutex integration", () => {
releaseBarrier = resolve;
});
centralDb.tryClaimTask = ((input) => {
centralDb.tryClaimTask = (((input: Parameters<CentralDatabase["tryClaimTask"]>[0]) => {
waiters += 1;
if (waiters === 2) {
releaseBarrier?.();
}
return barrier.then(() => originalTryClaim(input));
}) as CentralDatabase["tryClaimTask"];
}) as unknown) as CentralDatabase["tryClaimTask"];
};
installBarrier();
const [first, second] = await Promise.allSettled([
storeA.checkoutTask(agentA, taskId, { runId: "run-a" }),
storeB.checkoutTask(agentB, taskId, { runId: "run-b" }),
storeA.checkoutTask(agentA, taskId, { nodeId: "node-a", runId: "run-a" }),
storeB.checkoutTask(agentB, taskId, { nodeId: "node-b", runId: "run-b" }),
]);
const fulfilled = [first, second].filter((entry): entry is PromiseFulfilledResult<Awaited<ReturnType<AgentStore["checkoutTask"]>>> => entry.status === "fulfilled");
@@ -87,8 +87,8 @@ describe("cross-node claim mutex integration", () => {
installBarrier();
const [third, fourth] = await Promise.allSettled([
storeA.checkoutTask(agentA, taskId, { runId: "run-c" }),
storeB.checkoutTask(agentB, taskId, { runId: "run-d" }),
storeA.checkoutTask(agentA, taskId, { nodeId: "node-a", runId: "run-c" }),
storeB.checkoutTask(agentB, taskId, { nodeId: "node-b", runId: "run-d" }),
]);
const fulfilled2 = [third, fourth].filter((entry): entry is PromiseFulfilledResult<Awaited<ReturnType<AgentStore["checkoutTask"]>>> => entry.status === "fulfilled");