{"version":3,"file":"scheduler-control-service.d.ts","sourceRoot":"","sources":["../../../src/core/scheduler/scheduler-control-service.ts"],"names":[],"mappings":"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","sourcesContent":["/**\n * Scheduler Control Service (2.12.0).\n *\n * The authoritative application/operator boundary over durable scheduling\n * intent. It consumes the existing primitives — DurableMissionStore for mission\n * lifecycle, ExecutorControlService for executor liveness, and\n * AssignmentControlService for designation — and produces a durable Assignment\n * via a deterministic policy. It never reimplements those primitives.\n *\n *   - `enqueueIntent`  records pending scheduling intent for a mission.\n *   - `cancelIntent`   withdraws a pending/unschedulable intent.\n *   - `previewTick`    read-only deterministic decision preview (no mutation).\n *   - `runTick`        runs one scheduler pass: eligible missions -> assignable\n *                      executors -> deterministic policy -> durable assignment.\n *\n * The output of scheduling is an Assignment. Execution, worker daemons, remote\n * invocation, acceptance, execution start, live migration, and resource\n * reservation are explicitly out of scope and remain separate authority domains.\n */\n\nimport type { AssignmentControlService } from \"../assignment/assignment-control-service.js\";\nimport type { AssignmentRecord } from \"../assignment/assignment-types.js\";\nimport { evaluateAssignability, toAssignabilityStatus } from \"../assignment/compatibility.js\";\nimport type { CapabilityRouteEvaluator, RouteCandidate, RoutingEvaluation } from \"../capability-routing/route-types.js\";\nimport type { ExecutorControlService, ExecutorSummary } from \"../executor-registry/index.js\";\nimport {\n\ttype DurableMissionRecord,\n\ttype DurableMissionStore,\n\tisSafeMissionId,\n} from \"../mission-domain/durable-store.js\";\nimport { isExecutionLeaseActive } from \"../mission-domain/execution-lease.js\";\nimport { isTerminalMissionState } from \"../mission-domain/mission-state.js\";\nimport { chooseExecutor, type ExecutorCandidate, orderPendingIntents } from \"./policy.js\";\nimport { createSchedulingIntentRecord, type SchedulingIntentStore } from \"./scheduler-store.js\";\nimport {\n\tDEFAULT_SCHEDULING_POLICY,\n\ttype EnqueueSchedulingIntentInput,\n\ttype EnqueueSchedulingIntentOutcome,\n\ttype EnqueueSchedulingIntentStatus,\n\ttype ExecutorEligibility,\n\tintentIdForMission,\n\tisSafeIntentId,\n\tnewSchedulingTickId,\n\ttype SchedulingDecision,\n\tSchedulingError,\n\ttype SchedulingIntentDetail,\n\ttype SchedulingIntentListOptions,\n\ttype SchedulingIntentListResult,\n\ttype SchedulingIntentRecord,\n\ttype SchedulingIntentSummary,\n\ttype SchedulingPolicy,\n\ttype SchedulingTickResult,\n} from \"./scheduler-types.js\";\n\nexport interface SchedulerControlServiceOptions {\n\tstore: SchedulingIntentStore;\n\tmissions: DurableMissionStore;\n\texecutors: ExecutorControlService;\n\tassignments: AssignmentControlService;\n\tpolicy?: SchedulingPolicy;\n\tnow?: () => number;\n\t/** Intent id factory; default is the deterministic per-mission id. */\n\tintentIdFactory?: (missionId: string) => string;\n\ttickIdFactory?: () => string;\n\t/**\n\t * Optional Capability Router. When wired, the scheduler derives executor\n\t * eligibility from the router's route candidates (execution mode, remote\n\t * target health) instead of bare executor capability+ONLINE matching. The\n\t * router reports feasibility; the scheduler still selects among eligible\n\t * candidates via policy and creates the durable Assignment.\n\t */\n\trouter?: CapabilityRouteEvaluator;\n}\n\nexport class SchedulerControlService {\n\tprivate readonly _store: SchedulingIntentStore;\n\tprivate readonly _missions: DurableMissionStore;\n\tprivate readonly _executors: ExecutorControlService;\n\tprivate readonly _assignments: AssignmentControlService;\n\tprivate readonly _policy: SchedulingPolicy;\n\tprivate readonly _now: () => number;\n\tprivate readonly _intentIdFactory: (missionId: string) => string;\n\tprivate readonly _tickIdFactory: () => string;\n\tprivate readonly _router?: CapabilityRouteEvaluator;\n\n\tconstructor(options: SchedulerControlServiceOptions) {\n\t\tthis._store = options.store;\n\t\tthis._missions = options.missions;\n\t\tthis._executors = options.executors;\n\t\tthis._assignments = options.assignments;\n\t\tthis._policy = options.policy ?? DEFAULT_SCHEDULING_POLICY;\n\t\tthis._now = options.now ?? (() => Date.now());\n\t\tthis._intentIdFactory = options.intentIdFactory ?? ((missionId) => intentIdForMission(missionId));\n\t\tthis._tickIdFactory = options.tickIdFactory ?? (() => newSchedulingTickId());\n\t\tthis._router = options.router;\n\t}\n\n\tget store(): SchedulingIntentStore {\n\t\treturn this._store;\n\t}\n\n\t// =========================================================================\n\t// Read model\n\t// =========================================================================\n\n\tasync listIntents(options: SchedulingIntentListOptions = {}): Promise<SchedulingIntentListResult> {\n\t\tconst { records, corrupt } = await this._store.listRecords();\n\n\t\tlet entries = records.map((record) => this._toSummary(record));\n\t\tconst filter = options.filter;\n\t\tif (filter) {\n\t\t\tentries = entries.filter((entry) => {\n\t\t\t\tif (filter.missionId !== undefined && entry.missionId !== filter.missionId) return false;\n\t\t\t\tif (filter.state !== undefined && entry.state !== filter.state) return false;\n\t\t\t\treturn true;\n\t\t\t});\n\t\t}\n\n\t\tconst sort = options.sort ?? \"enqueuedAtMs\";\n\t\tconst direction = options.direction ?? \"asc\";\n\t\tentries.sort((a, b) => {\n\t\t\tlet cmp: number;\n\t\t\tif (sort === \"intentId\") cmp = a.intentId < b.intentId ? -1 : a.intentId > b.intentId ? 1 : 0;\n\t\t\telse cmp = a[sort] - b[sort] || (a.intentId < b.intentId ? -1 : 1);\n\t\t\treturn direction === \"desc\" ? -cmp : cmp;\n\t\t});\n\n\t\tconst offset = options.offset ?? 0;\n\t\tif (offset > 0) entries = entries.slice(offset);\n\t\tif (options.limit !== undefined) entries = entries.slice(0, options.limit);\n\n\t\treturn { entries, corrupt };\n\t}\n\n\tasync getIntent(intentId: string): Promise<SchedulingIntentDetail> {\n\t\tconst record = await this._requireIntent(intentId);\n\t\treturn this._toDetail(record);\n\t}\n\n\tasync getIntentForMission(missionId: string): Promise<SchedulingIntentDetail> {\n\t\treturn this.getIntent(this._intentIdFor(missionId));\n\t}\n\n\t// =========================================================================\n\t// Mutations\n\t// =========================================================================\n\n\tasync enqueueIntent(\n\t\tmissionId: string,\n\t\tinput: EnqueueSchedulingIntentInput = {},\n\t): Promise<EnqueueSchedulingIntentOutcome> {\n\t\tthis._assertMissionId(missionId);\n\t\tconst intentId = this._intentIdFor(missionId);\n\t\tconst now = input.now ?? this._now();\n\n\t\t// Fail fast on a terminal mission; scheduling a terminal mission is always\n\t\t// unschedulable, so enqueueing it is an operator error rather than a queue\n\t\t// entry that would immediately fail on the next tick.\n\t\tconst mission = await this._requireMission(missionId);\n\t\tif (isTerminalMissionState(mission.state)) {\n\t\t\tthrow new SchedulingError(\"MISSION_TERMINAL\", `Mission ${missionId} is terminal (${mission.state})`, {\n\t\t\t\tmissionId,\n\t\t\t\tstate: mission.state,\n\t\t\t});\n\t\t}\n\n\t\tconst record = createSchedulingIntentRecord({\n\t\t\tintentId,\n\t\t\tmissionId,\n\t\t\trequirements: input.requirements,\n\t\t\tpriority: input.priority ?? 0,\n\t\t\tnow,\n\t\t});\n\n\t\tconst created = await this._store.create(record);\n\t\tif (created.status === \"created\") {\n\t\t\treturn { intentId, missionId, state: \"PENDING\", status: \"created\", record };\n\t\t}\n\n\t\tconst reopened = await this._store.mutate<{\n\t\t\tstatus: EnqueueSchedulingIntentStatus;\n\t\t\trecord: SchedulingIntentRecord;\n\t\t}>(intentId, (current) => {\n\t\t\tif (current.state === \"PENDING\") {\n\t\t\t\treturn { kind: \"noop\", value: { status: \"idempotent\", record: current } };\n\t\t\t}\n\t\t\tif (current.state === \"ASSIGNED\") {\n\t\t\t\tthrow new SchedulingError(\n\t\t\t\t\t\"INTENT_ALREADY_ASSIGNED\",\n\t\t\t\t\t`Mission ${missionId} is already scheduled (${current.assignmentId ?? \"unknown assignment\"})`,\n\t\t\t\t\t{ missionId, assignmentId: current.assignmentId },\n\t\t\t\t);\n\t\t\t}\n\t\t\tconst next: SchedulingIntentRecord = {\n\t\t\t\t...current,\n\t\t\t\tstate: \"PENDING\",\n\t\t\t\trequirements: input.requirements ?? current.requirements,\n\t\t\t\tpriority: input.priority ?? current.priority,\n\t\t\t\tenqueuedAtMs: now,\n\t\t\t\tupdatedAtMs: now,\n\t\t\t\tassignmentId: undefined,\n\t\t\t\tunschedulableReason: undefined,\n\t\t\t\trevision: current.revision + 1,\n\t\t\t};\n\t\t\treturn { kind: \"write\", next, value: { status: \"reopened\", record: next } };\n\t\t});\n\t\tconst outcome = this._unwrapMutation(reopened, intentId);\n\t\treturn {\n\t\t\tintentId,\n\t\t\tmissionId,\n\t\t\tstate: outcome.record.state,\n\t\t\tstatus: outcome.status,\n\t\t\trecord: outcome.record,\n\t\t};\n\t}\n\n\tasync cancelIntent(missionId: string): Promise<SchedulingIntentRecord> {\n\t\tthis._assertMissionId(missionId);\n\t\tconst intentId = this._intentIdFor(missionId);\n\t\tconst now = this._now();\n\n\t\tconst assignedCurrent = await this._assignments.getCurrentForMission(missionId);\n\t\tconst result = await this._store.mutate(intentId, (current) => {\n\t\t\tif (current.state === \"CANCELLED\") return { kind: \"noop\", value: current };\n\t\t\tif (current.state === \"ASSIGNED\" && assignedCurrent?.assignmentId === current.assignmentId) {\n\t\t\t\tthrow new SchedulingError(\n\t\t\t\t\t\"INTENT_ALREADY_ASSIGNED\",\n\t\t\t\t\t`Intent ${intentId} is already ASSIGNED; release the assignment instead of cancelling the intent`,\n\t\t\t\t\t{ intentId, assignmentId: current.assignmentId },\n\t\t\t\t);\n\t\t\t}\n\t\t\tconst next: SchedulingIntentRecord = {\n\t\t\t\t...current,\n\t\t\t\tstate: \"CANCELLED\",\n\t\t\t\tupdatedAtMs: now,\n\t\t\t\tunschedulableReason: undefined,\n\t\t\t\tassignmentId: undefined,\n\t\t\t\trevision: current.revision + 1,\n\t\t\t};\n\t\t\treturn { kind: \"write\", next, value: next };\n\t\t});\n\n\t\treturn this._unwrapMutation(result, intentId);\n\t}\n\n\t// =========================================================================\n\t// Scheduler runs\n\t// =========================================================================\n\n\t/** Read-only deterministic decision preview. Never mutates any store. */\n\tasync previewTick(options: { now?: number } = {}): Promise<SchedulingTickResult> {\n\t\treturn this._tick(true, options);\n\t}\n\n\t/** Run one scheduler pass and persist decisions. */\n\tasync runTick(options: { now?: number } = {}): Promise<SchedulingTickResult> {\n\t\treturn this._tick(false, options);\n\t}\n\n\t// =========================================================================\n\t// Internals\n\t// =========================================================================\n\n\tprivate async _tick(dryRun: boolean, options: { now?: number }): Promise<SchedulingTickResult> {\n\t\tconst startedAtMs = options.now ?? this._now();\n\t\tconst runId = this._tickIdFactory();\n\n\t\tconst { records: allIntents, corrupt } = await this._store.listRecords();\n\t\tconst pending = orderPendingIntents(allIntents.filter((intent) => intent.state === \"PENDING\"));\n\n\t\tconst executorList = await this._executors.listExecutors();\n\t\tconst executors = [...executorList.entries].sort((a, b) =>\n\t\t\ta.executorId < b.executorId ? -1 : a.executorId > b.executorId ? 1 : 0,\n\t\t);\n\n\t\tconst currentAssignments = await this._assignments.listCurrentAssignments();\n\t\tconst counts = new Map<string, number>();\n\t\tfor (const record of currentAssignments.values()) {\n\t\t\tcounts.set(record.executorId, (counts.get(record.executorId) ?? 0) + 1);\n\t\t}\n\n\t\tconst decisions: SchedulingDecision[] = [];\n\t\tlet assignmentsCreated = 0;\n\t\tlet intentsAssigned = 0;\n\t\tlet intentsUnschedulable = 0;\n\n\t\tfor (const intent of pending) {\n\t\t\tlet decision = await this._decideForIntent(intent, executors, currentAssignments, counts);\n\n\t\t\tif (decision.decision === \"RECONCILE\") {\n\t\t\t\tif (!dryRun) await this._markAssigned(intent.intentId, decision.assignmentId ?? \"\");\n\t\t\t\tdecisions.push(decision);\n\t\t\t\tintentsAssigned++;\n\t\t\t\tcontinue;\n\t\t\t}\n\n\t\t\tif (decision.decision === \"UNSCHEDULABLE\") {\n\t\t\t\tif (!dryRun) await this._markUnschedulable(intent.intentId, decision.reason ?? \"unschedulable\");\n\t\t\t\tdecisions.push(decision);\n\t\t\t\tintentsUnschedulable++;\n\t\t\t\tcontinue;\n\t\t\t}\n\n\t\t\t// ASSIGN\n\t\t\tif (dryRun) {\n\t\t\t\tif (decision.executorId) {\n\t\t\t\t\tcounts.set(decision.executorId, (counts.get(decision.executorId) ?? 0) + 1);\n\t\t\t\t}\n\t\t\t\tdecisions.push(decision);\n\t\t\t\tintentsAssigned++;\n\t\t\t\tassignmentsCreated++;\n\t\t\t\tcontinue;\n\t\t\t}\n\n\t\t\ttry {\n\t\t\t\tconst outcome = await this._assignments.assignMission({\n\t\t\t\t\tmissionId: intent.missionId,\n\t\t\t\t\texecutorId: decision.executorId ?? \"\",\n\t\t\t\t\trequirements: intent.requirements,\n\t\t\t\t\tassignedBy: \"scheduler\",\n\t\t\t\t\texecutionMode: decision.executionMode,\n\t\t\t\t\tremoteTargetId: decision.remoteTargetId,\n\t\t\t\t});\n\t\t\t\tif (decision.executorId) {\n\t\t\t\t\tcounts.set(decision.executorId, (counts.get(decision.executorId) ?? 0) + 1);\n\t\t\t\t}\n\t\t\t\tawait this._markAssigned(intent.intentId, outcome.assignmentId);\n\t\t\t\tdecisions.push({ ...decision, assignmentId: outcome.assignmentId });\n\t\t\t\tintentsAssigned++;\n\t\t\t\tassignmentsCreated++;\n\t\t\t} catch (error) {\n\t\t\t\tdecision = await this._reconcileAssignmentRace(intent, error);\n\t\t\t\tif (decision.decision === \"RECONCILE\") {\n\t\t\t\t\tawait this._markAssigned(intent.intentId, decision.assignmentId ?? \"\");\n\t\t\t\t\tdecisions.push(decision);\n\t\t\t\t\tintentsAssigned++;\n\t\t\t\t} else {\n\t\t\t\t\tawait this._markUnschedulable(intent.intentId, decision.reason ?? \"unschedulable\");\n\t\t\t\t\tdecisions.push(decision);\n\t\t\t\t\tintentsUnschedulable++;\n\t\t\t\t}\n\t\t\t}\n\t\t}\n\n\t\treturn {\n\t\t\trunId,\n\t\t\tdryRun,\n\t\t\tpolicy: this._policy,\n\t\t\tstartedAtMs,\n\t\t\tfinishedAtMs: this._now(),\n\t\t\tdecisions,\n\t\t\tassignmentsCreated,\n\t\t\tintentsAssigned,\n\t\t\tintentsUnschedulable,\n\t\t\tcorrupt,\n\t\t};\n\t}\n\n\t/**\n\t * Compute a deterministic decision for one intent without side effects.\n\t * Mission-level unschedulability (missing/corrupt/terminal/active owner)\n\t * is distinguished from executor-level unschedulability (no assignable\n\t * executor).\n\t */\n\tprivate async _decideForIntent(\n\t\tintent: SchedulingIntentRecord,\n\t\texecutors: ExecutorSummary[],\n\t\tcurrentAssignments: Map<string, AssignmentRecord>,\n\t\tcounts: Map<string, number>,\n\t): Promise<SchedulingDecision> {\n\t\tconst mission = await this._missions.load(intent.missionId);\n\t\tif (mission.status === \"missing\") {\n\t\t\treturn {\n\t\t\t\tintentId: intent.intentId,\n\t\t\t\tmissionId: intent.missionId,\n\t\t\t\tdecision: \"UNSCHEDULABLE\",\n\t\t\t\treason: \"mission missing\",\n\t\t\t\teligibility: [],\n\t\t\t};\n\t\t}\n\t\tif (mission.status === \"corrupt\") {\n\t\t\treturn {\n\t\t\t\tintentId: intent.intentId,\n\t\t\t\tmissionId: intent.missionId,\n\t\t\t\tdecision: \"UNSCHEDULABLE\",\n\t\t\t\treason: `mission corrupt: ${mission.diagnostic}`,\n\t\t\t\teligibility: [],\n\t\t\t};\n\t\t}\n\t\tconst record = mission.record;\n\t\tif (isTerminalMissionState(record.state)) {\n\t\t\treturn {\n\t\t\t\tintentId: intent.intentId,\n\t\t\t\tmissionId: intent.missionId,\n\t\t\t\tdecision: \"UNSCHEDULABLE\",\n\t\t\t\treason: `mission terminal (${record.state})`,\n\t\t\t\teligibility: [],\n\t\t\t};\n\t\t}\n\t\tif (record.lease && isExecutionLeaseActive(record.lease, this._now())) {\n\t\t\treturn {\n\t\t\t\tintentId: intent.intentId,\n\t\t\t\tmissionId: intent.missionId,\n\t\t\t\tdecision: \"UNSCHEDULABLE\",\n\t\t\t\treason: \"mission has an active execution owner\",\n\t\t\t\teligibility: [],\n\t\t\t};\n\t\t}\n\n\t\tconst existing = currentAssignments.get(intent.missionId);\n\t\tif (existing) {\n\t\t\treturn {\n\t\t\t\tintentId: intent.intentId,\n\t\t\t\tmissionId: intent.missionId,\n\t\t\t\tdecision: \"RECONCILE\",\n\t\t\t\tassignmentId: existing.assignmentId,\n\t\t\t\teligibility: [],\n\t\t\t};\n\t\t}\n\n\t\treturn await this._decideExecutor(intent, executors, counts);\n\t}\n\n\tprivate async _decideExecutor(\n\t\tintent: SchedulingIntentRecord,\n\t\texecutors: ExecutorSummary[],\n\t\tcounts: Map<string, number>,\n\t): Promise<SchedulingDecision> {\n\t\tif (this._router) return this._decideExecutorRouted(intent, counts);\n\t\treturn this._decideExecutorLocal(intent, executors, counts);\n\t}\n\n\t/**\n\t * Routed executor decision: eligibility comes from the Capability Router's\n\t * route candidates (execution mode + remote target health are honored), while\n\t * the deterministic Scheduler policy still selects among eligible candidates.\n\t */\n\tprivate async _decideExecutorRouted(\n\t\tintent: SchedulingIntentRecord,\n\t\tcounts: Map<string, number>,\n\t): Promise<SchedulingDecision> {\n\t\tconst evaluation = await this._router!.evaluate({\n\t\t\trequirements: intent.requirements ?? {},\n\t\t\tmissionId: intent.missionId,\n\t\t});\n\t\tconst byId = new Map<string, RouteCandidate>(\n\t\t\tevaluation.candidates.map((candidate) => [candidate.executorId, candidate]),\n\t\t);\n\n\t\tconst eligibility: ExecutorEligibility[] = [];\n\t\tconst candidates: ExecutorCandidate[] = [];\n\n\t\tfor (const candidate of evaluation.candidates) {\n\t\t\teligibility.push({\n\t\t\t\texecutorId: candidate.executorId,\n\t\t\t\tstatus: candidate.workerStatus,\n\t\t\t\tcompatible: candidate.capabilityMatch,\n\t\t\t\tassignable: candidate.eligible,\n\t\t\t\tchosen: false,\n\t\t\t\treason: candidate.eligible ? undefined : candidate.rejectionReasons.join(\"; \"),\n\t\t\t\texecutionMode: candidate.executionMode,\n\t\t\t\tremoteTargetId: candidate.remoteTargetId,\n\t\t\t\ttargetHealth: candidate.targetHealth,\n\t\t\t\troutabilityStatus: candidate.status,\n\t\t\t\tpreferenceScore: candidate.preferenceScore,\n\t\t\t\tpreferenceReasons: candidate.preferenceReasons,\n\t\t\t});\n\t\t\tif (candidate.eligible) {\n\t\t\t\tcandidates.push({\n\t\t\t\t\texecutorId: candidate.executorId,\n\t\t\t\t\tstatus: candidate.workerStatus,\n\t\t\t\t\tassignability: {\n\t\t\t\t\t\tcompatible: candidate.capabilityMatch,\n\t\t\t\t\t\tassignable: candidate.eligible,\n\t\t\t\t\t\tstatus: toAssignabilityStatus(candidate.workerStatus),\n\t\t\t\t\t\tcompatibility: {\n\t\t\t\t\t\t\tcompatible: candidate.capabilityMatch,\n\t\t\t\t\t\t\tsatisfied: candidate.matchedRequirements,\n\t\t\t\t\t\t\tunsatisfied: candidate.rejectedRequirements,\n\t\t\t\t\t\t\twarnings: [],\n\t\t\t\t\t\t},\n\t\t\t\t\t},\n\t\t\t\t\tcurrentAssignmentCount: counts.get(candidate.executorId) ?? 0,\n\t\t\t\t});\n\t\t\t}\n\t\t}\n\n\t\tconst chosen = chooseExecutor(candidates, this._policy.mode);\n\t\tif (!chosen) {\n\t\t\treturn {\n\t\t\t\tintentId: intent.intentId,\n\t\t\t\tmissionId: intent.missionId,\n\t\t\t\tdecision: \"UNSCHEDULABLE\",\n\t\t\t\treason: this._unschedulableReason(evaluation),\n\t\t\t\teligibility,\n\t\t\t};\n\t\t}\n\n\t\tconst marked = eligibility.map((entry) =>\n\t\t\tentry.executorId === chosen.executorId ? { ...entry, chosen: true } : entry,\n\t\t);\n\t\tconst chosenCandidate = byId.get(chosen.executorId);\n\t\treturn {\n\t\t\tintentId: intent.intentId,\n\t\t\tmissionId: intent.missionId,\n\t\t\tdecision: \"ASSIGN\",\n\t\t\texecutorId: chosen.executorId,\n\t\t\texecutionMode: chosenCandidate?.executionMode,\n\t\t\tremoteTargetId: chosenCandidate?.remoteTargetId,\n\t\t\teligibility: marked,\n\t\t};\n\t}\n\n\tprivate _unschedulableReason(evaluation: RoutingEvaluation): string {\n\t\tif (evaluation.candidates.length === 0) return \"no execution routes available\";\n\t\tconst summary = evaluation.candidates\n\t\t\t.map((candidate) => `${candidate.executorId}=${candidate.status}`)\n\t\t\t.join(\", \");\n\t\treturn `no eligible execution route (${summary})`;\n\t}\n\n\tprivate _decideExecutorLocal(\n\t\tintent: SchedulingIntentRecord,\n\t\texecutors: ExecutorSummary[],\n\t\tcounts: Map<string, number>,\n\t): SchedulingDecision {\n\t\tconst eligibility: ExecutorEligibility[] = [];\n\t\tconst candidates: ExecutorCandidate[] = [];\n\n\t\tfor (const summary of executors) {\n\t\t\tconst assignability = evaluateAssignability({\n\t\t\t\trequirements: intent.requirements,\n\t\t\t\tcapabilities: summary.capabilities,\n\t\t\t\tstatus: toAssignabilityStatus(summary.status),\n\t\t\t\tretired: summary.retired,\n\t\t\t});\n\t\t\teligibility.push({\n\t\t\t\texecutorId: summary.executorId,\n\t\t\t\tstatus: summary.status,\n\t\t\t\tcompatible: assignability.compatible,\n\t\t\t\tassignable: assignability.assignable,\n\t\t\t\tchosen: false,\n\t\t\t\treason: assignability.assignable ? undefined : assignability.reason,\n\t\t\t});\n\t\t\tif (assignability.assignable) {\n\t\t\t\tcandidates.push({\n\t\t\t\t\texecutorId: summary.executorId,\n\t\t\t\t\tstatus: summary.status,\n\t\t\t\t\tassignability,\n\t\t\t\t\tcurrentAssignmentCount: counts.get(summary.executorId) ?? 0,\n\t\t\t\t});\n\t\t\t}\n\t\t}\n\n\t\tconst chosen = chooseExecutor(candidates, this._policy.mode);\n\t\tif (!chosen) {\n\t\t\treturn {\n\t\t\t\tintentId: intent.intentId,\n\t\t\t\tmissionId: intent.missionId,\n\t\t\t\tdecision: \"UNSCHEDULABLE\",\n\t\t\t\treason: `no assignable executor among ${eligibility.length} considered`,\n\t\t\t\teligibility,\n\t\t\t};\n\t\t}\n\n\t\tconst marked = eligibility.map((entry) =>\n\t\t\tentry.executorId === chosen.executorId ? { ...entry, chosen: true } : entry,\n\t\t);\n\t\treturn {\n\t\t\tintentId: intent.intentId,\n\t\t\tmissionId: intent.missionId,\n\t\t\tdecision: \"ASSIGN\",\n\t\t\texecutorId: chosen.executorId,\n\t\t\teligibility: marked,\n\t\t};\n\t}\n\n\t/** After an assign race, re-read the current assignment to reconcile honestly. */\n\tprivate async _reconcileAssignmentRace(intent: SchedulingIntentRecord, error: unknown): Promise<SchedulingDecision> {\n\t\tconst code =\n\t\t\ttypeof error === \"object\" && error !== null && \"code\" in error\n\t\t\t\t? String((error as { code?: unknown }).code)\n\t\t\t\t: undefined;\n\t\tconst current = await this._assignments.getCurrentForMission(intent.missionId);\n\t\tif (current) {\n\t\t\treturn {\n\t\t\t\tintentId: intent.intentId,\n\t\t\t\tmissionId: intent.missionId,\n\t\t\t\tdecision: \"RECONCILE\",\n\t\t\t\tassignmentId: current.assignmentId,\n\t\t\t\teligibility: [],\n\t\t\t};\n\t\t}\n\t\tconst message = error instanceof Error ? error.message : String(error);\n\t\treturn {\n\t\t\tintentId: intent.intentId,\n\t\t\tmissionId: intent.missionId,\n\t\t\tdecision: \"UNSCHEDULABLE\",\n\t\t\treason: `assignment failed${code ? ` (${code})` : \"\"}: ${message}`,\n\t\t\teligibility: [],\n\t\t};\n\t}\n\n\tprivate async _markAssigned(intentId: string, assignmentId: string): Promise<void> {\n\t\tconst now = this._now();\n\t\tconst result = await this._store.mutate(intentId, (current) => {\n\t\t\tif (current.state !== \"PENDING\") return { kind: \"noop\", value: undefined };\n\t\t\tconst next: SchedulingIntentRecord = {\n\t\t\t\t...current,\n\t\t\t\tstate: \"ASSIGNED\",\n\t\t\t\tassignmentId,\n\t\t\t\tunschedulableReason: undefined,\n\t\t\t\tupdatedAtMs: now,\n\t\t\t\trevision: current.revision + 1,\n\t\t\t};\n\t\t\treturn { kind: \"write\", next, value: undefined };\n\t\t});\n\t\tthis._unwrapTransition(result, intentId);\n\t}\n\n\tprivate async _markUnschedulable(intentId: string, reason: string): Promise<void> {\n\t\tconst now = this._now();\n\t\tconst result = await this._store.mutate(intentId, (current) => {\n\t\t\tif (current.state !== \"PENDING\") return { kind: \"noop\", value: undefined };\n\t\t\tconst next: SchedulingIntentRecord = {\n\t\t\t\t...current,\n\t\t\t\tstate: \"UNSCHEDULABLE\",\n\t\t\t\tassignmentId: undefined,\n\t\t\t\tunschedulableReason: reason,\n\t\t\t\tupdatedAtMs: now,\n\t\t\t\trevision: current.revision + 1,\n\t\t\t};\n\t\t\treturn { kind: \"write\", next, value: undefined };\n\t\t});\n\t\tthis._unwrapTransition(result, intentId);\n\t}\n\n\tprivate _unwrapTransition(\n\t\tresult:\n\t\t\t| { status: \"ok\"; value: unknown }\n\t\t\t| { status: \"missing\" }\n\t\t\t| { status: \"corrupt\"; intentId: string; diagnostic: string },\n\t\tintentId: string,\n\t): void {\n\t\t// Missing means the intent was removed concurrently; the transition is\n\t\t// simply skipped. Corrupt is surfaced, never silently folded.\n\t\tif (result.status === \"missing\") return;\n\t\tif (result.status === \"corrupt\") {\n\t\t\tthrow new SchedulingError(\"INTENT_CORRUPT\", `Intent ${intentId} is corrupt: ${result.diagnostic}`, {\n\t\t\t\tintentId,\n\t\t\t\tdiagnostic: result.diagnostic,\n\t\t\t});\n\t\t}\n\t}\n\n\tprivate _intentIdFor(missionId: string): string {\n\t\tthis._assertMissionId(missionId);\n\t\tconst intentId = this._intentIdFactory(missionId);\n\t\tif (!isSafeIntentId(intentId)) {\n\t\t\tthrow new SchedulingError(\"INVALID_INTENT_ID\", `Intent id factory produced an unsafe id: ${intentId}`, {\n\t\t\t\tmissionId,\n\t\t\t\tintentId,\n\t\t\t});\n\t\t}\n\t\treturn intentId;\n\t}\n\n\tprivate _requireIntent(intentId: string): Promise<SchedulingIntentRecord> {\n\t\tif (!isSafeIntentId(intentId)) {\n\t\t\tthrow new SchedulingError(\"INVALID_INTENT_ID\", `Unsafe intent id: ${intentId}`, { intentId });\n\t\t}\n\t\treturn this._store.load(intentId).then((loaded) => {\n\t\t\tif (loaded.status === \"missing\") {\n\t\t\t\tthrow new SchedulingError(\"INTENT_NOT_FOUND\", `Intent not found: ${intentId}`, { intentId });\n\t\t\t}\n\t\t\tif (loaded.status === \"corrupt\") {\n\t\t\t\tthrow new SchedulingError(\"INTENT_CORRUPT\", `Intent ${intentId} is corrupt: ${loaded.diagnostic}`, {\n\t\t\t\t\tintentId,\n\t\t\t\t\tdiagnostic: loaded.diagnostic,\n\t\t\t\t});\n\t\t\t}\n\t\t\treturn loaded.record;\n\t\t});\n\t}\n\n\tprivate async _requireMission(missionId: string): Promise<DurableMissionRecord> {\n\t\tthis._assertMissionId(missionId);\n\t\tconst loaded = await this._missions.load(missionId);\n\t\tif (loaded.status === \"missing\") {\n\t\t\tthrow new SchedulingError(\"MISSION_NOT_FOUND\", `Mission not found: ${missionId}`, { missionId });\n\t\t}\n\t\tif (loaded.status === \"corrupt\") {\n\t\t\tthrow new SchedulingError(\"MISSION_CORRUPT\", `Mission ${missionId} is corrupt: ${loaded.diagnostic}`, {\n\t\t\t\tmissionId,\n\t\t\t\tdiagnostic: loaded.diagnostic,\n\t\t\t});\n\t\t}\n\t\treturn loaded.record;\n\t}\n\n\tprivate _assertMissionId(missionId: string): void {\n\t\tif (!isSafeMissionId(missionId)) {\n\t\t\tthrow new SchedulingError(\"INVALID_MISSION_ID\", `Unsafe mission id: ${missionId}`, { missionId });\n\t\t}\n\t}\n\n\tprivate _unwrapMutation<T>(\n\t\tresult:\n\t\t\t| { status: \"ok\"; value: T }\n\t\t\t| { status: \"missing\" }\n\t\t\t| { status: \"corrupt\"; intentId: string; diagnostic: string },\n\t\tintentId: string,\n\t): T {\n\t\tif (result.status === \"missing\") {\n\t\t\tthrow new SchedulingError(\"INTENT_NOT_FOUND\", `Intent not found: ${intentId}`, { intentId });\n\t\t}\n\t\tif (result.status === \"corrupt\") {\n\t\t\tthrow new SchedulingError(\"INTENT_CORRUPT\", `Intent ${intentId} is corrupt: ${result.diagnostic}`, {\n\t\t\t\tintentId,\n\t\t\t\tdiagnostic: result.diagnostic,\n\t\t\t});\n\t\t}\n\t\treturn result.value;\n\t}\n\n\tprivate _toSummary(record: SchedulingIntentRecord): SchedulingIntentSummary {\n\t\treturn {\n\t\t\tintentId: record.intentId,\n\t\t\tmissionId: record.missionId,\n\t\t\tstate: record.state,\n\t\t\tpriority: record.priority,\n\t\t\tenqueuedAtMs: record.enqueuedAtMs,\n\t\t\tcreatedAtMs: record.createdAtMs,\n\t\t\tupdatedAtMs: record.updatedAtMs,\n\t\t\tassignmentId: record.assignmentId,\n\t\t\tunschedulableReason: record.unschedulableReason,\n\t\t};\n\t}\n\n\tprivate _toDetail(record: SchedulingIntentRecord): SchedulingIntentDetail {\n\t\treturn { ...this._toSummary(record), requirements: record.requirements };\n\t}\n}\n"]}