Skip to content

OpenCode server-API: fix agent→model binding, native fork & per-session diff#2188

Closed
shrimpwtf wants to merge 10 commits into
getpaseo:mainfrom
shrimpwtf:opencode-server-api-improvements
Closed

OpenCode server-API: fix agent→model binding, native fork & per-session diff#2188
shrimpwtf wants to merge 10 commits into
getpaseo:mainfrom
shrimpwtf:opencode-server-api-improvements

Conversation

@shrimpwtf

Copy link
Copy Markdown

Summary

Closes gaps in Paseo's OpenCode HTTP server-API integration. OpenCode already runs on the server API (not ACP) — this PR fixes the agent→model binding bug and lights up unused endpoints, all provider-scoped.

Headline fix: agent→model binding (atlas→kimi)

OpenCode's GET /agent returns each agent's bound model (e.g. oh-my-openagent binds atlaskimi). Paseo was discarding it, so switching agents never switched the model. Now:

  • AgentMode carries an optional model (providerID/modelID).
  • Selecting an agent/mode applies its bound model; an explicit model pick overrides until the next mode switch; the override does not persist across switches.
  • AgentManager.setAgentMode() surfaces the re-bound model into runtimeInfo.model so the composer picker updates.

Also included

  • Permission "Allow always" — documented/verified it persists server-side via OpenCode's reply:"always" (session-scoped); no nonexistent remember flag. Added a CI-gated e2e.
  • Native fork — "Fork chat" now routes into session.fork() for OpenCode (real provider child, history preserved), gated by an agentNativeFork capability. Other providers keep the generic flow.
  • Per-session diffsession.diff() mapped to a provider-agnostic changed-files shape, gated by an opencodeSessionDiff capability with a client update-host affordance (no fallback path).

Investigated / deferred (see mission backlog)

  • Workspace search (find*), agent-context shell, and user-facing diff/search UI panels were evaluated and deferred (duplicates or need a larger UI subsystem).
  • In-app provider auth was a spike only — feasible for code/api-key flows but blocked on a SECURITY.md policy decision ("Paseo never stores or transmits provider API keys").

Compatibility

  • All protocol schema additions are .optional(); new capabilities gated in server_info.features.* with dated COMPAT() tags (v0.2.0). No shared ACP files (copilot/cursor/kiro/trae/generic) were behavior-changed.

Testing

  • Per-feature unit tests via the fake OpenCode harness; real behaviors covered by CI-gated daemon-e2e/opencode-*.real.e2e.test.ts. npm run typecheck + targeted lint green. Full suite intentionally not run locally (per repo rules) — relies on CI.

@greptile-apps

greptile-apps Bot commented Jul 18, 2026

Copy link
Copy Markdown
Contributor

Greptile Summary

This PR closes three gaps in Paseo's OpenCode HTTP server-API integration: it fixes the agent\u2192model binding bug (switching agents now switches the effective model), adds a native fork path that preserves provider-side history, and maps session.diff() to a provider-agnostic changed-files shape. All new capabilities are guarded by server_info.features.* flags and all protocol additions are .optional().

  • Agent\u2192model binding: mapOpenCodeAgentToMode now carries the agent's bound model, setMode applies it via applyAgentBoundModelForModeSelection, and AgentManager.setAgentMode surfaces the result in runtimeInfo so the UI model picker updates.
  • Native fork: forkSession on OpenCodeAgentClient calls OpenCode's fork endpoint preserving provider-side history; session.ts adds handleAgentForkRequest mirroring the import flow; view.tsx gates OpenCode forks through it while leaving all other providers unchanged.
  • Per-session diff: diffSession() on OpenCodeAgentSession calls session.diff() and maps FileDiff entries to SessionChangedFile, with a client-side capability guard that shows an "update host" affordance on older daemons.

Confidence Score: 4/5

