Skip to content

Design extensible Session error details #35325

Open

Description

@kitlangton

Context

#35272 intentionally keeps the public Session error wire contract minimal and forward-compatible:

type SessionError = {
  type: string
  message: string
}

Core may classify failures more precisely internally, but internal provider, permission, and tool fields should not become permanent public commitments without demonstrated consumers and an explicit compatibility model.

Goal

Design the next public Session error model, if richer machine-readable details are justified, without requiring database resets or breaking older SDK clients when new error classifications or fields are introduced.

Investigation

  1. Inventory concrete consumer needs across the TUI, App, SDK users, automation, telemetry, retries, authentication recovery, permission display, and tool diagnostics.
  2. Audit established error contracts and document the tradeoffs behind them:
    • RFC 9457 Problem Details
    • JSON:API error objects
    • Stripe and GitHub REST errors
    • GraphQL errors and extensions
    • gRPC Status and typed details
    • OpenAI and other relevant model-provider APIs
  3. Compare at least these candidate shapes with ideal consumer call sites:
    • Minimal open { type, message }
    • Open envelope plus optional JSON details or data
    • Stable coarse category plus open type
    • Known typed details layered over an open wire envelope
    • Versioned rich error contracts
  4. Pressure-test unknown future types, malformed known details, persisted historical events, mixed-version clients, generated SDK ergonomics, and whether a generic field bag becomes an ungoverned junk drawer.
  5. Separate internal failure algebras from the public transport contract and identify which information belongs in logs, events, dedicated fields, or error details.

Compatibility Laws

  • Older clients must accept newly introduced error type values.
  • Unknown types must remain renderable through message.
  • New optional fields must not make historical durable events undecodable.
  • No required per-type field may be added without an explicit versioning and migration decision.
  • Retry policy must not depend on public error details unless that behavior is deliberately part of the contract.
  • Public type values must not be renamed or repurposed after release.

Deliverables

  • A survey table of analogous APIs and the constraints each design addresses.
  • An inventory of actual OpenCode consumer requirements.
  • Two or three candidate wire contracts with generated TypeScript examples and fallback handling.
  • A recommended contract, naming rationale (type, code, name, details, or data), and compatibility laws.
  • A persistence/versioning decision and migration analysis.
  • Focused schema and mixed-version compatibility tests for the selected design.

Non-goals

  • Do not expose the current internal error union merely because it already exists.
  • Do not add a generic details bag until at least one concrete consumer and governance rule justify it.
  • Do not change retry semantics as part of the error-shape design.

