src/agents/swarmServerClient.ts

import http from "node:http"; import { existsSync } from "node:fs"; import path from "node:path"; import { resolveSwarmDbPath } from "./swarmDbConnection.ts";

const PROTOCOL_VERSION = 1; const DIRECT_HARNESS_SPAWN_CAPABILITIES = [ "pty.spawn.args", "pty.spawn.env", "pty.spawn.initial_input" ] as const;

export type SwarmServerHealth = { ok?: boolean; v?: number; capabilities?: unknown[]; };

export function healthSupportsDirectHarnessSpawn(health: SwarmServerHealth | null | undefined): boolean { const capabilities = new Set( (Array.isArray(health?.capabilities) ? health.capabilities : []) .map((value) => String(value || "").trim()) .filter(Boolean) ); return health?.ok === true && Number(health.v) === PROTOCOL_VERSION && DIRECT_HARNESS_SPAWN_CAPABILITIES.every((capability) => capabilities.has(capability)); }

export type SwarmServerPtyInfo = { id: string; command: string; cwd: string; started_at: number; exit_code: number | null; bound_instance_id: string | null; cols: number; rows: number; lease?: unknown; };

export type SwarmServerSpawnPtyRequest = { v: number; cwd: string; harness: "claude" | "codex"; role?: string | null; scope?: string | null; label?: string | null; name?: string | null; instance_id?: string | null; cols?: number | null; rows?: number | null; args?: string[]; env?: Record<string, string>; initial_input?: string | null; };

export type SwarmServerSpawnPtyResponse = { v: number; pty: SwarmServerPtyInfo; };

export type SwarmServerSnapshot = { ptys?: SwarmServerPtyInfo[]; };

export function resolveSwarmServerSocketPath(dbPath?: string | null): string { const resolvedDbPath = resolveSwarmDbPath(dbPath || ""); return path.join(path.dirname(resolvedDbPath), "server", "swarm-server.sock"); }

function errorMessageFromPayload(raw: string, fallback: string): string { try { const parsed = JSON.parse(raw); if (parsed && typeof parsed === "object") { const message = String((parsed as Record<string, unknown>).message || "").trim(); if (message) return message; } } catch { // ignore } return fallback; }

export class SwarmServerClient { readonly socketPath: string; readonly timeoutMs: number;

constructor({ dbPath, timeoutMs = 1500 }: { dbPath?: string | null; timeoutMs?: number } = {}) { this.socketPath = resolveSwarmServerSocketPath(dbPath || ""); this.timeoutMs = Math.max(100, Math.floor(Number(timeoutMs) || 1500)); }

async isAvailable(): Promise { if (!existsSync(this.socketPath)) return false; try { const health = await this.getHealth(); return health?.ok === true && Number(health.v) === PROTOCOL_VERSION; } catch { return false; } }

getHealth(): Promise { return this.requestJson("GET", "/health"); }

async supportsDirectHarnessSpawn(): Promise { if (!existsSync(this.socketPath)) return false; try { return healthSupportsDirectHarnessSpawn(await this.getHealth()); } catch { return false; } }

spawnPty(request: Omit<SwarmServerSpawnPtyRequest, "v">): Promise { return this.requestJson("POST", "/pty", { v: PROTOCOL_VERSION, ...request }); }

async closePty(ptyId: string, force = true): Promise { const normalizedPtyId = String(ptyId || "").trim(); if (!normalizedPtyId) return; await this.requestJson("DELETE", /pty/${encodeURIComponent(normalizedPtyId)}, { v: PROTOCOL_VERSION, pty_id: normalizedPtyId, force }); }

fetchState(): Promise { return this.requestJson("GET", "/state"); }

fetchPtyReplay(ptyId: string): Promise { const normalizedPtyId = String(ptyId || "").trim(); if (!normalizedPtyId) return Promise.resolve(""); return this.requestText("GET", /pty/${encodeURIComponent(normalizedPtyId)}/replay); }

private requestText(method: string, requestPath: string, body?: unknown): Promise { return new Promise((resolve, reject) => { const payload = body === undefined ? "" : JSON.stringify(body); const headers: Record<string, string | number> = { Accept: "text/plain" }; if (body !== undefined) { headers["Content-Type"] = "application/json"; headers["Content-Length"] = Buffer.byteLength(payload); }

  const req = http.request(
    {
      socketPath: this.socketPath,
      method,
      path: requestPath,
      headers,
      timeout: this.timeoutMs
    },
    (res) => {
      const chunks: Buffer[] = [];
      res.on("data", (chunk) => {
        chunks.push(Buffer.isBuffer(chunk) ? chunk : Buffer.from(chunk));
      });
      res.on("end", () => {
        const raw = Buffer.concat(chunks).toString("utf8");
        const statusCode = Number(res.statusCode || 0);
        if (statusCode < 200 || statusCode >= 300) {
          reject(
            new Error(
              errorMessageFromPayload(
                raw,
                `swarm-server ${method} ${requestPath} failed with status ${statusCode}`
              )
            )
          );
          return;
        }
        resolve(raw);
      });
    }
  );

  req.on("timeout", () => {
    req.destroy(new Error(`swarm-server ${method} ${requestPath} timed out`));
  });
  req.on("error", reject);
  req.end(payload);
});

}

private requestJson(method: string, requestPath: string, body?: unknown): Promise { return new Promise((resolve, reject) => { const payload = body === undefined ? "" : JSON.stringify(body); const headers: Record<string, string | number> = { Accept: "application/json" }; if (body !== undefined) { headers["Content-Type"] = "application/json"; headers["Content-Length"] = Buffer.byteLength(payload); }

  const req = http.request(
    {
      socketPath: this.socketPath,
      method,
      path: requestPath,
      headers,
      timeout: this.timeoutMs
    },
    (res) => {
      const chunks: Buffer[] = [];
      res.on("data", (chunk) => {
        chunks.push(Buffer.isBuffer(chunk) ? chunk : Buffer.from(chunk));
      });
      res.on("end", () => {
        const raw = Buffer.concat(chunks).toString("utf8");
        const statusCode = Number(res.statusCode || 0);
        if (statusCode < 200 || statusCode >= 300) {
          reject(
            new Error(
              errorMessageFromPayload(
                raw,
                `swarm-server ${method} ${requestPath} failed with status ${statusCode}`
              )
            )
          );
          return;
        }
        if (!raw.trim()) {
          resolve(undefined as T);
          return;
        }
        try {
          resolve(JSON.parse(raw) as T);
        } catch (error) {
          reject(new Error(`failed to decode swarm-server response: ${error}`));
        }
      });
    }
  );

  req.on("timeout", () => {
    req.destroy(new Error(`swarm-server ${method} ${requestPath} timed out`));
  });
  req.on("error", reject);
  req.end(payload);
});

} }