- sessions list as left rail (was full-page list view)
- group sessions by Today/This Week/Last Week/Older
- active row uses subtle bg (was 2px border)
- status dots only render for non-ready states
- header shows agent name from agentNameById fallback
- composer in rounded bordered container with consolidated footer
- stop button is filled-dark SVG square (was bold ascii character)
- markdown sizes tightened (h1 17px, h2 15px, h3 13.5px, body 13.5/1.6)
- user messages render as bold quote-headers with hairline separator
- redirect /sessions to latest session
test_create_agent_same_name_different_user_returns_200 requested monkeypatch
but only used app.dependency_overrides. Pyright flagged the parameter as
unaccessed; remove it.
Use getattr with a None default and isinstance narrowing instead of hasattr to
unwrap prisma.Json-style configs. Pyright was flagging .data access on the
dict branch of the union; this rewrites the path so the type narrows cleanly
without an Any cast.
Verifies agent ownership before listing (404 for unknown/unowned agents).
Sessions are scoped by created_by independent of the agent check, so a
mis-owned session row cannot leak. Optional ?status= filter, same offset
cursor as GET /v2/agents. sandbox_url and sandbox_metadata stripped per
contract \xc2\xa76.2/\xc2\xa76.3.
Both scoped by created_by, ordered by created_at desc, simple offset-based
pagination via take/skip. list_sessions_for_agent supports an optional
SessionStatus filter.
Used by the new GET /v2/agents and GET /v2/agents/:agent_id/sessions
listing endpoints. Each list response carries data + next_cursor + has_more
for simple offset-based pagination.
send_message:
- assert {providerID, modelID} object body (not string)
- 'openrouter/anthropic/claude-3' splits on first '/' only
- models without '/' omit the field entirely
- model=None omits the field
list_messages: assert {info, parts} envelopes normalize to MessageRow,
with reasoning/step-start/step-finish filtered out of content and the
combined providerID/modelID showing up as our model string.
stream_events: assert text deltas route through partID -> type tracking,
the partID map persists across multiple message.part.updated events,
session.idle is enriched with the tracked assistant message_id, and
permission.asked triggers a fire-and-forget POST to
/session/:sid/permissions/:per_id with {response:'once'}.
Update fixtures to use the {info, parts} envelope, epoch-ms timestamps,
camelCase wire field names (providerID/modelID), and the message.part.delta
event shape. Add coverage for:
- filtering step-start/step-finish/reasoning parts out of content
- tool state.status branches (running/completed/error/pending)
- the part_types map populated by message.part.updated
- message.part.delta routing via part_types lookup (text vs reasoning,
unknown partID drops)
- message.updated -> message.completed when info.time.completed is set
- session.error unwrapping the nested error.data.message string
- server.heartbeat / session.created drop to None
send_message: opencode 1.14.41 rejects string-form model with 400; split
our '<provider>/<model>' on the first '/' into the {providerID, modelID}
object form. Omit the field entirely if there's no '/' so opencode falls
back to its default agent/model.
stream_events: maintain a per-stream partID -> type map populated from
message.part.updated events so message.part.delta events can be routed
to text/reasoning. Track the in-flight assistant message_id from
message.updated so session.idle events (which carry only sessionID in
real opencode) can be enriched before yielding message.completed.
permission gating: opencode emits permission.asked events that block
tool calls until granted. Auto-grant via fire-and-forget
POST /session/:sid/permissions/:per_id body {response:'once'} so MVP
tool calls don't hang. Will surface as a real event type once we expose
permission gating to v2 callers.
opencode messages are nested as {info, parts}, not flat. Field naming on
the wire is camelCase with capital-ID suffix (sessionID, messageID,
partID, callID, providerID, modelID). Timestamps are epoch ms ints.
Part types include text/reasoning/tool/step-start/step-finish — only
text parts go into content; only tool parts go into tools. Tool I/O
lives under part.state.input / part.state.output (or .error).
Streaming text deltas come via message.part.delta events with
{messageID, partID, field, delta} — the part type is NOT on the delta
event, so normalize_opencode_event now takes a part_types dict the
caller maintains across the stream. session.idle has no messageID in
real opencode payloads — emit message.completed with message_id=None
and let the adapter enrich it from tracked state. session.error
unwraps the nested error.data.message string.
Cover idle_timeout_minutes <= timeout_minutes contract \xc2\xa76.2 invariant plus
existing timeout_minutes [1, 1440] bound. New test_types.py keeps typing
concerns separate from endpoint tests in test_agents.py.
Per contract \xc2\xa76.2, idle_timeout_minutes must be in [1, timeout_minutes].
Previously only ge=1 was enforced, so idle=1440 with timeout=10 would pass.
Add a Pydantic v2 model_validator(mode="after") that raises ValueError when
idle_timeout_minutes > timeout_minutes; FastAPI surfaces it as a 422.