Skip to content

feat: SSH loopback forwarding and native manual compaction for Codex and OpenCode - #30

Open
dviejokfs wants to merge 4 commits into
mainfrom
feat/ssh-forwarding-native-compaction
Open

dviejokfs wants to merge 4 commits into
mainfrom
feat/ssh-forwarding-native-compaction

Conversation

@dviejokfs

@dviejokfs dviejokfs commented Oct 5, 2026 •

Copy link
Copy Markdown
Contributor

Why

Temps Fleet is moving every turn — local, remote daemon and SSH, for every harness — onto this SDK so it can delete its own legacy drivers and Bun sidecar. Two gaps blocked that:

  • OpenCode::serve() could not run on an SshTransport: the client talks to the server on the SDK host's loopback, and the transport never forwarded the port.
  • RuntimeHandle::compact only worked for Claude, so Fleet still compacted Codex and OpenCode sessions through its legacy drivers.

What

  • CommandSpec::loopback_ports — loopback ports a provider process serves that the SDK host must reach. SshTransport::spawn adds -L 127.0.0.1:<port>:127.0.0.1:<port> for each (with ExitOnForwardFailure=yes, so a failed forward fails the connection instead of leaving the client unable to reach the server). opencode serve declares its port. Local execution ignores the field.
  • Manual compaction for Codex and OpenCode. On a /compact invocation:
    • Codex (app-server mode) sends thread/compact/start once the thread is open instead of turn/start, and finishes on thread/compacted (or the contextCompaction item / turn/completed).
    • OpenCode (serve mode) posts /session/{id}/summarize with the pinned {providerID, modelID} instead of a message; the existing compaction-part / session.compacted / session.idle handling reports the lifecycle. Without a pinned model the turn fails with InvalidRequest { field: "model" }.
    • Both emit CompactionStarted/Completed with CompactionTrigger::Manual.
  • TurnCapabilities::manual_compaction (serde-defaulted) — Claude, Codex app-server and OpenCode serve set it; RuntimeDriverCapabilities::manual_compaction follows it instead of provider == Claude.
  • is_manual_compaction_prompt moves from the Claude adapter to providers so all adapters detect the invocation the same way.

Follow-up fixes on this branch

  • Retained Codex compaction never finished. thread/compact/start answers with an empty result, so a retained connection had no turn id for the compaction and dropped all of its frames as uncorrelated. A manual compaction now adopts the turn id from turn/started.
  • A manual compaction no longer emits the "completed without a text response" warning.

Testing

  • Unit tests: Codex compacts the opened thread instead of starting a turn; OpenCode summarizes with its model (and refuses without one); SshTransport forwards ports before the destination, only when requested.
  • Integration (tests/codex_app_server.rs): the fixture app server now plays thread/compact/start exactly as codex app-server 0.159 does (captured from the real binary: empty result, turn/started, contextCompaction item started/completed, turn/completed, no thread/compacted). Covered one-shot and on a retained connection after a normal turn — the retained case reproduced the hang before the fix.
  • cargo fmt --check, cargo clippy --all-targets --all-features -D warnings, cargo test --locked --all-features all pass.
  • End to end in Temps Fleet (linked to this branch): /compact on a Codex conversation with a retained app server completes and shrinks the context (7% → 2%); Claude turns run over SshTransport against a Docker sshd. OpenCode over SSH and OpenCode compaction are covered by unit tests only (no working OpenCode login on the test machine).

🤖 Generated with Claude Code

…and OpenCode

- `CommandSpec::loopback_ports`: ports a provider process serves on loopback
  that the SDK host must reach. `SshTransport` forwards each one with `ssh -L`
  (and `ExitOnForwardFailure=yes`), so `OpenCode::serve()` runs on SSH targets.
  Local execution ignores the field.
- Manual compaction for Codex (`thread/compact/start` on the app server) and
  OpenCode (`/session/{id}/summarize` on `opencode serve`, which needs a pinned
  model). Both report the manual compaction lifecycle like Claude.
- `TurnCapabilities::manual_compaction` states which adapters support it; the
  retained driver's capability follows it instead of naming Claude alone.
- `is_manual_compaction_prompt` moves to `providers` so every adapter detects
  the `/compact` invocation `CompactionInput` produces the same way.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
@greptile-apps

greptile-apps Bot commented Oct 5, 2026 •

Copy link
Copy Markdown

RetriggerConfidence Score: 5/5

[High risk] Adds SSH port forwarding and compaction support across multiple providers.

The PR appears safe to merge based on the reviewed changes.

Summary

The PR adds SSH loopback forwarding and native manual compaction for Codex app-server and OpenCode serve. The follow-up changes authenticate the OpenCode HTTP bridge, reject unsupported compaction instructions, correlate retained Codex compaction events, and narrow suppression of empty-response warnings.

Diagram
%%{init: {'theme': 'neutral'}}%%
flowchart LR
  Caller[Runtime caller] --> Runtime[Runtime]
  Runtime -->|compact| Adapter{Provider adapter}
  Adapter -->|thread/compact/start| Codex[Codex app-server]
  Adapter -->|authenticated summarize| OpenCode[OpenCode serve]
  Runtime -->|spawn with loopback ports| SSH[SSH transport]
  SSH -->|loopback forward| OpenCode
  Codex -->|compaction events| Runtime
  OpenCode -->|compaction events| Runtime
Loading

Reviews (4) · Last reviewed commit: "fix: authenticate opencode serve and har..."

Comment thread src/providers/codex_app_server.rs
Comment thread src/providers/codex_app_server.rs
Comment thread src/providers/opencode_serve.rs
Comment thread src/providers/opencode_serve.rs
Comment thread src/ssh.rs
`thread/compact/start` answers with an empty result, so a retained
connection had no turn id for the compaction and dropped every frame of it
as uncorrelated: the invocation never finished. A manual compaction now
adopts the turn id from `turn/started`.

The fixture app server plays `thread/compact/start` exactly as
codex app-server 0.159 does (empty result, `turn/started`, a
`contextCompaction` item, `turn/completed`), with one-shot and retained
coverage.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
Comment thread src/providers/codex_app_server.rs Outdated
A compaction has no reply text by design; the empty-response warning only
added noise to every /compact.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
Comment thread src/runtime.rs Outdated
Review follow-ups for SSH forwarding and native compaction:

- Security: `opencode serve` runs with OPENCODE_SERVER_USERNAME/PASSWORD
  (password derived per process and port from a random SDK secret) and the
  HTTP bridge sends Basic credentials on every request, the readiness probe
  and the event stream. A forwarded or local loopback port no longer exposes
  an unauthenticated session API.
- Codex: a manual compaction adopts `turn/started`'s turn id only from its
  own thread, ends when its `contextCompaction` item completes, and accepts
  the older `thread/compacted` for its own thread on a retained connection.
- OpenCode: `session.compacted` ends a manual compaction successfully; an
  idle session that never confirmed it fails clearly instead of reporting
  "finished without producing a reply".
- Compaction instructions: new `compaction_instructions` capability (Claude
  only). `RuntimeHandle::compact` and the Codex/OpenCode adapters refuse
  instructions instead of silently dropping them.
- The empty-reply warning is skipped only for adapters that performed a
  native compaction; pi, Codex exec and OpenCode run still warn.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>

This branch has not been deployed

No deployments
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

1 participant