Activity

  1. added
    discussionUsed for feature requests, proposals, ideas, etc. Open discussion
    coreAnything pertaining to core functionality of the application (opencode server stuff)
    gang-grillDesign topics queued for later group grilling
    on Jul 4, 2026
  2. Dante-dan commented on Oct 1, 2026

    @Dante-dan
    Contributor

    I surveyed the current consumers at dev 0112a92. My recommendation is to retain the two-field envelope first, make its compatibility boundary explicit, and defer additional fields until a migrated consumer requires them. There is one concrete candidate for richer context—ACP auth recovery—but the current consumer uses the legacy contract.

    Actual consumers and boundaries

    Consumer Current need / behavior Implication for the new Session contract
    App Session message projection Maps the new message error to legacy UnknownError; recognizes cancellation using type substrings abort/interrupt. message already supplies display; classification should use documented stable exact values rather than substring matching once types exist. No new details needed for this path.
    App retry display Uses scheduled retry attempt, next time and error message. Retry display is a separate event concern; adding retryable to every Session error is unnecessary here.
    TUI notifications Legacy session.error: special-cases MessageAbortedError and the text SSE read timed out; otherwise generic notification. Cancellation/timeout classification is useful, but stable type + message can cover it. This is not yet a consumer of the new envelope.
    App notifications Persists legacy event error; OS notification uses session title or generic fallback, not structured context. No demonstrated need for provider/tool fields. Stored notification history is another compatibility boundary.
    CLI automation Legacy event: renders data.message/name and emits the error object as JSON. Migration needs explicit output compatibility; text fallback suffices for rendering. Downstream consumers outside this repository remain unverified.
    ACP prompt completion Legacy classifications determine cancelled/max_tokens/refusal; ProviderAuthError.data.providerID produces AuthRequiredError. Concrete machine-action consumer: provider identification supports auth recovery. Before adding it publicly, decide how a migrated ACP consumer obtains it, whether session/model context suffices, and how absence falls back to service failure.
    App error page Formats init/runtime error chains, including provider ID, HTTP status, response body and retryability. This handles a different error surface. Its diagnostics do not justify copying all those fields into durable Session errors.
    Tool failure publisher Publishes failure message with call ID / assistant message ID / provider context on the enclosing tool event. Repeating tool IDs in a generic error details bag would duplicate context; retain the event boundary.
    SDK / generated clients New message errors currently generate type: "unknown"; legacy event errors remain a name/data union. Open runtime acceptance and generated type acceptance must be audited separately. A typed SDK is not evidence that new arbitrary values are already supported.
    Telemetry / external SDK integrations No concrete additional-field requirement established in this source survey. This is a coverage gap, not evidence that no external consumer exists. Do not expose raw provider payloads on that assumption.

    An important difference from #35272's intended open contract: at this revision, SessionMessage.UnknownError uses Schema.Literal("unknown"), and SessionErrorUnknown also has the literal type. The generated client repeats that constraint. These readers cannot be assumed forward-compatible with a new type merely because the payload still has two fields. This is static source evidence, not a deployed mixed-version test result.

    External designs and candidate shapes

    Shape / precedent Useful property Tradeoff for Session
    Minimal {type,message} Open classification, universal display fallback Best starting point; requires truly open reader schemas and stable names.
    RFC 9457 {type,title,status,detail,instance,...extensions} Defines ignoring unknown extensions; discourages parsing human detail HTTP status / request instance do not naturally describe every persisted session/tool failure. Borrow compatibility laws, not the whole HTTP shape.
    JSON:API errors array of {code,title,detail,source,meta,...} Good field/pointer attribution and multiple validation errors Array/pointer semantics add little to a single Session failure with context already on its event.
    Stripe category + message + code + optional param/context Separates actionable machine code from prose A useful model only when a consumer acts on an explicitly supported code/field.
    GitHub REST message + documentation URL + endpoint-specific errors Separates general explanation from resource/field failures Request/endpoint details should not become universal Session obligations.
    GraphQL message + path/locations + extensions Separates execution context from diagnostic extension data Unrestricted extensions can become the junk drawer this issue excludes; governance remains necessary.
    gRPC status code + message, optional typed rich details Rich details can be separately typed and optional Codec/type-registry complexity and uneven client support are costly for durable JSON events.
    Closed per-type union mirroring internal failures Strong narrowing inside core Couples transport to internal provider/tool algebra and rejects future variants unless an open fallback is real.

    Three bounded options:

    // A: retain the minimal public shape; recommended first step.
    type SessionFailure = { type: string; message: string }
    
    // B: only if migrated ACP auth recovery needs public provider identity.
    // Missing context remains valid, including on historical events.
    type SessionFailureWithProvider = SessionFailure & {
      providerID?: string
    }
    
    // C: a separately governed extension protocol; do not add by default.
    // Only registered keys have documented consumers, schemas and owners.
    type SessionFailureWithExtensions = SessionFailure & {
      extensions?: { "opencode.auth.v1"?: { providerID?: string } }
    }
    
    // All three: a client must keep the message fallback for unknown types,
    // absent context, and unrecognized optional extensions.
    function display(error: SessionFailure): string {
      return error.message
    }

    A avoids an extra machine code competing with type. B makes provider identity a public lifetime commitment but is simpler than C if that is the only proven need. C is justified only by multiple independently evolving consumers; one auth field does not justify an extensibility framework. For future stable names, use semantic lower-case values, not internal class names or provider SDK names; do not emit new values until the reader transition is decided. Existing unknown must retain its meaning.

    Compatibility and persistence decision

    1. Widen readers before writers emit new type values. Existing literal-only generated clients/schema decoders are a real constraint: an unversioned server cannot promise compatibility by just changing its latest SDK. Until old-reader behavior is verified or a capability/version boundary exists, keep emitting unknown to those readers. Do not pretend a string catch-all added today repairs yesterday's client.
    2. Keep message required and usable for every valid error. Unknown types render it; known types with missing optional context render it too. Never use substring classification or parse message text to drive auth/retry decisions.
    3. Additional context stays optional on new and historical events; no reset/backfill is needed for an additive field. This does not make incompatible new discriminator values additive. Historical literal-unknown events remain valid forever.
    4. Separate producer validation from defensive consumer handling. Malformed optional context should disable specialization and preserve a valid message; a missing/non-string message needs a bounded generic fallback at the consumer boundary, not an invented successful schema decode.
    5. Keep internal provider failures, permission algebra, raw responses/headers and retry decisions internal or on their existing dedicated surfaces. The existing RetryError already has a distinct schema; this proposal does not change retry behavior.
    6. If required per-type context ever becomes necessary, make that a separate version/migration decision. Do not silently convert optional provider context into a required discriminated union.

    After design approval, focused checks should cover: historical unknown replay; future unknown type + message; extra optional fields; missing/wrong-shaped optional context; malformed required fields; generated SDK compile/schema decode; old reader/new writer and new reader/old writer; and no change to retry/auth fallback behavior. The literal-only old-reader case should be included as an expected incompatibility until the transition resolves it, rather than hidden by a new-SDK-only test.

    Could the core team review A first, with the reader/writer transition explicitly included, and keep B conditional on ACP migration needing provider identity? This survey is static analysis; no wire/schema implementation or runtime tests have been performed.

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Metadata

Metadata

Assignees

No one assigned

    Labels

    2.0coreAnything pertaining to core functionality of the application (opencode server stuff)discussionUsed for feature requests, proposals, ideas, etc. Open discussiongang-grillDesign topics queued for later group grilling

    Type

    No type

    Projects

    No projects

      Milestone

      No milestone

      Relationships

      None yet

      Development

      No branches or pull requests

      Issue actions