Safe to merge for the fork and diff features; the agent-model binding path has a fragility where a mode switch can throw if the OpenCode server is unavailable.

The three headline features are well-structured and correctly gated by capability flags. The one concern is that setMode in OpenCodeAgentSession now calls getAvailableModes() (an HTTP round-trip to the OpenCode daemon) to resolve the bound model. If that call throws, the entire mode switch is rejected rather than succeeding without the bound-model side-effect. Before this PR, setMode had no live-server dependency.

packages/server/src/server/agent/providers/opencode-agent.ts — specifically resolveAgentBoundModel / applyAgentBoundModelForModeSelection inside setMode.

Important Files Changed

Filename Overview
packages/server/src/server/agent/providers/opencode-agent.ts Adds agent→model binding, native fork, and per-session diff for OpenCode. Core logic is sound, but setMode now makes a live network call to getAvailableModes() to resolve the bound model — if the OpenCode server is unavailable the mode switch fails entirely rather than degrading gracefully.
packages/server/src/server/agent/agent-manager.ts Extracts registerImportedProviderSession shared between import and fork, and surfaces the re-bound model after a mode switch via getRuntimeInfo(). Refactor is clean; model surfacing is correct for OpenCode and a no-op for other providers.
packages/server/src/server/session.ts Adds handleAgentForkRequest for the native-fork RPC path, mirroring the existing import pattern and correctly threading the parent agent label.
packages/server/src/server/agent/import-sessions.ts Adds forkProviderSession as a thin orchestration wrapper mirroring importProviderSession. Consistent with existing pattern; no issues.
packages/server/src/server/agent/agent-sdk-types.ts Adds fork/diff types and parentSessionId to ImportedProviderSession. parentSessionId is populated by OpenCode's fork response but never consumed in production code.
packages/protocol/src/messages.ts Adds AgentForkRequest/Response message pair and opencodeSessionDiff/agentNativeFork capability flags. All additions are .optional() and backward-compatible.
packages/app/src/agent-stream/view.tsx Routes OpenCode fork through the new native-fork RPC when agentNativeFork capability is present; all other providers fall through unchanged.
packages/client/src/daemon-client.ts Adds forkAgent client method following the same request/response pattern as other RPC methods. No issues.

Sequence Diagram

%%{init: {'theme': 'neutral'}}%%
sequenceDiagram
    participant UI as view.tsx
    participant DC as DaemonClient
    participant Sess as session.ts
    participant AM as AgentManager
    participant OC as OpenCodeAgentClient

    Note over UI,OC: Native Fork flow
    UI->>DC: forkAgent(agentId, opts)
    DC->>Sess: agent.fork.request
    Sess->>AM: forkProviderSession(...)
    AM->>OC: forkSession(providerHandleId, cwd)
    OC->>OC: client.session.fork(sessionID, directory)
    OC-->>AM: ImportedProviderSession
    AM->>AM: registerImportedProviderSession(forked)
    AM-->>Sess: ManagedAgent
    Sess-->>DC: agent.fork.response
    DC-->>UI: AgentSnapshotPayload
    UI->>UI: navigateToAgent(forkedAgent)

    Note over UI,OC: Agent-Model binding flow
    UI->>DC: setAgentMode(agentId, modeId)
    DC->>Sess: set_agent_mode_request
    Sess->>AM: setAgentMode(agentId, modeId)
    AM->>OC: session.setMode(modeId)
    OC->>OC: getAvailableModes() [network call]
    OC->>OC: applyAgentBoundModelForModeSelection
    AM->>OC: session.getRuntimeInfo()
    OC-->>AM: model bound to agent
    AM->>AM: "agent.runtimeInfo.model = effectiveModel"
    AM-->>Sess: emitState(agent)
    Sess-->>DC: agent_state (model updated)
