Skip to content

feat(opencode): add plan mode, reasoning effort, and status telemetry#688

Merged
tiann merged 7 commits into
tiann:mainfrom
swear01:feat/opencode-plan-mode
May 27, 2026
Merged

feat(opencode): add plan mode, reasoning effort, and status telemetry#688
tiann merged 7 commits into
tiann:mainfrom
swear01:feat/opencode-plan-mode

Conversation

@swear01
Copy link
Copy Markdown
Contributor

@swear01 swear01 commented May 25, 2026

Summary

  • add real OpenCode plan mode support so the existing web option affects runtime behavior
  • wire OpenCode reasoning effort through the CLI, hub, and web flow so it can be shown and changed from the settings panel
  • bridge OpenCode ACP usage into the existing token-count pipeline so the web status bar can show ctx and cache

Test plan

  • bun run test -- src/agent/messageConverter.test.ts src/agent/backends/acp/AcpSdkBackend.test.ts in cli
  • bun run typecheck in cli
  • bun run test -- src/chat/normalize.test.ts in web
  • bun run typecheck in web
  • Manual verification in an isolated hub + runner environment for OpenCode session controls and status telemetry

Issues

Notes

  • Also includes OpenCode web status telemetry (ctx / cache) so ACP usage data is visible in the existing composer/status bar without introducing an agent-specific UI path.

Made with Cursor

swear01 and others added 3 commits May 25, 2026 10:55
Bridge OpenCode ACP usage updates into the existing token-count pipeline so the web status bar can show live context and cache information without a separate UI path.

Co-authored-by: Cursor <cursoragent@cursor.com>
Copy link
Copy Markdown

@github-actions github-actions Bot left a comment

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Findings

  • [Major] OpenCode plan mode is exposed to local sessions without local enforcement — adding plan to the shared OpenCode modes makes CLI/API paths accept it for local OpenCode sessions, but the enforcement added in this PR lives in the remote launcher/ACP permission handler. The local launcher only mirrors permission.* hook events, so local plan mode can still execute tools, defeating the mode's safety expectation. Evidence shared/src/modes.ts:29.
    Suggested fix:

    if (flavor === 'opencode' && mode === 'plan' && sessionResult.session.agentState?.controlledByUser === true) {
        return c.json({ error: 'OpenCode plan mode is only supported for remote sessions' }, 409)
    }
  • [Major] Failed OpenCode effort switches remain persisted — the config RPC stores the requested effort and reports it as applied before the remote launcher successfully calls session/set_config_option; if OpenCode rejects the value or method, the catch path only mutates the current batch and sends a message, leaving hub/session state on the rejected value and causing later turns to retry it. Evidence cli/src/opencode/runOpencode.ts:147.
    Suggested fix:

    const fallbackEffort = this.currentBackendEffort;
    batch.mode.modelReasoningEffort = fallbackEffort;
    session.setModelReasoningEffort(fallbackEffort);
    session.pushKeepAlive();
    this.onReasoningEffortRollback?.(fallbackEffort);

Questions

  • None.

Summary

  • Review mode: initial
  • Two issues found: remote-only plan enforcement is exposed to local OpenCode sessions, and failed OpenCode reasoning-effort switches can leave persisted state out of sync with the backend.

Testing

  • Not run (automation; static review only).

HAPI Bot

Comment thread shared/src/modes.ts
export type KimiPermissionMode = typeof KIMI_PERMISSION_MODES[number]

export const OPENCODE_PERMISSION_MODES = ['default', 'yolo'] as const
export const OPENCODE_PERMISSION_MODES = ['default', 'plan', 'yolo'] as const
Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

[MAJOR] This exposes plan as an OpenCode mode for every path, including the default local hapi opencode --permission-mode plan flow and web permission-mode updates on controlled-by-user sessions. The PR only enforces plan in the remote launcher/ACP permission handler; opencodeLocalLauncher just mirrors permission.* hook events, so local plan mode can still run tools.

Suggested fix:

if (flavor === 'opencode' && mode === 'plan' && sessionResult.session.agentState?.controlledByUser === true) {
    return c.json({ error: 'OpenCode plan mode is only supported for remote sessions' }, 409)
}

Also reject terminal local startup unless --hapi-starting-mode remote:

if (options.permissionMode === 'plan' && options.startingMode !== 'remote') {
    throw new Error('OpenCode plan mode is only supported in remote mode')
}

if (config.model !== undefined) {
sessionModel = config.model;
}
if (config.modelReasoningEffort !== undefined) {
Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

[MAJOR] This records the requested effort as applied before the remote launcher has successfully called session/set_config_option. If OpenCode rejects the value or the method, the catch path only mutates the current batch and sends a message; the hub/session state still says the rejected effort is active, and future messages are queued with the same stale sessionModelReasoningEffort.

Suggested fix:

const fallbackEffort = this.currentBackendEffort;
batch.mode.modelReasoningEffort = fallbackEffort;
session.setModelReasoningEffort(fallbackEffort);
session.pushKeepAlive();
this.onReasoningEffortRollback?.(fallbackEffort);

Wire onReasoningEffortRollback to update sessionModelReasoningEffort before the next queued turn.

swear01 added 2 commits May 26, 2026 09:45
…t on failure

- Block local OpenCode plan startup (tools not enforced in local path)
- Allow remote OpenCode plan only (ACP permission handler denies tools)
- Guard web /permission-mode endpoint for local OpenCode plan sessions
- Rollback session reasoning effort when OpenCode rejects set_config_option
- Wire rollback callback through opencodeLoop to runOpencode closure
- Add tests: local plan rejected, remote plan allowed, web guard, effort rollback
… without refresh

- Retry early failures (RPC may still be registering on new sessions)
- Poll briefly until availableModels is non-empty
- Stop polling once model options are discovered
- Add tests for retry/poll/stop policy
@swear01
Copy link
Copy Markdown
Contributor Author

swear01 commented May 26, 2026

Bot feedback addressed in two follow-up commits

1. OpenCode plan mode restricted to remote sessions

  • runOpencode: rejects --permission-mode plan on local/terminal startup (startingMode !== "remote")
  • hub/web/routes/sessions: /permission-mode endpoint returns 409 for local OpenCode plan sessions
  • Reasoning: local OpenCode path lacks tool-execution enforcement; only remote ACP path has the permission handler that blocks tools in plan mode (cli/src/opencode/utils/permissionHandler.ts)

2. Reasoning effort rollback on backend rejection

  • opencodeRemoteLauncher: when setConfigOption fails or is unsupported, now calls rollbackReasoningEffort which resets:
    • current batch effort
    • session.setModelReasoningEffort
    • session.pushKeepAlive (syncs hub/UI state)
    • onReasoningEffortRollback callback → updates runOpencode closure variable
  • This prevents stale rejected effort from persisting into subsequent queued turns

3. Web: auto-retry OpenCode models query (no refresh needed)

  • useOpencodeModels hook: retries early failures (RPC may not be registered yet on brand-new sessions), polls until availableModels is non-empty, then stops
  • Fixes the race where opening a fresh OpenCode session showed only permission + effort in the settings panel, requiring a page refresh to see the model selector

Added tests

  • cli/src/opencode/runOpencode.test.ts — local plan rejected, remote plan allowed
  • cli/src/opencode/opencodeRemoteLauncher.test.ts — effort rollback on failure
  • hub/src/web/routes/sessions.test.ts — web guard: local OpenCode plan rejected, remote allowed
  • web/src/hooks/queries/useOpencodeModels.test.ts — retry/poll/stop policy

All tests pass; typecheck clean for CLI + Hub + Web.

Copy link
Copy Markdown

@github-actions github-actions Bot left a comment

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Findings

  • [Minor] OpenCode model discovery can poll forever on empty metadata — getOpencodeModelsRefetchInterval returns 1000 for every successful response with an empty availableModels array, and the session RPC explicitly returns that shape when the backend has no cached metadata. For OpenCode builds or ACP responses that never expose model metadata, every open active session view keeps calling /api/sessions/:id/opencode-models once per second indefinitely. Evidence web/src/hooks/queries/useOpencodeModels.ts:24.
    Suggested fix:
    const MAX_OPENCODE_MODEL_DISCOVERY_POLLS = 10
    
    export function getOpencodeModelsRefetchInterval(
        enabled: boolean,
        data: OpencodeModelsResponse | undefined,
        pollCount: number
    ): 1000 | false {
        if (!enabled || pollCount >= MAX_OPENCODE_MODEL_DISCOVERY_POLLS) {
            return false
        }
        if (!data || data.success === false) {
            return 1000
        }
        return (data.availableModels?.length ?? 0) > 0 ? false : 1000
    }
    
    refetchInterval: (query) => getOpencodeModelsRefetchInterval(
        enabled,
        query.state.data as OpencodeModelsResponse | undefined,
        query.state.dataUpdateCount + query.state.errorUpdateCount
    ),

Questions

  • None.

Summary

  • Review mode: follow-up after new commits
  • Prior bot findings appear addressed. One remaining issue: empty OpenCode model metadata now causes unbounded per-second polling while the session page stays open.

Testing

  • Not run (automation; static review only).

HAPI Bot

if (data.success === false) {
return 1000
}
return (data.availableModels?.length ?? 0) > 0 ? false : 1000
Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

[MINOR] This keeps polling forever when the backend returns a successful empty model snapshot. The remote launcher returns { success: true, availableModels: [], currentModelId: null } when it has no cached metadata, and this helper maps that same state to another 1s poll with no attempt/time cap. For OpenCode builds or ACP responses that never expose model metadata, every open active session view will keep issuing /api/sessions/:id/opencode-models RPCs once per second indefinitely.

Suggested fix:

const MAX_OPENCODE_MODEL_DISCOVERY_POLLS = 10

export function getOpencodeModelsRefetchInterval(
    enabled: boolean,
    data: OpencodeModelsResponse | undefined,
    pollCount: number
): 1000 | false {
    if (!enabled || pollCount >= MAX_OPENCODE_MODEL_DISCOVERY_POLLS) {
        return false
    }
    if (!data || data.success === false) {
        return 1000
    }
    return (data.availableModels?.length ?? 0) > 0 ? false : 1000
}

refetchInterval: (query) => getOpencodeModelsRefetchInterval(
    enabled,
    query.state.data as OpencodeModelsResponse | undefined,
    query.state.dataUpdateCount + query.state.errorUpdateCount
),

@swear01
Copy link
Copy Markdown
Contributor Author

swear01 commented May 26, 2026

Addressed in 8ab1cc9.

Changes:

  • Root cause: OpenCode model discovery now also extracts model metadata from ACP configOptions (category: "model") when the models.availableModels block is missing. This covers ACP responses that expose the model selector only through config options.
  • No more ambiguous empty success from active session RPC: when no OpenCode model metadata is cached at all, listOpencodeModels returns success: false with an explicit unavailable error instead of success: true + empty models.
  • Safeguard: web model discovery polling is capped at 10 attempts using dataUpdateCount + errorUpdateCount, so an active session view cannot poll /opencode-models forever even if metadata never becomes available.

Validation:

  • cd cli && ../node_modules/.bin/tsc --noEmit
  • cd cli && ../node_modules/.bin/vitest run src/agent/backends/acp/AcpSdkBackend.test.ts src/modules/common/opencodeModels.test.ts src/opencode/opencodeRemoteLauncher.test.ts
  • cd web && ../node_modules/.bin/tsc --noEmit
  • cd web && ../node_modules/.bin/vitest run src/hooks/queries/useOpencodeModels.test.ts

@swear01
Copy link
Copy Markdown
Contributor Author

swear01 commented May 27, 2026

I think the code has no problem
The test fail since the OpenAI model quota is reached.
Please manually review or re trigger the checks

Copy link
Copy Markdown

@github-actions github-actions Bot left a comment

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Findings

  • No high-confidence issues found in the added/modified lines.

Questions

  • None.

Summary

  • Review mode: follow-up after new commits
  • Prior bot finding about unbounded OpenCode model polling is addressed with a poll cap. Residual risk: I could not execute the test suite in this runner because bun is unavailable, so this is a static review only.

Testing

  • Not run: bun is not installed in this runner (/bin/bash: bun: command not found).

HAPI Bot

@tiann tiann merged commit 6f2bb7d into tiann:main May 27, 2026
2 checks passed
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.

OpenCode: model reasoning effort setting fails with 400 OpenCode: support plan mode

2 participants