hermes(agent): reconnect stock dashboard feeds
This commit is contained in:
parent
04bd50ca74
commit
0bec6edba3
@ -48,6 +48,88 @@ if (!source.includes(attachBefore)) {
|
|||||||
source = source.replace(socketBefore, socketAfter);
|
source = source.replace(socketBefore, socketAfter);
|
||||||
source = source.replace(attachBefore, attachAfter);
|
source = source.replace(attachBefore, attachAfter);
|
||||||
fs.writeFileSync(path, source);
|
fs.writeFileSync(path, source);
|
||||||
|
|
||||||
|
// The terminal socket already reconnects indefinitely after a transient
|
||||||
|
// outage. The sidebar's events/gateway sockets did not: one deployment or
|
||||||
|
// network flap left the page in a permanent manual-Reconnect state. Retry the
|
||||||
|
// whole sidebar connection set with capped backoff while keeping auth/origin
|
||||||
|
// rejections fail-closed.
|
||||||
|
const sidebarPath = "/opt/hermes/web/src/components/ChatSidebar.tsx";
|
||||||
|
let sidebar = fs.readFileSync(sidebarPath, "utf8");
|
||||||
|
const retryStateBefore = ' const [version, setVersion] = useState(0);';
|
||||||
|
const retryStateAfter = [
|
||||||
|
retryStateBefore,
|
||||||
|
' const eventsRetryAttempt = useRef(0);',
|
||||||
|
].join("\n");
|
||||||
|
const socketStateBefore = [
|
||||||
|
' let unmounting = false;',
|
||||||
|
' let ws: WebSocket | null = null;',
|
||||||
|
].join("\n");
|
||||||
|
const socketStateAfter = [
|
||||||
|
' let unmounting = false;',
|
||||||
|
' let ws: WebSocket | null = null;',
|
||||||
|
' let reconnectTimer: ReturnType<typeof setTimeout> | null = null;',
|
||||||
|
].join("\n");
|
||||||
|
const handlersBefore = [
|
||||||
|
' ws.addEventListener("error", () => surface(DISCONNECTED));',
|
||||||
|
'',
|
||||||
|
' ws.addEventListener("close", (ev) => {',
|
||||||
|
' if (ev.code === 4401 || ev.code === 4403) {',
|
||||||
|
' surface(`events feed rejected (${ev.code}) — reload the page`);',
|
||||||
|
' } else if (ev.code !== 1000) {',
|
||||||
|
' surface(DISCONNECTED);',
|
||||||
|
' }',
|
||||||
|
' });',
|
||||||
|
].join("\n");
|
||||||
|
const handlersAfter = [
|
||||||
|
' ws.addEventListener("open", () => {',
|
||||||
|
' eventsRetryAttempt.current = 0;',
|
||||||
|
' setError(null);',
|
||||||
|
' });',
|
||||||
|
'',
|
||||||
|
' ws.addEventListener("error", () => surface(DISCONNECTED));',
|
||||||
|
'',
|
||||||
|
' ws.addEventListener("close", (ev) => {',
|
||||||
|
' if (unmounting || ev.code === 1000) return;',
|
||||||
|
' if (ev.code === 4401 || ev.code === 4403) {',
|
||||||
|
' surface(`events feed rejected (${ev.code}) — reload the page`);',
|
||||||
|
' return;',
|
||||||
|
' }',
|
||||||
|
' const attempt = Math.min(eventsRetryAttempt.current + 1, 6);',
|
||||||
|
' eventsRetryAttempt.current = attempt;',
|
||||||
|
' const delay = Math.min(500 * 2 ** (attempt - 1), 5000);',
|
||||||
|
' surface(`${DISCONNECTED}; reconnecting…`);',
|
||||||
|
' reconnectTimer = setTimeout(() => {',
|
||||||
|
' reconnectTimer = null;',
|
||||||
|
' if (!unmounting) setVersion((v) => v + 1);',
|
||||||
|
' }, delay);',
|
||||||
|
' });',
|
||||||
|
].join("\n");
|
||||||
|
const cleanupBefore = [
|
||||||
|
' return () => {',
|
||||||
|
' unmounting = true;',
|
||||||
|
' ws?.close();',
|
||||||
|
' };',
|
||||||
|
].join("\n");
|
||||||
|
const cleanupAfter = [
|
||||||
|
' return () => {',
|
||||||
|
' unmounting = true;',
|
||||||
|
' if (reconnectTimer) clearTimeout(reconnectTimer);',
|
||||||
|
' ws?.close();',
|
||||||
|
' };',
|
||||||
|
].join("\n");
|
||||||
|
for (const [before, after, label] of [
|
||||||
|
[retryStateBefore, retryStateAfter, "retry state"],
|
||||||
|
[socketStateBefore, socketStateAfter, "socket state"],
|
||||||
|
[handlersBefore, handlersAfter, "socket handlers"],
|
||||||
|
[cleanupBefore, cleanupAfter, "socket cleanup"],
|
||||||
|
]) {
|
||||||
|
if (!sidebar.includes(before)) {
|
||||||
|
throw new Error(`Hermes ChatSidebar ${label} patch context changed`);
|
||||||
|
}
|
||||||
|
sidebar = sidebar.replace(before, after);
|
||||||
|
}
|
||||||
|
fs.writeFileSync(sidebarPath, sidebar);
|
||||||
NODE
|
NODE
|
||||||
|
|
||||||
# The upstream OIDC gate authenticates users but deliberately treats the
|
# The upstream OIDC gate authenticates users but deliberately treats the
|
||||||
@ -395,6 +477,8 @@ RUN cd /opt/hermes/web \
|
|||||||
&& npm run build \
|
&& npm run build \
|
||||||
&& grep -Fq 'if (unmounting) return;' src/pages/ChatPage.tsx \
|
&& grep -Fq 'if (unmounting) return;' src/pages/ChatPage.tsx \
|
||||||
&& grep -Fq 'resume:${resumeParam}' src/pages/ChatPage.tsx \
|
&& grep -Fq 'resume:${resumeParam}' src/pages/ChatPage.tsx \
|
||||||
|
&& grep -Fq 'eventsRetryAttempt.current' src/components/ChatSidebar.tsx \
|
||||||
|
&& grep -Fq 'reconnecting…' src/components/ChatSidebar.tsx \
|
||||||
&& grep -Fq 'HERMES_DASHBOARD_OIDC_ALLOWED_USER_IDS' \
|
&& grep -Fq 'HERMES_DASHBOARD_OIDC_ALLOWED_USER_IDS' \
|
||||||
/opt/hermes/hermes_cli/dashboard_auth/middleware.py \
|
/opt/hermes/hermes_cli/dashboard_auth/middleware.py \
|
||||||
&& grep -Fq '_resolve_request_route' \
|
&& grep -Fq '_resolve_request_route' \
|
||||||
|
|||||||
@ -170,7 +170,7 @@ spec:
|
|||||||
requests: {cpu: 25m, memory: 32Mi}
|
requests: {cpu: 25m, memory: 32Mi}
|
||||||
limits: {cpu: 100m, memory: 64Mi}
|
limits: {cpu: 100m, memory: 64Mi}
|
||||||
- name: install-agent-tools
|
- name: install-agent-tools
|
||||||
image: registry.bstein.dev/bstein/hermes-agent@sha256:46de11122226a68adad6d902e81ef9b4b53deb0433dcc61f57fa43fae2e3a3a5
|
image: registry.bstein.dev/bstein/hermes-agent@sha256:0577eb22d28b1ffc649405d31919b2527d9e591ccaa4a1e168dd6d6392a5a43f
|
||||||
imagePullPolicy: IfNotPresent
|
imagePullPolicy: IfNotPresent
|
||||||
command:
|
command:
|
||||||
- sh
|
- sh
|
||||||
@ -218,7 +218,7 @@ spec:
|
|||||||
requests: {cpu: 100m, memory: 256Mi}
|
requests: {cpu: 100m, memory: 256Mi}
|
||||||
limits: {cpu: "1", memory: 1Gi}
|
limits: {cpu: "1", memory: 1Gi}
|
||||||
- name: patch-auth
|
- name: patch-auth
|
||||||
image: registry.bstein.dev/bstein/hermes-agent@sha256:46de11122226a68adad6d902e81ef9b4b53deb0433dcc61f57fa43fae2e3a3a5
|
image: registry.bstein.dev/bstein/hermes-agent@sha256:0577eb22d28b1ffc649405d31919b2527d9e591ccaa4a1e168dd6d6392a5a43f
|
||||||
imagePullPolicy: IfNotPresent
|
imagePullPolicy: IfNotPresent
|
||||||
command:
|
command:
|
||||||
- /opt/hermes/.venv/bin/python
|
- /opt/hermes/.venv/bin/python
|
||||||
@ -241,7 +241,7 @@ spec:
|
|||||||
requests: {cpu: 25m, memory: 64Mi}
|
requests: {cpu: 25m, memory: 64Mi}
|
||||||
limits: {cpu: 100m, memory: 128Mi}
|
limits: {cpu: 100m, memory: 128Mi}
|
||||||
- name: patch-tui-gateway
|
- name: patch-tui-gateway
|
||||||
image: registry.bstein.dev/bstein/hermes-agent@sha256:46de11122226a68adad6d902e81ef9b4b53deb0433dcc61f57fa43fae2e3a3a5
|
image: registry.bstein.dev/bstein/hermes-agent@sha256:0577eb22d28b1ffc649405d31919b2527d9e591ccaa4a1e168dd6d6392a5a43f
|
||||||
imagePullPolicy: IfNotPresent
|
imagePullPolicy: IfNotPresent
|
||||||
command:
|
command:
|
||||||
- /opt/hermes/.venv/bin/python
|
- /opt/hermes/.venv/bin/python
|
||||||
@ -264,7 +264,7 @@ spec:
|
|||||||
requests: {cpu: 25m, memory: 64Mi}
|
requests: {cpu: 25m, memory: 64Mi}
|
||||||
limits: {cpu: 100m, memory: 128Mi}
|
limits: {cpu: 100m, memory: 128Mi}
|
||||||
- name: patch-codex-runtime
|
- name: patch-codex-runtime
|
||||||
image: registry.bstein.dev/bstein/hermes-agent@sha256:46de11122226a68adad6d902e81ef9b4b53deb0433dcc61f57fa43fae2e3a3a5
|
image: registry.bstein.dev/bstein/hermes-agent@sha256:0577eb22d28b1ffc649405d31919b2527d9e591ccaa4a1e168dd6d6392a5a43f
|
||||||
imagePullPolicy: IfNotPresent
|
imagePullPolicy: IfNotPresent
|
||||||
command:
|
command:
|
||||||
- /opt/hermes/.venv/bin/python
|
- /opt/hermes/.venv/bin/python
|
||||||
@ -295,7 +295,7 @@ spec:
|
|||||||
requests: {cpu: 25m, memory: 64Mi}
|
requests: {cpu: 25m, memory: 64Mi}
|
||||||
limits: {cpu: 100m, memory: 128Mi}
|
limits: {cpu: 100m, memory: 128Mi}
|
||||||
- name: bootstrap-coordinator
|
- name: bootstrap-coordinator
|
||||||
image: registry.bstein.dev/bstein/hermes-agent@sha256:46de11122226a68adad6d902e81ef9b4b53deb0433dcc61f57fa43fae2e3a3a5
|
image: registry.bstein.dev/bstein/hermes-agent@sha256:0577eb22d28b1ffc649405d31919b2527d9e591ccaa4a1e168dd6d6392a5a43f
|
||||||
imagePullPolicy: IfNotPresent
|
imagePullPolicy: IfNotPresent
|
||||||
command:
|
command:
|
||||||
- /opt/hermes/.venv/bin/python
|
- /opt/hermes/.venv/bin/python
|
||||||
@ -321,7 +321,7 @@ spec:
|
|||||||
requests: {cpu: 50m, memory: 128Mi}
|
requests: {cpu: 50m, memory: 128Mi}
|
||||||
limits: {cpu: 500m, memory: 512Mi}
|
limits: {cpu: 500m, memory: 512Mi}
|
||||||
- name: configure-agent-clients
|
- name: configure-agent-clients
|
||||||
image: registry.bstein.dev/bstein/hermes-agent@sha256:46de11122226a68adad6d902e81ef9b4b53deb0433dcc61f57fa43fae2e3a3a5
|
image: registry.bstein.dev/bstein/hermes-agent@sha256:0577eb22d28b1ffc649405d31919b2527d9e591ccaa4a1e168dd6d6392a5a43f
|
||||||
imagePullPolicy: IfNotPresent
|
imagePullPolicy: IfNotPresent
|
||||||
command:
|
command:
|
||||||
- sh
|
- sh
|
||||||
@ -332,11 +332,17 @@ spec:
|
|||||||
set +a
|
set +a
|
||||||
/opt/hermes/.venv/bin/python /opt/coordinator/configure_agent_clients.py
|
/opt/hermes/.venv/bin/python /opt/coordinator/configure_agent_clients.py
|
||||||
/opt/hermes/.venv/bin/python /opt/coordinator/migrate_herdr_state.py
|
/opt/hermes/.venv/bin/python /opt/coordinator/migrate_herdr_state.py
|
||||||
|
# Client configuration restores the persisted Codex CLI login.
|
||||||
|
# Refresh routing afterwards so AUTO sees the app-server lane on
|
||||||
|
# the first request instead of waiting for the hourly steward.
|
||||||
|
/opt/hermes/.venv/bin/python /opt/coordinator/hermes_coordinator.py --once
|
||||||
env:
|
env:
|
||||||
- {name: HERMES_HOME, value: /opt/data}
|
- {name: HERMES_HOME, value: /opt/data}
|
||||||
|
- {name: HERMES_AUTH_FILE, value: /shared-auth/auth.json}
|
||||||
- {name: HOME, value: /opt/data/home}
|
- {name: HOME, value: /opt/data/home}
|
||||||
- {name: CODEX_HOME, value: /opt/data/home/.codex}
|
- {name: CODEX_HOME, value: /opt/data/home/.codex}
|
||||||
- {name: CLAUDE_CONFIG_DIR, value: /opt/data/home/.claude}
|
- {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}
|
- {name: PATH, value: /opt/coordinator:/opt/data/tools/bin:/opt/hermes/.venv/bin:/usr/local/bin:/usr/bin:/bin}
|
||||||
securityContext:
|
securityContext:
|
||||||
allowPrivilegeEscalation: false
|
allowPrivilegeEscalation: false
|
||||||
@ -346,12 +352,14 @@ spec:
|
|||||||
type: RuntimeDefault
|
type: RuntimeDefault
|
||||||
volumeMounts:
|
volumeMounts:
|
||||||
- {name: home, mountPath: /opt/data}
|
- {name: home, mountPath: /opt/data}
|
||||||
|
- {name: provider-auth, mountPath: /shared-auth}
|
||||||
- {name: coordinator, mountPath: /opt/coordinator, readOnly: true}
|
- {name: coordinator, mountPath: /opt/coordinator, readOnly: true}
|
||||||
|
- {name: auth-patch, mountPath: /opt/hermes/hermes_cli/auth.py, subPath: auth.py}
|
||||||
resources:
|
resources:
|
||||||
requests: {cpu: 25m, memory: 32Mi}
|
requests: {cpu: 25m, memory: 32Mi}
|
||||||
limits: {cpu: 250m, memory: 128Mi}
|
limits: {cpu: 250m, memory: 128Mi}
|
||||||
- name: prepare-ttyd-index
|
- name: prepare-ttyd-index
|
||||||
image: registry.bstein.dev/bstein/hermes-agent@sha256:46de11122226a68adad6d902e81ef9b4b53deb0433dcc61f57fa43fae2e3a3a5
|
image: registry.bstein.dev/bstein/hermes-agent@sha256:0577eb22d28b1ffc649405d31919b2527d9e591ccaa4a1e168dd6d6392a5a43f
|
||||||
imagePullPolicy: IfNotPresent
|
imagePullPolicy: IfNotPresent
|
||||||
command:
|
command:
|
||||||
- /opt/hermes/.venv/bin/python
|
- /opt/hermes/.venv/bin/python
|
||||||
@ -373,7 +381,7 @@ spec:
|
|||||||
limits: {cpu: 250m, memory: 128Mi}
|
limits: {cpu: 250m, memory: 128Mi}
|
||||||
containers:
|
containers:
|
||||||
- name: hermes
|
- name: hermes
|
||||||
image: registry.bstein.dev/bstein/hermes-agent@sha256:46de11122226a68adad6d902e81ef9b4b53deb0433dcc61f57fa43fae2e3a3a5
|
image: registry.bstein.dev/bstein/hermes-agent@sha256:0577eb22d28b1ffc649405d31919b2527d9e591ccaa4a1e168dd6d6392a5a43f
|
||||||
imagePullPolicy: IfNotPresent
|
imagePullPolicy: IfNotPresent
|
||||||
command: [/init, /opt/hermes/docker/main-wrapper.sh]
|
command: [/init, /opt/hermes/docker/main-wrapper.sh]
|
||||||
args: [gateway, run]
|
args: [gateway, run]
|
||||||
@ -504,7 +512,7 @@ spec:
|
|||||||
- {name: allowlist, mountPath: /etc/oauth2-proxy, readOnly: true}
|
- {name: allowlist, mountPath: /etc/oauth2-proxy, readOnly: true}
|
||||||
- {name: oauth-tmp, mountPath: /tmp}
|
- {name: oauth-tmp, mountPath: /tmp}
|
||||||
- name: terminal
|
- name: terminal
|
||||||
image: registry.bstein.dev/bstein/hermes-agent@sha256:46de11122226a68adad6d902e81ef9b4b53deb0433dcc61f57fa43fae2e3a3a5
|
image: registry.bstein.dev/bstein/hermes-agent@sha256:0577eb22d28b1ffc649405d31919b2527d9e591ccaa4a1e168dd6d6392a5a43f
|
||||||
imagePullPolicy: IfNotPresent
|
imagePullPolicy: IfNotPresent
|
||||||
command: [/bin/sh, -ec]
|
command: [/bin/sh, -ec]
|
||||||
args:
|
args:
|
||||||
@ -596,7 +604,7 @@ spec:
|
|||||||
requests: {cpu: 25m, memory: 64Mi}
|
requests: {cpu: 25m, memory: 64Mi}
|
||||||
limits: {cpu: 500m, memory: 512Mi}
|
limits: {cpu: 500m, memory: 512Mi}
|
||||||
- name: cli-lane-runner
|
- name: cli-lane-runner
|
||||||
image: registry.bstein.dev/bstein/hermes-agent@sha256:46de11122226a68adad6d902e81ef9b4b53deb0433dcc61f57fa43fae2e3a3a5
|
image: registry.bstein.dev/bstein/hermes-agent@sha256:0577eb22d28b1ffc649405d31919b2527d9e591ccaa4a1e168dd6d6392a5a43f
|
||||||
imagePullPolicy: IfNotPresent
|
imagePullPolicy: IfNotPresent
|
||||||
command: [/bin/sh, -ec]
|
command: [/bin/sh, -ec]
|
||||||
args:
|
args:
|
||||||
@ -637,7 +645,7 @@ spec:
|
|||||||
requests: {cpu: 100m, memory: 256Mi}
|
requests: {cpu: 100m, memory: 256Mi}
|
||||||
limits: {cpu: "3", memory: 6Gi}
|
limits: {cpu: "3", memory: 6Gi}
|
||||||
- name: model-steward
|
- name: model-steward
|
||||||
image: registry.bstein.dev/bstein/hermes-agent@sha256:46de11122226a68adad6d902e81ef9b4b53deb0433dcc61f57fa43fae2e3a3a5
|
image: registry.bstein.dev/bstein/hermes-agent@sha256:0577eb22d28b1ffc649405d31919b2527d9e591ccaa4a1e168dd6d6392a5a43f
|
||||||
imagePullPolicy: IfNotPresent
|
imagePullPolicy: IfNotPresent
|
||||||
command: [/opt/hermes/.venv/bin/python, /opt/coordinator/hermes_coordinator.py, --loop, --interval, "3600"]
|
command: [/opt/hermes/.venv/bin/python, /opt/coordinator/hermes_coordinator.py, --loop, --interval, "3600"]
|
||||||
env:
|
env:
|
||||||
|
|||||||
@ -716,6 +716,32 @@ def test_agent_root_is_stock_dashboard_and_terminal_is_a_separate_path():
|
|||||||
] == "hermes-hermes-agent-terminal-slash@kubernetescrd"
|
] == "hermes-hermes-agent-terminal-slash@kubernetescrd"
|
||||||
|
|
||||||
|
|
||||||
|
def test_agent_dashboard_reconnects_all_transient_websockets():
|
||||||
|
dockerfile = (
|
||||||
|
HERMES.parents[1] / "dockerfiles/Dockerfile.hermes-agent"
|
||||||
|
).read_text(encoding="utf-8")
|
||||||
|
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
|
||||||
|
|
||||||
|
|
||||||
|
def test_agent_refreshes_routes_after_restoring_cli_logins():
|
||||||
|
deployment = _agent_deployment()
|
||||||
|
init_containers = {
|
||||||
|
item["name"]: item
|
||||||
|
for item in deployment["spec"]["template"]["spec"]["initContainers"]
|
||||||
|
}
|
||||||
|
configure = init_containers["configure-agent-clients"]
|
||||||
|
command = configure["command"][-1]
|
||||||
|
assert "configure_agent_clients.py" in command
|
||||||
|
assert command.index("configure_agent_clients.py") < command.index(
|
||||||
|
"hermes_coordinator.py --once"
|
||||||
|
)
|
||||||
|
env = {item["name"]: item["value"] for item in configure["env"]}
|
||||||
|
assert env["HERMES_AUTH_FILE"] == "/shared-auth/auth.json"
|
||||||
|
assert env["PYTHONPATH"] == "/opt/hermes"
|
||||||
|
|
||||||
|
|
||||||
def test_flux_health_checks_follow_the_owner_oauth_sidecar():
|
def test_flux_health_checks_follow_the_owner_oauth_sidecar():
|
||||||
flux = yaml.safe_load(FLUX_HERMES.read_text())
|
flux = yaml.safe_load(FLUX_HERMES.read_text())
|
||||||
checks = {
|
checks = {
|
||||||
|
|||||||
Loading…
x
Reference in New Issue
Block a user