/** * ECS dispatcher adapter. * * Implements the sdk `Dispatcher` interface (submit/cancel) with the launch * machinery the interface seam leaves to this module: * * 1. generation-check the attempt's `attempt_id` + `lease_generation` * (stale generation rejected), * 2. persist the launch intent (clientToken, startedBy, request digest) * BEFORE calling ECS, * 3. call RunTask with a deterministic clientToken derived from * (run_id, attempt_id) and an immutable request digest, * 4. on a lost RunTask response, reconcile the SAME token — list tasks by * startedBy, describe them — before any new attempt; a new attempt is * forbidden while the existing attempt remains unresolved. * * The AWS SDK is never called from tests: the `EcsRunTaskClient` interface is * the seam, `createAwsEcsClient` is the only place the real SDK is imported, * and every test injects a mock client. * * Cross-process fencing requires a store with atomic transactional claims and * transitions. The generic state machine's separate reads and writes alone do * not establish that guarantee. * * Nothing here names a concrete cluster, task definition, subnet, or account: * all infrastructure identifiers come from configuration (R4). */ import type { DispatchResult, Dispatcher } from "../../dispatcher.js"; import type { RunExecutionStore } from "../storage.js"; import type { AttemptRecord, FrozenAdmission } from "../types.js"; import { type RunStateMachine } from "../state-machine.js"; import { type ReceiptService } from "../receipts.js"; import { DescribeTasksCommand, ListTasksCommand, RunTaskCommand, StopTaskCommand } from "@aws-sdk/client-ecs"; /** ClientToken bound: ECS RunTask accepts up to 32 ASCII characters. */ export declare const CLIENT_TOKEN_BYTES = 16; export interface EcsTaskState { taskArn: string; lastStatus: string; /** Set when the task reached a terminal status. */ stopCode?: string | null; exitCode?: number | null; } export interface EcsRunTaskInput { cluster: string; taskDefinition: string; containerName: string; clientToken: string; startedBy: string; launchType: "FARGATE"; cpu: string; memory: string; subnets: string[]; securityGroups: string[]; environment: { name: string; value: string; }[]; } export interface EcsRunTaskResult { taskArn: string; } /** The seam every test mocks; the real implementation lives in createAwsEcsClient. */ export interface EcsRunTaskClient { runTask(input: EcsRunTaskInput): Promise; /** Task arns launched with a given startedBy token. */ listTasksByStartedBy(startedBy: string): Promise; describeTasks(taskArns: string[]): Promise; stopTask(taskArn: string): Promise; } export interface EcsDispatcherConfig { cluster: string; taskDefinition: string; containerName: string; subnets: string[]; securityGroups: string[]; region: string; } export interface EcsDispatcherOptions { store: RunExecutionStore; stateMachine?: RunStateMachine; receipts?: ReceiptService; /** Claim identity; defaults to "dispatcher". */ workerId?: string; now?: () => Date; /** Trusted per-attempt supervisor transport. Must be deterministic for the frozen * admission: a replay keeps the identical ECS clientToken and request. Never * inherited by the skill process. */ supervisorEnvironment?: (admission: FrozenAdmission, attempt: AttemptRecord) => { name: string; value: string; }[]; } export type LaunchOutcome = { kind: "launched"; attemptId: string; taskId: string; } | { kind: "already-launched"; attemptId: string; taskId: string; } | { kind: "previous-terminal"; attemptId: string; } | { kind: "ambiguous"; attemptId: string; } | { kind: "launch-failed-absent"; attemptId: string; } | { kind: "claim-refused"; attemptId: string; reason: string; } | { kind: "no-admission"; } | { kind: "run-terminal"; status: string; }; /** * Deterministic ECS clientToken derived from (run_id, attempt_id). Same input * always yields the same token, so a retried or reconciled launch is * idempotent from ECS's point of view. */ export declare function clientTokenFor(runId: string, attemptId: string): string; /** startedBy token, the durable handle reconciliation lists tasks by. */ export declare function startedByFor(runId: string, attemptNumber: number): string; /** Immutable digest of the frozen request this attempt launches. */ export declare function requestDigestFor(admission: FrozenAdmission, attemptId: string): string; export declare class EcsDispatcher implements Dispatcher { private readonly config; private readonly client; private readonly store; private readonly stateMachine; private readonly receipts; private readonly workerId; private readonly now; private readonly supervisorEnvironment; constructor(config: EcsDispatcherConfig, client: EcsRunTaskClient, options: EcsDispatcherOptions); /** sdk Dispatcher surface: submit an ADMITTED run (execution domain) to the launch machinery. */ submit(run: FrozenAdmission): Promise; /** Confirm the task stopped before recording cancellation and its receipt. */ cancel(runId: string): Promise; /** * Launch the next attempt of a run. * * A previous attempt whose launch outcome is unknown (launching / ambiguous / * launched) is reconciled FIRST. Missing observations remain ambiguous; * terminal observations return to the owner without minting another attempt. */ launchAttempt(runId: string): Promise; /** ECS is eventually consistent: empty lists and missing descriptions cannot * prove absence. Only an exact recognized observation can change launch state. */ reconcile(admission: FrozenAdmission, attempt: AttemptRecord): Promise; private runTaskInput; private cancellationReceipt; private writeCancellationReceipt; } /** Injectable command transport exercises the actual AWS command adapter without network. */ export interface EcsCommandTransport { send(command: RunTaskCommand | ListTasksCommand | DescribeTasksCommand | StopTaskCommand): Promise; } export interface AwsEcsClientOptions { cluster?: string; transport?: EcsCommandTransport; } /** Pass an explicit cluster when restoring an existing named-cluster attempt. * The legacy one-argument factory binds to the first operation's cluster; read * operations before RunTask retain AWS's default-cluster behavior. */ export declare function createAwsEcsClient(region: string, options?: AwsEcsClientOptions): EcsRunTaskClient;