/** * ClaimService — Manages task claim lifecycle (lease, heartbeat, grace period) * * Agents claim WorkItems from the Task Pool via this service. Each claim has: * - Lease (default 10min): agent must complete or extend before expiry * - Heartbeat (2min interval): agent signals liveness and renews the lease * - Grace period (3min): buffer after lease expiry before forced revocation * - Renewal: the Reconciler renews the lease of a holder it sees working * (recent PTY/API activity), so a claim is revoked only when its holder has * gone quiet — not simply 13 minutes after it was taken * - Max extensions (3): how many times a lease can be renewed * * The Reconciler uses this service to detect expired/grace-exceeded claims. * * @module services/task-pool/claim.service */ import { PoolStorage } from './pool-storage.js'; import { type TaskClaim, type CreateClaimInput } from '../../types/v2/claim.types.js'; /** * Consecutive grace-period revocations (with NO intervening heartbeat) after * which an agent's session is considered HUNG — it keeps claiming work but * never proves liveness. This is the Irissair orchestrator 假死 signature: * claimed hourly, never heartbeated, revoked ~13 min later, repeat. Surfaced * via {@link ClaimService.getHungAgents}. */ export declare const HUNG_SESSION_GRACE_REVOKE_THRESHOLD = 3; /** * Result of a heartbeat operation. */ export interface HeartbeatResult { /** Whether the heartbeat was accepted */ success: boolean; /** Updated claim (if success) */ claim?: TaskClaim; /** Error reason (if failure) */ reason?: string; } /** * Result of a lease extension attempt. */ export interface ExtendLeaseResult { /** Whether the extension was granted */ success: boolean; /** Updated claim (if success) */ claim?: TaskClaim; /** Error reason (if failure) */ reason?: string; } /** * Summary of expired claims found during a scan. */ export interface ExpiredClaimsSummary { /** Claims past lease expiry but within grace period */ expiring: TaskClaim[]; /** Claims past grace period — should be revoked */ graceExceeded: TaskClaim[]; } /** * Manages the lifecycle of task claims in the Task Pool. * * Responsibilities: * - Create claims when agents claim work items * - Process heartbeats from agents to prove liveness * - Extend leases when agents need more time * - Release claims when agents finish or give up * - Revoke claims when grace period is exceeded * - Scan for expired claims (used by Reconciler) * * @example * ```typescript * const claimService = new ClaimService(storage); * * // Agent claims a work item * const claim = await claimService.createClaim({ * workItemId: 'wi-123', * agentId: 'agent-leo', * }); * * // Agent sends heartbeats periodically * await claimService.heartbeat(claim.id, 'agent-leo'); * * // Agent finishes * await claimService.release(claim.id, 'completed'); * ``` */ export declare class ClaimService { private readonly storage; private readonly logger; /** * Per-agent count of consecutive grace-period revocations with NO intervening * heartbeat. Incremented on each grace revoke, reset to 0 on any successful * heartbeat (proof the session is alive). Powers hung-session detection * ({@link getHungAgents}). */ private readonly consecutiveGraceRevokes; constructor(storage: PoolStorage); /** * Creates a new claim for a work item. * * Validates input, checks for duplicate claims, and creates the claim * with default or specified lease parameters. * * @param input - Claim creation parameters * @returns The created TaskClaim * @throws Error if input is invalid or work item already has an active claim */ createClaim(input: CreateClaimInput): Promise; /** * Processes a heartbeat from an agent for a specific claim. * * Renews the lease from now (see {@link renewClaimLease}) and updates * lastHeartbeatAt. The heartbeat is only accepted if the claim is active or * expiring and belongs to the requesting agent. * * Before 2026-09-29 a heartbeat only stamped lastHeartbeatAt: the Reconciler * measures expiry and grace from `leaseExpiresAt`, so a heartbeating agent * was still revoked 13 minutes after it claimed (WI f34f09b0, CE-19). * * @param claimId - The claim ID * @param agentId - The agent sending the heartbeat (must match claim owner) * @returns HeartbeatResult indicating success or failure */ heartbeat(claimId: string, agentId: string): Promise; /** * Renews an active or expiring claim's lease because its holder was seen * working (system-side heartbeat). Used by the Reconciler instead of * revoking a claim whose agent is visibly busy — agents do not run the * heartbeat skill, so without this every claim was revoked 13 minutes after * it was taken, however hard its holder was working on it. * * @param claimId - The claim ID * @param now - Current time in ms (default: Date.now()) * @returns The renewed claim, or undefined when it is missing or already ended */ renewLease(claimId: string, now?: number): Promise; /** * Extends the lease on a claim, giving the agent more time. * * Extension is only allowed if: * - Claim is active * - Extension count < maxExtensions * - Agent owns the claim * * @param claimId - The claim ID * @param agentId - The agent requesting extension (must match claim owner) * @returns ExtendLeaseResult indicating success or failure */ extendLease(claimId: string, agentId: string): Promise; /** * Voluntarily releases a claim (agent finished or gave up). * * @param claimId - The claim ID to release * @param reason - Why the claim is being released * @throws Error if claim not found */ release(claimId: string, reason: string): Promise; /** * Force-revokes a claim (system action when grace period exceeded). * * @param claimId - The claim ID to revoke * @param reason - Why the claim is being revoked * @throws Error if claim not found */ revoke(claimId: string, reason: string): Promise; /** * Consecutive grace-period revocations (with no intervening heartbeat) for an * agent. Resets to 0 on the agent's next successful heartbeat. * * @param agentId - The agent/session id. * @returns The current consecutive grace-revoke count (0 if none). */ getConsecutiveGraceRevokes(agentId: string): number; /** * Agents whose consecutive grace-revoke count is at or above `threshold` — * their sessions keep claiming work but never heartbeat, i.e. they look HUNG. * * @param threshold - Minimum consecutive grace-revokes (default * {@link HUNG_SESSION_GRACE_REVOKE_THRESHOLD}). * @returns Agent ids that currently look hung. */ getHungAgents(threshold?: number): string[]; /** * Clear an agent's consecutive grace-revoke count. Called by the hung-session * watchdog after it triggers a recovery (session restart) so the freshly * restarted session is not immediately re-flagged as hung before it has had a * chance to heartbeat. * * @param agentId - The agent/session id to clear. */ clearGraceRevokes(agentId: string): void; /** * Scans all active claims and categorizes expired ones. * * Returns two lists: * - `expiring`: lease expired but within grace period (should be marked 'expiring') * - `graceExceeded`: past grace period (should be revoked and work item released) * * @param now - Current timestamp in ms (default: Date.now()) * @param gracePeriodMs - Grace period duration (default: DEFAULT_GRACE_PERIOD_MS) * @returns Summary of expired claims */ scanExpiredClaims(now?: number, gracePeriodMs?: number): Promise; /** * Marks claims as 'expiring' status (called by Reconciler fast loop). * * @param claimIds - IDs of claims to mark as expiring */ markExpiring(claimIds: string[]): Promise; /** * Gets an active claim for a specific agent. * * @param agentId - Agent session name * @returns The active claim, or undefined */ getActiveClaimByAgent(agentId: string): Promise; /** * Gets an active claim for a specific work item. * * @param workItemId - The work item ID * @returns The active claim, or undefined */ getActiveClaimByWorkItem(workItemId: string): Promise; /** * Gets all active claims. * * @returns Array of active/expiring claims */ getActiveClaims(): Promise; /** * Gets a claim by ID. * * @param claimId - The claim ID * @returns The claim, or undefined */ getClaimById(claimId: string): Promise; /** * Checks whether an agent's heartbeat is stale (no heartbeat within expected interval). * * @param claim - The claim to check * @param heartbeatIntervalMs - Expected heartbeat interval (default: 2min) * @param toleranceMs - Additional tolerance before considering stale (default: 30s) * @param now - Current timestamp in ms * @returns True if the last heartbeat is older than interval + tolerance */ isHeartbeatStale(claim: TaskClaim, heartbeatIntervalMs?: number, toleranceMs?: number, now?: number): boolean; /** * Flushes pending storage writes to disk. */ flush(): Promise; } //# sourceMappingURL=claim.service.d.ts.map