diff --git a/archivebox/opencode/templates/opencode/agent.html b/archivebox/opencode/templates/opencode/agent.html
index 8a252295..e6f41a67 100644
--- a/archivebox/opencode/templates/opencode/agent.html
+++ b/archivebox/opencode/templates/opencode/agent.html
@@ -206,6 +206,78 @@
});
})();
+
{% endif %}
{% endblock %}
diff --git a/archivebox/opencode/urls.py b/archivebox/opencode/urls.py
index 1b073502..bf8fad73 100644
--- a/archivebox/opencode/urls.py
+++ b/archivebox/opencode/urls.py
@@ -1,10 +1,11 @@
from django.urls import path, re_path
-from archivebox.opencode.views import agent_view, opencode_proxy_view
+from archivebox.opencode.views import agent_health_view, agent_view, opencode_proxy_view
urlpatterns = [
path("", agent_view, name="opencode-agent"),
+ path("opencode/_archivebox/health", agent_health_view, name="opencode-health"),
re_path(
r"^opencode(?:/(?P.*))?$",
opencode_proxy_view,
diff --git a/archivebox/opencode/views.py b/archivebox/opencode/views.py
index 18779188..c689abee 100644
--- a/archivebox/opencode/views.py
+++ b/archivebox/opencode/views.py
@@ -26,6 +26,7 @@ from django.http import (
HttpRequest,
HttpResponse,
HttpResponseForbidden,
+ JsonResponse,
StreamingHttpResponse,
)
from django.shortcuts import redirect, render
@@ -378,14 +379,29 @@ def _health(settings: dict) -> bool:
return False
+def _opencode_version(settings: dict) -> str:
+ try:
+ response = requests.get(
+ f"{settings['origin']}/global/health",
+ timeout=2,
+ )
+ response.raise_for_status()
+ data = response.json()
+ return str(data.get("version") or "") if isinstance(data, dict) else ""
+ except (requests.RequestException, ValueError):
+ return ""
+
+
def _ensure_opencode(settings: dict) -> tuple[bool, str]:
global _PROCESS
started_process: subprocess.Popen | None = None
workdir = settings["workdir"].resolve()
with _PROCESS_LOCK:
- if (_PROCESS is not None and _PROCESS.poll() is None) or _health(settings):
+ if _health(settings):
return True, ""
+ if _PROCESS is not None and _PROCESS.poll() is None:
+ _stop_owned_process(_PROCESS)
try:
binary, git_binary, binary_env = _resolve_binary(
@@ -494,10 +510,15 @@ def agent_view(request: HttpRequest):
settings["archivebox_api_url"] = api_url
ok, error = _ensure_opencode(settings)
recent_session_id = ""
+ opencode_version = ""
if ok:
try:
with _SESSION_LOCK:
recent_session_id = _ensure_default_session(settings)
+ opencode_version = _opencode_version(settings)
+ if not opencode_version:
+ ok = False
+ error = "OpenCode health check did not report its version."
except (requests.RequestException, RuntimeError, ValueError) as err:
ok = False
error = f"OpenCode project initialization failed: {err}"
@@ -514,6 +535,8 @@ def agent_view(request: HttpRequest):
# open the durable session URL so a fresh browser does not land on the
# transient new-session route and appear to have lost prior sessions.
"proxy_url": _project_route(settings["workdir"], recent_session_id),
+ "proxy_prefix": _PROXY_PREFIX,
+ "opencode_version": opencode_version,
"workdir": str(settings["workdir"].resolve()),
"recent_session_id": recent_session_id,
}
@@ -525,6 +548,23 @@ def agent_view(request: HttpRequest):
)
+def agent_health_view(request: HttpRequest):
+ config = _machine_config()
+ _require_enabled(config)
+ auth_response = _require_superuser(request)
+ if auth_response:
+ return auth_response
+
+ version = _opencode_version(_settings(config))
+ healthy = bool(version)
+ response = JsonResponse(
+ {"healthy": healthy, "version": version},
+ status=200 if healthy else 503,
+ )
+ response.headers["Cache-Control"] = "no-store"
+ return response
+
+
def _proxy_url(settings: dict, path: str | None) -> str:
rel = "/" if not path else f"/{path}"
return urljoin(settings["origin"], rel)
diff --git a/archivebox/tests/test_opencode_agent.py b/archivebox/tests/test_opencode_agent.py
index b0ee9d2d..4bd89e2f 100644
--- a/archivebox/tests/test_opencode_agent.py
+++ b/archivebox/tests/test_opencode_agent.py
@@ -186,6 +186,15 @@ def test_opencode_agent_superuser_gets_admin_wrapper(admin_client, live_opencode
assert f'