feat(opencode): add plan mode, reasoning effort, and status telemetry#688
Conversation
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>
There was a problem hiding this comment.
Findings
-
[Major] OpenCode plan mode is exposed to local sessions without local enforcement — adding
planto 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 mirrorspermission.*hook events, so local plan mode can still execute tools, defeating the mode's safety expectation. Evidenceshared/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. Evidencecli/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
| 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 |
There was a problem hiding this comment.
[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) { |
There was a problem hiding this comment.
[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.
…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
Bot feedback addressed in two follow-up commits1. OpenCode plan mode restricted to remote sessions
2. Reasoning effort rollback on backend rejection
3. Web: auto-retry OpenCode models query (no refresh needed)
Added tests
All tests pass; typecheck clean for CLI + Hub + Web. |
There was a problem hiding this comment.
Findings
- [Minor] OpenCode model discovery can poll forever on empty metadata —
getOpencodeModelsRefetchIntervalreturns1000for every successful response with an emptyavailableModelsarray, 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-modelsonce per second indefinitely. Evidenceweb/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 |
There was a problem hiding this comment.
[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
),|
Addressed in Changes:
Validation:
|
|
I think the code has no problem |
There was a problem hiding this comment.
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
bunis unavailable, so this is a static review only.
Testing
- Not run:
bunis not installed in this runner (/bin/bash: bun: command not found).
HAPI Bot
Summary
ctxandcacheTest plan
bun run test -- src/agent/messageConverter.test.ts src/agent/backends/acp/AcpSdkBackend.test.tsinclibun run typecheckinclibun run test -- src/chat/normalize.test.tsinwebbun run typecheckinwebIssues
Notes
ctx/cache) so ACP usage data is visible in the existing composer/status bar without introducing an agent-specific UI path.Made with Cursor