Skip to content
Closed
Show file tree
Hide file tree
Changes from all commits
Commits
Show all changes
47 commits
Select commit Hold shift + click to select a range
02331bd
feat(opencode): add GlobalEvent model, factory, and ServerState integ…
Apr 16, 2026
ccdf294
feat(opencode): update _event_generator for GlobalEvent wrapping and …
Apr 16, 2026
6743edd
docs(rfc): promote RFC-0022 from draft to accepted
Apr 16, 2026
55eb14c
test(opencode): add GlobalEvent SSE integration and backward compat t…
Apr 16, 2026
61418c8
fix(opencode): resolve ruff lint violations (D301, D403, TC001)
Apr 16, 2026
5ba4c53
refactor(opencode-server): add global_router to test app fixture and …
Apr 16, 2026
5d4fc83
fix(opencode-server): add SSE stream fixture and broadcast_event exce…
Apr 16, 2026
d4b9c52
refactor(opencode-server): add docstrings and improve code quality in…
Apr 16, 2026
98ab5dd
test(opencode-server): add SSE integration tests for /global/event
Apr 16, 2026
cc0f11a
test(opencode-server): add on_first_subscriber callback tests
Apr 16, 2026
9b708c3
test(opencode-server): add client disconnect cleanup tests
Apr 16, 2026
a075a3a
test(opencode-server): add _extract_session_id exhaustiveness tests
Apr 16, 2026
fa3fbb6
test(opencode-server): add concurrent subscriber tests
Apr 16, 2026
3d543db
test(opencode-server): add ServerState.broadcast_event direct tests
Apr 16, 2026
1dedc2b
test(opencode-server): add /global/health endpoint tests
Apr 16, 2026
10371e5
test(opencode-server): add GlobalEvent edge case tests
Apr 16, 2026
28b9aec
fix(opencode-server): OpenCode v1.4.0 compat — variant nesting, FileD…
Apr 17, 2026
537ed25
fix(opencode-server): align SSE event protocol and session state
Apr 17, 2026
615197d
fix(opencode-server): serialize async prompt dispatch
Apr 17, 2026
4c094c7
test(opencode-server): cover session concurrency reloads
Apr 17, 2026
7130ad5
fix(opencode-server): cancel session background tasks on abort
Apr 17, 2026
d3d30ea
fix(opencode-server): split question ids from permissions
Apr 17, 2026
d907bc9
fix(opencode-server): wrap global heartbeat events
Apr 17, 2026
309dbe0
test(opencode-server): update sse compliance coverage
Apr 17, 2026
79b69c6
fix(opencode-server): silence no-session event warnings and optimize …
Apr 17, 2026
1508379
test(opencode-server): cover no-session events and unknown-type warning
Apr 17, 2026
404a3a5
fix(opencode-server): add has_pending_permission to avoid private att…
Apr 17, 2026
d20bbf2
fix(opencode-server): extract _event_to_dict and document shared-agen…
Apr 17, 2026
cf2f620
fix(opencode-storage): add project persistence support for OpenCode s…
Apr 18, 2026
0656ef4
fix(opencode-server): add OTLP compatibility sinks for 1.4.4+
Apr 18, 2026
8bea4af
fix(opencode-server): add global compatibility routes for newer clients
Apr 18, 2026
0c3f412
fix(opencode-server): unblock queued prompts on per-turn completion
Apr 18, 2026
c2ecb82
feat: 修复事件监控问题
Apr 18, 2026
0171ea1
fix(opencode-server): snapshot shared agent state for turns
Apr 19, 2026
e74a8e3
fix(opencode-server): narrow shared agent locking on route paths
Apr 19, 2026
53c4339
test(opencode-server): cover concurrent session coexistence
Apr 19, 2026
ca38535
fix(opencode-server): fix hooks and session-creation race conditions
Apr 19, 2026
798cf79
feat: 修复501日志内部错误
Apr 19, 2026
64241a6
fix(opencode-server): pass snapshot through BaseAgent.run
Apr 19, 2026
9f56140
fix(opencode-server): bound SSE subscriber queues
Apr 19, 2026
a1053b8
fix(opencode-server): normalize routing-check paths
Apr 19, 2026
6fc4dc2
fix(opencode-server): avoid idle flicker during async handoff
Apr 19, 2026
bd4b549
fix(opencode-server): bind snapshots to resolved agent
Apr 19, 2026
fa06305
fix(opencode-server): stabilize snapshot and async handoff
Apr 19, 2026
30c0efb
fix(opencode-server): clean up provider lookups and paths
Apr 19, 2026
d5414c1
feat: 解决 gimini code review
Apr 20, 2026
1f7865c
feat: todo future
Apr 20, 2026
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
493 changes: 493 additions & 0 deletions docs/architecture/session-isolation-analysis.md