Loading
%%{init: {'theme': 'base', 'themeVariables': {"darkMode": true, "background": "#0d1117", "primaryColor": "#21262d", "primaryTextColor": "#e6edf3", "primaryBorderColor": "#8b949e", "lineColor": "#8b949e", "textColor": "#e6edf3", "edgeLabelBackground": "#161b22", "actorBkg": "#21262d", "actorBorder": "#8b949e", "actorTextColor": "#e6edf3", "actorLineColor": "#8b949e", "signalColor": "#8b949e", "signalTextColor": "#e6edf3", "noteBkgColor": "#373320", "noteBorderColor": "#d4a72c", "noteTextColor": "#f0e6c0", "labelBoxBkgColor": "#21262d", "labelBoxBorderColor": "#8b949e", "labelTextColor": "#e6edf3", "loopTextColor": "#e6edf3", "activationBkgColor": "#30363d", "activationBorderColor": "#8b949e"}}}%%
sequenceDiagram
    participant UI as view.tsx
    participant DC as DaemonClient
    participant Sess as session.ts
    participant AM as AgentManager
    participant OC as OpenCodeAgentClient

    Note over UI,OC: Native Fork flow
    UI->>DC: forkAgent(agentId, opts)
    DC->>Sess: agent.fork.request
    Sess->>AM: forkProviderSession(...)
    AM->>OC: forkSession(providerHandleId, cwd)
    OC->>OC: client.session.fork(sessionID, directory)
    OC-->>AM: ImportedProviderSession
    AM->>AM: registerImportedProviderSession(forked)
    AM-->>Sess: ManagedAgent
    Sess-->>DC: agent.fork.response
    DC-->>UI: AgentSnapshotPayload
    UI->>UI: navigateToAgent(forkedAgent)

    Note over UI,OC: Agent-Model binding flow
    UI->>DC: setAgentMode(agentId, modeId)
    DC->>Sess: set_agent_mode_request
    Sess->>AM: setAgentMode(agentId, modeId)
    AM->>OC: session.setMode(modeId)
    OC->>OC: getAvailableModes() [network call]
    OC->>OC: applyAgentBoundModelForModeSelection
    AM->>OC: session.getRuntimeInfo()
    OC-->>AM: model bound to agent
    AM->>AM: "agent.runtimeInfo.model = effectiveModel"
    AM-->>Sess: emitState(agent)
    Sess-->>DC: agent_state (model updated)
Loading

Reviews (3): Last reviewed commit: "fix(opencode): address greptile review o..." | Re-trigger Greptile

Comment thread packages/server/src/server/agent/providers/opencode-agent.ts Outdated
shrimpwtf and others added 10 commits July 18, 2026 23:13
Additive, protocol-safe field on AgentMode for per-mode model
association. Includes both-direction wire-compat test.

Co-authored-by: factory-droid[bot] <138933559+factory-droid[bot]@users.noreply.github.com>
mapOpenCodeAgentToMode now carries each OpenCode agent's bound model as
AgentMode.model (providerID/modelID). OpenCodeAgentSession applies the
selected agent's bound model as the effective prompt model on setMode,
lets an explicit setModel override win until the next switch, and
re-applies the new agent's bound model across switches (atlas→kimi,
sisyphus). Covered by unit tests using the fake OpenCode harness.

Co-authored-by: factory-droid[bot] <138933559+factory-droid[bot]@users.noreply.github.com>
setAgentMode now reads the session's resulting effective model via
getRuntimeInfo() and surfaces it into agent.config.model and
runtimeInfo.model, then emits state so the client model picker reflects
the switch (e.g. OpenCode plugins binding atlas->kimi). Covered by an
agent-manager test asserting runtimeInfo.model updates on a mode switch
to an agent with a bound model.

Co-authored-by: factory-droid[bot] <138933559+factory-droid[bot]@users.noreply.github.com>
OpenCode persists 'Allow always' server-side through reply:"always" on
permission.reply (there is no separate 'remember' boolean in the API,
verified against @opencode-ai/sdk 1.14.46 and the OpenCode permissions
docs). Document that intent in resolveOpenCodePermissionReply and add a
real e2e asserting a remembered bash permission is not re-prompted on a
subsequent matching call within the session.

