Skip to content
Closed
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
13 changes: 13 additions & 0 deletions docs/features/README.md
Original file line number Diff line number Diff line change
Expand Up @@ -291,6 +291,19 @@ Selection and stage checks start in
[personal_commands.rs](../../src/personal_commands.rs). They do not prove live
provider availability or real application/Brave context changes.

```ts
OpenCode discovery // modes.opencode-discovery; shared with Voice Action
├── GET /api/info -> Match PID/version to private service registration
├── Exact CLI 404 -> Try /api/status, then /api/health, within the same deadline
└── Other failure / invalid response -> Show error; explicit Retry
```

HEX 2.1.18 uses the two older names, both of which return 404 on OpenCode
`0.0.0-dev-19726`. The source fix adds `/api/info`.
[OpenCode compatibility](../opencode-compatibility.md#september-17-2026-endpoint-change)
records endpoint observations and fixture coverage. Endpoint availability alone
does not establish provider-backed processing or installed-app recovery.

Application activations compare the picker's bundle name with the foreground
application's localized name. When Finder shows all filename extensions, the
picker name arrives as `Ghostty.app`; [context.rs](../../src/context.rs) strips
Expand Down
2 changes: 2 additions & 0 deletions docs/features/voice-action.md
Original file line number Diff line number Diff line change
Expand Up @@ -26,6 +26,8 @@ reachable when OpenCode is unavailable; the pane offers setup/retry actions.
HEX discovers `opencode2` and its managed service through
[dictation_processor.rs](../../src/dictation_processor.rs), not a separate
HEX-owned provider service.
See [shared OpenCode discovery](README.md#process-text-with-modes) for current
endpoint compatibility and verification limits.

Discovery uses `opencode2 api get /api/status`; an exact CLI 404 falls back to
`/api/health` for older V2 services. Both attempts share the original deadline
Expand Down
25 changes: 23 additions & 2 deletions docs/opencode-compatibility.md
Original file line number Diff line number Diff line change
Expand Up @@ -28,8 +28,12 @@ HEX discovers a separately installed `opencode2`, optionally overridden by
managed service, or restart or update it. `opencode2 api` discovers or starts
the service through OpenCode's own lifecycle.

Catalog loading and generation use `opencode2 api get /api/health` to identify
Catalog loading and generation use `opencode2 api get /api/info` to identify
the authenticated active server, starting it through the CLI when needed.
For older servers, discovery tries `/api/status`, then `/api/health`, only when
the preceding command fails with the exact `HTTP 404 Not Found` diagnostic.
All attempts share the original deadline and cancellation signal. Other failures
and invalid responses stop discovery without trying another endpoint.
`opencode2 debug paths` locates its state directory. HEX reads endpoint and
password together from the bounded, owner-only service registration matching
that server's PID and version.
Expand Down Expand Up @@ -66,9 +70,26 @@ published to npm's `dev` tag; `next` was an older beta, not the newest V2 build.
Resolve the published version rather than assuming a moving tag identifies the
desired source commit.

Check the connected server's `/api/health` version as well as the CLI version:
Check the connected server's `/api/info` version as well as the CLI version:
the CLI's `api` command permits a server version mismatch. Do not replace a
user's running service to perform compatibility validation.

Keep regression coverage for body/credential privacy, curl configuration escaping,
large bodies, loopback-only admission, blocked input pipes, and cancellation.

## September 17, 2026 endpoint change

OpenCode CLI and server `0.0.0-dev-19726` returned HTTP 404 and exit code 1 for
both `/api/status` and `/api/health`. `/api/info` succeeded, with PID and version
matching the active service registration. Authenticated reads of `/api/model`
and `/api/model/default` also returned HTTP 200. The
[current troubleshooting guide](https://opencode.ai/v2/docs/troubleshooting)
documents `/api/info`.

HEX 2.1.18 supports the two older names and fails discovery on this version.
The source fix prefers `/api/info` while retaining both older names.
`service_discovery_supports_current_and_legacy_endpoints` covers each endpoint
generation, fallback order, non-404 failures, invalid responses, and private
diagnostics through a fixture CLI. The shared-deadline/cancellation fixture
covers the three-endpoint path. These checks do not prove provider-backed
generation or installed-app recovery.
79 changes: 56 additions & 23 deletions src/dictation_processor.rs
Original file line number Diff line number Diff line change
Expand Up @@ -593,12 +593,15 @@ fn discover_opencode_service_with(
pid: u32,
version: String,
}
let mut output = run(&["api", "get", "/api/status"])?;
// Older OpenCode versions expose only /api/health. Other failures are not retries.
if !output.status.success()
&& String::from_utf8_lossy(&output.stderr).trim() == "HTTP 404 Not Found"
{
output = run(&["api", "get", "/api/health"])?;
let mut output = run(&["api", "get", "/api/info"])?;
// Try older endpoint names only when the previous endpoint is missing.
for path in ["/api/status", "/api/health"] {
if output.status.success()
|| String::from_utf8_lossy(&output.stderr).trim() != "HTTP 404 Not Found"
{
break;
}
output = run(&["api", "get", path])?;
}
let health: Health = serde_json::from_str(&decode(output)?)
.map_err(|_| eyre!("OpenCode returned an invalid service health response"))?;
Expand Down Expand Up @@ -1560,54 +1563,84 @@ mod tests {
.unwrap();
fs::set_permissions(&registration, fs::Permissions::from_mode(0o600)).unwrap();
let executable = root.join("opencode2");
for (status, health, succeeds, expected_calls) in [
let available = "printf '%s\\n' '{\"pid\":42,\"version\":\"fixture-version\"}'";
let missing = "echo 'HTTP 404 Not Found' >&2; exit 1";
for (info, status, health, succeeds, expected_calls) in [
(
"printf '%s\\n' '{\"pid\":42,\"version\":\"fixture-version\"}'",
"echo 'HTTP 404 Not Found' >&2; exit 1",
available,
missing,
missing,
true,
"api get /api/status\ndebug paths\n",
"api get /api/info\ndebug paths\n",
),
(
"echo 'HTTP 404 Not Found' >&2; exit 1",
"printf '%s\\n' '{\"pid\":42,\"version\":\"fixture-version\"}'",
missing,
available,
missing,
true,
"api get /api/status\napi get /api/health\ndebug paths\n",
"api get /api/info\napi get /api/status\ndebug paths\n",
),
(
missing,
missing,
available,
true,
"api get /api/info\napi get /api/status\napi get /api/health\ndebug paths\n",
),
(
"echo 'HTTP 401 Unauthorized' >&2; exit 1",
"exit 0",
"exit 0",
false,
"api get /api/status\n",
"api get /api/info\n",
),
(
"echo 'HTTP 500 Internal Server Error' >&2; exit 1",
"exit 0",
"exit 0",
false,
"api get /api/status\n",
"api get /api/info\n",
),
(
"echo 'private diagnostic mentioning HTTP 404 Not Found' >&2; exit 1",
"exit 0",
"exit 0",
false,
"api get /api/status\n",
"api get /api/info\n",
),
(
"echo 'private invalid response'",
"exit 0",
"exit 0",
false,
"api get /api/status\n",
"api get /api/info\n",
),
(
"echo 'HTTP 404 Not Found' >&2; exit 1",
missing,
"echo 'HTTP 401 Unauthorized' >&2; exit 1",
"exit 0",
false,
"api get /api/info\napi get /api/status\n",
),
(
missing,
missing,
"echo 'private diagnostic' >&2; exit 1",
false,
"api get /api/status\napi get /api/health\n",
"api get /api/info\napi get /api/status\napi get /api/health\n",
),
(
missing,
missing,
missing,
false,
"api get /api/info\napi get /api/status\napi get /api/health\n",
),
] {
fs::write(
&executable,
format!(
"#!/bin/sh\nprintf '%s\\n' \"$*\" >> calls\ncase \"$*\" in\n'api get /api/status') {status} ;;\n'api get /api/health') {health} ;;\n'debug paths') printf 'state %s\\n' \"$PWD\" ;;\n*) exit 2 ;;\nesac\n"
"#!/bin/sh\nprintf '%s\\n' \"$*\" >> calls\ncase \"$*\" in\n'api get /api/info') {info} ;;\n'api get /api/status') {status} ;;\n'api get /api/health') {health} ;;\n'debug paths') printf 'state %s\\n' \"$PWD\" ;;\n*) exit 2 ;;\nesac\n"
),
)
.unwrap();
Expand All @@ -1622,7 +1655,7 @@ mod tests {
assert_eq!(
fs::read_to_string(root.join("calls")).unwrap(),
expected_calls,
"status fixture: {status}"
"info: {info}; status: {status}; health: {health}"
);
if succeeds {
assert_eq!(
Expand Down Expand Up @@ -1650,7 +1683,7 @@ mod tests {
let executable = root.join("opencode2");
fs::write(
&executable,
"#!/bin/sh\nprintf '%s\\n' \"$*\" >> calls\nsleep 2\ncase \"$*\" in\n'api get /api/status') echo 'HTTP 404 Not Found' >&2; exit 1 ;;\n'api get /api/health') echo '{\"pid\":42,\"version\":\"fixture-version\"}' ;;\n*) exit 2 ;;\nesac\n",
"#!/bin/sh\nprintf '%s\\n' \"$*\" >> calls\ncase \"$*\" in\n'api get /api/info') echo 'HTTP 404 Not Found' >&2; exit 1 ;;\n'api get /api/status') sleep 2; echo 'HTTP 404 Not Found' >&2; exit 1 ;;\n'api get /api/health') sleep 2; echo '{\"pid\":42,\"version\":\"fixture-version\"}' ;;\n*) exit 2 ;;\nesac\n",
)
.unwrap();
fs::set_permissions(&executable, fs::Permissions::from_mode(0o755)).unwrap();
Expand All @@ -1664,7 +1697,7 @@ mod tests {
assert!(error.to_string().contains("exceeded"), "{error}");
assert_eq!(
fs::read_to_string(root.join("calls")).unwrap(),
"api get /api/status\napi get /api/health\n"
"api get /api/info\napi get /api/status\napi get /api/health\n"
);
fs::remove_file(root.join("calls")).unwrap();
let error = discover_opencode_service_with(
Expand Down