import { randomUUID } from 'node:crypto'; import { db } from '../db/index.js'; import { getHost, queueHostOperation, waitHostOperationStatus } from '../hosts/index.js'; import { trackActivity } from '../pipeline/activity.js'; import { latestGameSubmission } from '../fleet/lifecycle-evidence.js'; export interface NativeInstance { id: string; server: string; host_id: string; generation: number; library_id: string; image: string; bind_address: string; port: number; public_address: string | null; public_port: number | null; created_at: string; retired_at: string | null; } export function nativeInstance(server: string): NativeInstance | null { return db().prepare('SELECT * FROM native_instances WHERE server = ? AND retired_at IS NULL').get(server) as NativeInstance | undefined ?? null; } export function nativeDeployment(id: string): boolean { return !!db().prepare(`SELECT 1 FROM native_provisioning WHERE deployment_id = ? UNION ALL SELECT 1 FROM native_creation_intents WHERE deployment_id = ? UNION ALL SELECT 1 FROM native_creation_cancellations c JOIN native_creation_intents i ON i.deployment_id=c.creation_id WHERE c.cleanup_id = ? LIMIT 1`).get(id, id, id); } export function unplacedNativeCreation(server: string): string | null { const row = db().prepare(`SELECT d.id FROM server_deployments d JOIN native_creation_intents i ON i.deployment_id=d.id LEFT JOIN native_provisioning n ON n.deployment_id=d.id WHERE d.server=? AND d.server_deleted_at IS NULL AND d.action='create' AND d.state='queued' AND n.deployment_id IS NULL ORDER BY d.rowid DESC LIMIT 1`).get(server) as { id: string } | undefined; return row?.id ?? null; } const observations = new Map>(); const readKinds = new Set(['instance.inspect', 'files.read', 'files.stat', 'files.list', 'files.manifest', 'logs.read']); /** Share overlapping observations only, never completed results or mutations. */ export async function nativeRequest>(instance: NativeInstance, kind: string, payload: Record = {}, key?: string): Promise { const host = getHost(instance.host_id); if (!host || host.connectionState !== 'online') throw new Error('Host agent is offline; instance state is unknown.'); const readOnly = readKinds.has(kind) || (kind === 'rcon.execute' && ['status', 'getstatus', 'version'].includes(String(payload.command))); if (key || !readOnly) return executeRequest(instance, kind, payload, key); // A probe after a lifecycle submission must not join an old-process read. const identity = JSON.stringify([instance.host_id, instance.id, instance.generation, instance.server, latestGameSubmission(instance.server), kind, payload]); const pending = observations.get(identity); if (pending) return pending as Promise; const request = executeRequest(instance, kind, payload).finally(() => { observations.delete(identity); }); observations.set(identity, request); return request; } /** Only the agent receives host paths, Docker access, or game credentials. */ async function executeRequest(instance: NativeInstance, kind: string, payload: Record, key?: string): Promise { const operation = queueHostOperation(instance.host_id, kind, payload, { instanceId: instance.id, generation: instance.generation, idempotencyKey: key ?? randomUUID() }); const result = await trackActivity(`agent:${operation.id}`, `Waiting for ${kind} on ${instance.server}`, () => waitHostOperationStatus(operation.id, 25_000)); if (result.state !== 'completed') throw new Error(result.error || `Agent operation ${result.id} is ${result.state}; its original ID is retained.`); // Do not apply replies from a retired generation to a replacement server. const current = nativeInstance(instance.server); if (current?.id !== instance.id || current.generation !== instance.generation) throw new Error('Instance was replaced during the operation.'); return result.result as T; } export async function nativeHealth(server: string): Promise<{ ok: true; state: string } | { ok: false; reason: string; state?: string }> { const instance = nativeInstance(server); if (!instance) return { ok: false, reason: 'Native instance is not registered' }; try { const result = await nativeRequest<{ state: string; healthy: boolean }>(instance, 'instance.inspect'); return result.healthy ? { ok: true, state: result.state } : { ok: false, state: result.state, reason: `Container ${result.state}` }; } catch (error) { return { ok: false, state: 'unknown', reason: (error as Error).message }; } }