diff --git a/dockerfiles/Dockerfile.hermes-agent b/dockerfiles/Dockerfile.hermes-agent index b43d6a9c4..39fc6c7f8 100644 --- a/dockerfiles/Dockerfile.hermes-agent +++ b/dockerfiles/Dockerfile.hermes-agent @@ -27,7 +27,13 @@ const socketBefore = [ ' const ws = new WebSocket(url);', ].join("\n"); const socketAfter = [ - ' const url = await api.buildWsUrl("/api/pty", params);', + ' let url: string;', + ' try {', + ' url = await api.buildWsUrl("/api/pty", params);', + ' } catch {', + ' if (!unmounting) scheduleReconnect(1006);', + ' return;', + ' }', ' if (unmounting) return;', ' const ws = new WebSocket(url);', ].join("\n"); @@ -70,6 +76,32 @@ const socketStateAfter = [ ' let ws: WebSocket | null = null;', ' let reconnectTimer: ReturnType | null = null;', ].join("\n"); +const ticketBefore = [ + ' const url = await buildWsUrl("/api/events", { channel });', + ' if (unmounting) {', + ' return;', + ' }', + ' ws = new WebSocket(url);', +].join("\n"); +const ticketAfter = [ + ' let url: string;', + ' try {', + ' url = await buildWsUrl("/api/events", { channel });', + ' } catch {', + ' if (unmounting) return;', + ' const attempt = Math.min(eventsRetryAttempt.current + 1, 6);', + ' eventsRetryAttempt.current = attempt;', + ' const delay = Math.min(500 * 2 ** (attempt - 1), 5000);', + ' setError("events feed disconnected — tool calls may not appear; reconnecting…");', + ' reconnectTimer = setTimeout(() => {', + ' reconnectTimer = null;', + ' if (!unmounting) setVersion((v) => v + 1);', + ' }, delay);', + ' return;', + ' }', + ' if (unmounting) return;', + ' ws = new WebSocket(url);', +].join("\n"); const handlersBefore = [ ' ws.addEventListener("error", () => surface(DISCONNECTED));', '', @@ -121,6 +153,7 @@ const cleanupAfter = [ for (const [before, after, label] of [ [retryStateBefore, retryStateAfter, "retry state"], [socketStateBefore, socketStateAfter, "socket state"], + [ticketBefore, ticketAfter, "ticket retry"], [handlersBefore, handlersAfter, "socket handlers"], [cleanupBefore, cleanupAfter, "socket cleanup"], ]) { diff --git a/services/hermes/agent-deployment.yaml b/services/hermes/agent-deployment.yaml index ba4dc335b..fad2b3f7c 100644 --- a/services/hermes/agent-deployment.yaml +++ b/services/hermes/agent-deployment.yaml @@ -170,7 +170,7 @@ spec: requests: {cpu: 25m, memory: 32Mi} limits: {cpu: 100m, memory: 64Mi} - name: install-agent-tools - image: registry.bstein.dev/bstein/hermes-agent@sha256:0577eb22d28b1ffc649405d31919b2527d9e591ccaa4a1e168dd6d6392a5a43f + image: registry.bstein.dev/bstein/hermes-agent@sha256:cf07be056feea8e4f2d5512899f990732f3d26b4915257de8f90105d96cec67d imagePullPolicy: IfNotPresent command: - sh @@ -218,7 +218,7 @@ spec: requests: {cpu: 100m, memory: 256Mi} limits: {cpu: "1", memory: 1Gi} - name: patch-auth - image: registry.bstein.dev/bstein/hermes-agent@sha256:0577eb22d28b1ffc649405d31919b2527d9e591ccaa4a1e168dd6d6392a5a43f + image: registry.bstein.dev/bstein/hermes-agent@sha256:cf07be056feea8e4f2d5512899f990732f3d26b4915257de8f90105d96cec67d imagePullPolicy: IfNotPresent command: - /opt/hermes/.venv/bin/python @@ -241,7 +241,7 @@ spec: requests: {cpu: 25m, memory: 64Mi} limits: {cpu: 100m, memory: 128Mi} - name: patch-tui-gateway - image: registry.bstein.dev/bstein/hermes-agent@sha256:0577eb22d28b1ffc649405d31919b2527d9e591ccaa4a1e168dd6d6392a5a43f + image: registry.bstein.dev/bstein/hermes-agent@sha256:cf07be056feea8e4f2d5512899f990732f3d26b4915257de8f90105d96cec67d imagePullPolicy: IfNotPresent command: - /opt/hermes/.venv/bin/python @@ -264,7 +264,7 @@ spec: requests: {cpu: 25m, memory: 64Mi} limits: {cpu: 100m, memory: 128Mi} - name: patch-codex-runtime - image: registry.bstein.dev/bstein/hermes-agent@sha256:0577eb22d28b1ffc649405d31919b2527d9e591ccaa4a1e168dd6d6392a5a43f + image: registry.bstein.dev/bstein/hermes-agent@sha256:cf07be056feea8e4f2d5512899f990732f3d26b4915257de8f90105d96cec67d imagePullPolicy: IfNotPresent command: - /opt/hermes/.venv/bin/python @@ -295,7 +295,7 @@ spec: requests: {cpu: 25m, memory: 64Mi} limits: {cpu: 100m, memory: 128Mi} - name: bootstrap-coordinator - image: registry.bstein.dev/bstein/hermes-agent@sha256:0577eb22d28b1ffc649405d31919b2527d9e591ccaa4a1e168dd6d6392a5a43f + image: registry.bstein.dev/bstein/hermes-agent@sha256:cf07be056feea8e4f2d5512899f990732f3d26b4915257de8f90105d96cec67d imagePullPolicy: IfNotPresent command: - /opt/hermes/.venv/bin/python @@ -305,7 +305,10 @@ spec: - {name: HERMES_HOME, value: /opt/data} - {name: HERMES_AUTH_FILE, value: /shared-auth/auth.json} - {name: HOME, value: /opt/data/home} + - {name: CODEX_HOME, value: /opt/data/home/.codex} + - {name: CLAUDE_CONFIG_DIR, value: /opt/data/home/.claude} - {name: PYTHONPATH, value: /opt/hermes} + - {name: PATH, value: /opt/coordinator:/opt/data/tools/bin:/opt/hermes/.venv/bin:/usr/local/bin:/usr/bin:/bin} securityContext: allowPrivilegeEscalation: false runAsUser: 10000 @@ -321,7 +324,7 @@ spec: requests: {cpu: 50m, memory: 128Mi} limits: {cpu: 500m, memory: 512Mi} - name: configure-agent-clients - image: registry.bstein.dev/bstein/hermes-agent@sha256:0577eb22d28b1ffc649405d31919b2527d9e591ccaa4a1e168dd6d6392a5a43f + image: registry.bstein.dev/bstein/hermes-agent@sha256:cf07be056feea8e4f2d5512899f990732f3d26b4915257de8f90105d96cec67d imagePullPolicy: IfNotPresent command: - sh @@ -359,7 +362,7 @@ spec: requests: {cpu: 25m, memory: 32Mi} limits: {cpu: 250m, memory: 128Mi} - name: prepare-ttyd-index - image: registry.bstein.dev/bstein/hermes-agent@sha256:0577eb22d28b1ffc649405d31919b2527d9e591ccaa4a1e168dd6d6392a5a43f + image: registry.bstein.dev/bstein/hermes-agent@sha256:cf07be056feea8e4f2d5512899f990732f3d26b4915257de8f90105d96cec67d imagePullPolicy: IfNotPresent command: - /opt/hermes/.venv/bin/python @@ -381,7 +384,7 @@ spec: limits: {cpu: 250m, memory: 128Mi} containers: - name: hermes - image: registry.bstein.dev/bstein/hermes-agent@sha256:0577eb22d28b1ffc649405d31919b2527d9e591ccaa4a1e168dd6d6392a5a43f + image: registry.bstein.dev/bstein/hermes-agent@sha256:cf07be056feea8e4f2d5512899f990732f3d26b4915257de8f90105d96cec67d imagePullPolicy: IfNotPresent command: [/init, /opt/hermes/docker/main-wrapper.sh] args: [gateway, run] @@ -512,7 +515,7 @@ spec: - {name: allowlist, mountPath: /etc/oauth2-proxy, readOnly: true} - {name: oauth-tmp, mountPath: /tmp} - name: terminal - image: registry.bstein.dev/bstein/hermes-agent@sha256:0577eb22d28b1ffc649405d31919b2527d9e591ccaa4a1e168dd6d6392a5a43f + image: registry.bstein.dev/bstein/hermes-agent@sha256:cf07be056feea8e4f2d5512899f990732f3d26b4915257de8f90105d96cec67d imagePullPolicy: IfNotPresent command: [/bin/sh, -ec] args: @@ -604,7 +607,7 @@ spec: requests: {cpu: 25m, memory: 64Mi} limits: {cpu: 500m, memory: 512Mi} - name: cli-lane-runner - image: registry.bstein.dev/bstein/hermes-agent@sha256:0577eb22d28b1ffc649405d31919b2527d9e591ccaa4a1e168dd6d6392a5a43f + image: registry.bstein.dev/bstein/hermes-agent@sha256:cf07be056feea8e4f2d5512899f990732f3d26b4915257de8f90105d96cec67d imagePullPolicy: IfNotPresent command: [/bin/sh, -ec] args: @@ -645,14 +648,17 @@ spec: requests: {cpu: 100m, memory: 256Mi} limits: {cpu: "3", memory: 6Gi} - name: model-steward - image: registry.bstein.dev/bstein/hermes-agent@sha256:0577eb22d28b1ffc649405d31919b2527d9e591ccaa4a1e168dd6d6392a5a43f + image: registry.bstein.dev/bstein/hermes-agent@sha256:cf07be056feea8e4f2d5512899f990732f3d26b4915257de8f90105d96cec67d imagePullPolicy: IfNotPresent command: [/opt/hermes/.venv/bin/python, /opt/coordinator/hermes_coordinator.py, --loop, --interval, "3600"] env: - {name: HERMES_HOME, value: /opt/data} - {name: HERMES_AUTH_FILE, value: /shared-auth/auth.json} - {name: HOME, value: /opt/data/home} + - {name: CODEX_HOME, value: /opt/data/home/.codex} + - {name: CLAUDE_CONFIG_DIR, value: /opt/data/home/.claude} - {name: PYTHONPATH, value: /opt/hermes} + - {name: PATH, value: /opt/coordinator:/opt/data/tools/bin:/opt/hermes/.venv/bin:/usr/local/bin:/usr/bin:/bin} securityContext: allowPrivilegeEscalation: false runAsUser: 10000 diff --git a/testing/tests/test_hermes_cli_lanes.py b/testing/tests/test_hermes_cli_lanes.py index 5ee258d7e..57106999b 100644 --- a/testing/tests/test_hermes_cli_lanes.py +++ b/testing/tests/test_hermes_cli_lanes.py @@ -723,6 +723,8 @@ def test_agent_dashboard_reconnects_all_transient_websockets(): assert "eventsRetryAttempt.current" in dockerfile assert "if (!unmounting) setVersion((v) => v + 1);" in dockerfile assert "events feed rejected (${ev.code}) — reload the page" in dockerfile + assert 'url = await api.buildWsUrl("/api/pty", params);' in dockerfile + assert 'url = await buildWsUrl("/api/events", { channel });' in dockerfile def test_agent_refreshes_routes_after_restoring_cli_logins(): @@ -740,6 +742,24 @@ def test_agent_refreshes_routes_after_restoring_cli_logins(): env = {item["name"]: item["value"] for item in configure["env"]} assert env["HERMES_AUTH_FILE"] == "/shared-auth/auth.json" assert env["PYTHONPATH"] == "/opt/hermes" + for name in ("bootstrap-coordinator", "configure-agent-clients"): + route_env = { + item["name"]: item["value"] + for item in init_containers[name]["env"] + } + assert route_env["CODEX_HOME"] == "/opt/data/home/.codex" + assert "/opt/data/tools/bin" in route_env["PATH"] + + containers = { + item["name"]: item + for item in deployment["spec"]["template"]["spec"]["containers"] + } + steward_env = { + item["name"]: item["value"] + for item in containers["model-steward"]["env"] + } + assert steward_env["CODEX_HOME"] == "/opt/data/home/.codex" + assert "/opt/data/tools/bin" in steward_env["PATH"] def test_flux_health_checks_follow_the_owner_oauth_sidecar():