Co-authored-by: factory-droid[bot] <138933559+factory-droid[bot]@users.noreply.github.com>
Add a provider-scoped forkSession on OpenCodeAgentClient that calls the
native session.fork({ messageID }) endpoint, returning a real provider
child session that preserves provider-side history rather than
re-importing a text transcript. The forked session is tied to its parent
via parentSessionId. The generic client-side fork flow for other
providers is unchanged.

Co-authored-by: factory-droid[bot] <138933559+factory-droid[bot]@users.noreply.github.com>
Add server_info.features.opencodeSessionDiff capability flag (schema +
emit, dated COMPAT v0.2.0) gating an OpenCode per-session changed-files
view. Wire OpenCodeAgentSession.diffSession() to the native session.diff()
endpoint, mapping FileDiff entries to a provider-agnostic
SessionChangedFile shape. Client detects the capability via a single-point
gate and shows an update-host affordance when absent (no fallback path).

Co-authored-by: factory-droid[bot] <138933559+factory-droid[bot]@users.noreply.github.com>
Wire the client Fork chat action into OpenCodeAgentClient.forkSession() for
OpenCode agents. Add AgentManager.forkProviderSession (mirroring
importProviderSession) plus an import-sessions orchestration wrapper that
places the forked child in a workspace and registers it as a managed agent
tied to its parent. Expose it over a new capability-gated agent.fork RPC and
a DaemonClient.forkAgent method; the agent-stream fork handler uses native
fork for OpenCode when the daemon advertises agentNativeFork and keeps the
generic re-import flow for other providers.

Co-authored-by: factory-droid[bot] <138933559+factory-droid[bot]@users.noreply.github.com>
…n diff

Co-authored-by: factory-droid[bot] <138933559+factory-droid[bot]@users.noreply.github.com>
Align the SDK with the installed opencode server binary (1.18.3),
resolving version skew from 1.14.46. Wire-compatible: build, typecheck,
lint, and the opencode-agent test suite (70 tests) all pass. The
agent-name round-trip (GET /agent name -> prompt agent field) and the
permission reply:"always" model are unchanged in 1.18.3.

Co-authored-by: factory-droid[bot] <138933559+factory-droid[bot]@users.noreply.github.com>
- Throw on diff entries missing a file path instead of silently emitting an empty path
- Use the real OpenCode test model config in the permission-remember e2e instead of a placeholder

Co-authored-by: factory-droid[bot] <138933559+factory-droid[bot]@users.noreply.github.com>
@shrimpwtf
shrimpwtf force-pushed the opencode-server-api-improvements branch from ef7db6b to 6f092ed Compare July 18, 2026 19:14
sakurayun pushed a commit to sakurayun/paseo-reclaude that referenced this pull request Jul 20, 2026
…ssion diff)

Resolved view.tsx conflict by keeping both Codex rate-limit reset state and
OpenCode agentNativeFork capability flag.
sakurayun pushed a commit to sakurayun/paseo-reclaude that referenced this pull request Jul 20, 2026
Bring in Add Transcripts UI, export RPC, draft transcript attachments, and docs.
Kept OpenCode native fork (getpaseo#2188) handlers/types alongside transcript export.
@shrimpwtf

Copy link
Copy Markdown
Author

Closing in favor of #2256, which splits out just the agent→model-on-mode-switch fix as a self-contained, provider-clean change: no if(provider==opencode) branches, and no opencode details in the protocol (all OpenCode logic stays inside opencode-agent.ts). The native-fork and per-session-diff pieces can follow as their own focused PRs if wanted. Thanks for the review feedback.

@shrimpwtf shrimpwtf closed this Jul 20, 2026
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