Large diffs are not rendered by default.

192 changes: 192 additions & 0 deletions docs/rca/opencode-1.4.4-compat-and-queue-unblock.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,192 @@
# RCA: OpenCode 1.4.4+ compatibility and queued prompt unblock

## Summary

This issue was caused by **three separate but related problems**:

1. **Protocol compatibility gaps for OpenCode 1.4.4+**
- missing OTLP compatibility endpoints
- missing `/global/*` compatibility routes

2. **Incorrect completion signaling for queued prompts**
- the server only emitted completion when the **entire async queue** drained
- the client expected a **per-turn completion signal** (`session.idle`)

3. **A route-owned timeout on synchronous `/message` processing**
- the server wrapped the whole streamed turn in a hard `asyncio.timeout(...)`
- long silent waits (especially question / permission waits) were treated as route failures

Together, these caused:
- `405 Method Not Allowed` during OTLP export
- startup/runtime compatibility issues with newer OpenCode clients
- delayed exit after the first interaction
- queued prompts appearing stuck until much later
- `500 Internal Server Error` when a sync OpenCode turn stayed silent for too long
- follow-up `/question/{id}/reply` requests returning `404` after the server had already torn down the pending question

---

## Symptoms

- OpenCode `1.4.3` worked normally
- OpenCode `1.4.4+` showed failures
- logs included:
- `Failed to export metrics batch code: 405, reason: Method Not Allowed`
- after the first interaction:
- the client could not exit promptly
- later prompts were queued
- the previous turn was not considered finished in real time
- after a longer silent wait during `/message` processing:
- the route failed with `TimeoutError`
- the agent stream saw `CancelledError`
- later question replies could hit `404 Not Found`

---

## Root Cause

### 1. Missing OTLP endpoints
Newer OpenCode clients send:
- `POST /v1/metrics`
- `POST /v1/traces`
- `POST /v1/logs`

These requests were not explicitly handled and fell through to a catch-all route that only supported `GET/HEAD/OPTIONS`, resulting in `405`.

### 2. Missing global compatibility routes
The server did not provide:
- `GET /global/config`
- `PATCH /global/config`
- `POST /global/dispose`
- `POST /global/upgrade`

This created compatibility issues for newer client lifecycle flows.

### 3. Completion signaling was too coarse
Queued async prompts only triggered `session.idle` when the **whole queue** drained.

However, the client depends on `session.idle` as the signal that:
- the current turn is done
- input can be unblocked
- the session can continue or exit

As a result, a completed queued turn could still appear unfinished to the client.

### 4. Busy-session enqueue did not guarantee worker startup
If an async prompt was enqueued while a synchronous `/message` turn was already running, the queue item could exist without a guaranteed worker handoff immediately after the sync turn completed.

### 5. `/message` used request/response timeout semantics for an event-driven interaction
OpenCode session turns are not purely request/response.

During a single `/message` turn, the server may legitimately spend a long time with no streamed model output while it is:
- waiting for a tool approval
- waiting for a question reply
- waiting for other user-driven side-channel events

The server wrapped the entire sync turn in a hard timeout:

- `async with asyncio.timeout(STREAM_TIMEOUT_SECONDS)`

When that timeout fired, it cancelled the active stream, which produced the observed chain:

- `TimeoutError` at the route level
- `CancelledError` inside the agent's event queue wait
- `500` returned from `POST /session/{id}/message`
- cleanup of pending question state, causing later `/question/{id}/reply` to return `404`

The key mistake was treating "no stream output yet" as equivalent to "the turn is broken". In this protocol, a silent turn can still be healthy because progress may be happening through separate events.

---

## Fix

### OTLP compatibility
Added minimal compatibility sinks:
- `POST /v1/metrics`
- `POST /v1/traces`
- `POST /v1/logs`

These return success and intentionally discard payloads.

### Global compatibility routes
Added minimal safe compatibility routes:
- `GET /global/config`
- `PATCH /global/config`
- `POST /global/dispose`
- `POST /global/upgrade`

Behavior is intentionally minimal:
- config routes reuse existing config behavior
- dispose/upgrade are safe stubs with no destructive side effects

### Queued prompt unblock
Updated queue/session lifecycle handling so that:
- each queued async turn emits a **per-turn** completion signal
- full `mark_session_idle()` only happens when the queue is truly empty
- sync `/message` completion immediately hands off to queued async work
- async enqueue always ensures a worker exists

This was implemented by:
- adding queue/worker helper methods in `ServerState`
- adding `emit_session_turn_complete(session_id)`
- updating async queue draining logic in `message_routes.py`
- updating sync-to-async handoff behavior

### Long-wait sync turn handling
Updated synchronous `/message` processing so that it no longer applies a route-owned hard timeout to the entire agent stream.

Behavior now:
- the sync turn remains alive as long as the underlying work is still active
- question / permission flows can complete through the normal event endpoints
- the route no longer converts a legitimate silent wait into `CancelledError` + `TimeoutError` + `500`

This was implemented by:
- removing the `asyncio.timeout(...)` wrapper around `adapter.process_stream(iterator)` in `message_routes.py`
- preserving the existing event-driven lifecycle instead of forcing request-timeout semantics onto it

---

## Validation

Targeted regression coverage was added for:
- OTLP compatibility endpoints
- `/global/*` compatibility routes
- queued async prompt worker startup
- per-turn `session.idle` signaling
- long-running sync `/message` turns that stay silent before resuming
- concurrency behavior
- SSE/global event compliance
- OpenCode storage/project persistence

### Result
- **256 passed**
- **6 skipped**

---

## Key Lessons

1. **End of streamed output is not the same as turn completion**
- clients often depend on explicit lifecycle events, not just stream exhaustion

2. **Queue completion and turn completion are different concepts**
- signaling only when the whole queue drains is too coarse for interactive clients

3. **A silent turn is not necessarily a hung turn**
- OpenCode uses side-channel events (`question`, permission, SSE lifecycle) during active work
- route-level hard timeouts can break valid interactions by destroying state the client still depends on

4. **Compatibility fixes should start with minimal safe behavior**
- protocol recovery first, full functionality later

5. **Do not trust delegated implementation blindly**
- route-level compatibility fixes must be verified by reading actual code and running focused tests

---

## Related commits

- `5537d2d7f` `fix(opencode-storage): add project persistence support for OpenCode storage`
- `6de476f92` `fix(opencode-server): add OTLP compatibility sinks for 1.4.4+`
- `e28090479` `fix(opencode-server): add global compatibility routes for newer clients`
- `c28e2f3f1` `fix(opencode-server): unblock queued prompts on per-turn completion`
Original file line number Diff line number Diff line change
@@ -1,12 +1,12 @@
---
rfc_id: RFC-0022
title: OpenCode v1.4.4+ GlobalEvent Protocol Support
status: DRAFT
status: ACCEPTED
author: yuchen.liu
reviewers: []
created: 2026-04-15
last_updated: 2026-04-15
decision_date:
last_updated: 2026-04-16
decision_date: 2026-04-16
related_prds: []
related_rfcs:
- RFC-0013-subagent-event-unification.md
Expand Down Expand Up @@ -635,33 +635,46 @@ No new endpoints. Changes are limited to the SSE data format:

---

## Decision Record
## Implementation Status

> Implemented 2026-04-16. Key deviations from the original design:

| # | Deviation | Rationale |
|---|-----------|-----------|
| 1 | `GlobalEventFactory` placed in `global_routes.py` instead of `state.py` | Avoids circular imports — `state.py` cannot import from `models/events.py` without creating a dependency cycle |
| 2 | `wrap()` returns `str` (JSON) instead of `GlobalEvent` model instance | The SSE generator needs serialized JSON strings; returning the model would require the caller to serialize, adding unnecessary coupling |
| 3 | Reuses `_serialize_event()` for payload generation | The existing `_serialize_event()` already handles event serialization correctly; duplicating that logic in the factory would violate DRY |
| 4 | Uses `json.dumps(ensure_ascii=False)` instead of `model_dump_json()` | `model_dump_json()` escapes non-ASCII characters by default; `ensure_ascii=False` preserves Unicode content in event payloads |

---

> Complete this section after RFC review is concluded.
## Decision Record

### Decision

**Status**: PENDING
**Status**: ACCEPTED

**Date**:
**Date**: 2026-04-16

**Approvers**:
**Approvers**: yuchen.liu

### Decision Summary

[To be completed after review]
Accepted Option 2 (Formal GlobalEvent Model) with pragmatic implementation deviations documented above.

### Key Discussion Points

[To be completed after review]
- Circular import issue required moving factory out of `state.py`
- Serialization strategy prioritized simplicity and DRY over strict model-driven design

### Conditions of Approval

[To be completed after review]
- Must not break the existing `/event` endpoint
- Must pass manual testing with OpenCode v1.4.4+ TUI

### Dissenting Opinions

[To be completed after review]
None

---

Expand Down
Loading