Skip to content

Add OpenCode as a synthesis backend - #80

Merged
milanshen merged 14 commits into
mainfrom
feat/synthesis-opencode
Aug 18, 2026
Merged

Add OpenCode as a synthesis backend#80
milanshen merged 14 commits into
mainfrom
feat/synthesis-opencode

Conversation

@milanshen

@milanshen milanshen commented Aug 17, 2026

Copy link
Copy Markdown
Contributor

Context

Session debriefs could only run through the Claude Code or Codex CLIs. With
neither installed, synthesis could not be enabled at all — the Settings dialog
left it off because no backend reported as available. OpenCode was already a
first-class read source, so the gap was only on the invoke side.

Changes

  • OpenCode as a third synthesis backend — detected on PATH like the others.
    • Offers the free OpenCode Zen models, plus Whichever model OpenCode is set to use.
    • That list comes from opencode models opencode --verbose, cached for ten minutes
      and filtered to zero-cost models that OpenCode has not deprecated.
    • The probe runs from an empty directory with --pure and project config off.
      OpenCode loads project plugins from the working directory, so a probe inheriting
      the collector's cwd would run repository code on opening Settings. Global config
      is left intact, since enumerating models needs its providers.
    • It stays a probe rather than a read of the embedded price table: only 7 of the 27
      free models OpenCode has published are still live, that table carries no
      retirement signal, and it freezes at release while the free tier turns over in
      weeks.
    • Defaults to opencode/deepseek-v4-flash-free, run at its high reasoning variant.
    • That last option passes no --model, leaving OpenCode to resolve one itself:
      its configured model key, else the model last selected in the CLI.
    • Paid providers are reached that way rather than listed — OpenCode resolves them
      from ambient credentials, and the full list runs to hundreds of entries.
  • Compensating for flags OpenCode does not have
    • No structured output → schema described in the prompt, reply parsed leniently,
      then checked for the fields the schema marks required. A partial object now
      fails into cooldown rather than being cached as a debrief.
    • No system-prompt flag → instructions ride along with the message.
    • No tool flags → tools denied through an injected config.
    • No ephemeral mode → each run gets its own scratch database under
      ~/.coslash/synthesis, removed once the run ends, so runs never enter the
      user's own OpenCode history.
    • Per run rather than shared: OpenCode instances that overlap in time deadlock
      during init on a shared database, and the manager runs up to four at once.
    • A crash skips the cleanup, so directories older than an hour are swept at
      startup. The cutoff sits well past the 90s run timeout, so a second collector
      on --port cannot lose a live run's database to the sweep.
    • --dir passed explicitly, since OpenCode reads PWD before the process
      working directory.
  • Model validation relaxed to a shape check, across all three backends.
    • The dropdown still offers the same curated options.
    • settings.json now accepts any model the chosen CLI can reach, including one
      behind an API proxy such as ANTHROPIC_BASE_URL.
    • Ids starting with - are rejected, so a value cannot be read as a CLI flag.
  • Each backend's default model runs at high reasoning effort.
    • Codex: -c model_reasoning_effort=high; OpenCode: --variant high; Claude: --effort high.
    • Scoped to the default model on each backend, so an explicitly chosen model
      keeps the CLI's own default.
    • Note --effort works on claude-haiku-4-5 through the CLI even though the
      Messages API rejects output_config.effort for that model.
  • Debrief outcomes are no longer truncated, for every backend.
    • Previously capped at 200 characters with a trailing ellipsis; now only
      whitespace-collapsed. Goals, decisions, and next step keep their caps.

Test

  • Automated
    • go build ./... && go vet ./... && go test ./... — passed
    • Unit tests cover the OpenCode parse path: incomplete objects rejected
      (including null goals), bare, fenced, and prose-wrapped objects accepted.
    • Unit tests cover JSON framing (nesting, braces and escapes inside strings) and
      the scratch sweep (abandoned removed, in-flight and unrelated left alone).
    • npm run build && npm test && npm run lint — passed
  • Manual
    • Real debriefs generated through all three backends: Claude, Codex, and OpenCode.
    • Reasoning-effort flags confirmed with logging shims on PATH — present for each
      backend's default model, absent for a non-default one.
    • Scratch database verified: after a full run, zero synthesis sessions in the
      user's OpenCode database and one in the scratch file. Verified before the
      database became per-run; concurrent runs not yet exercised by hand.
    • Probe isolation verified against OpenCode 1.18.18: a repo carrying a marker
      plugin in .opencode/plugin and .opencode/plugins executed it on the old
      invocation, and did not on the new one, which still returned the same 7 models.
    • Outcome verified untruncated end to end (512 and 685 characters, no ellipsis).
    • Validation matrix: curated and proxy-style ids accepted; flag-shaped and
      whitespace ids rejected.
    • Free-model list cross-checked against OpenCode's registry: the 7 models the CLI
      reports match filtering models.opencode.ai/api.json on zero cost and
      non-deprecated status, exactly.

Screenshots

  • Settings dialog — synthesis on, three backend cards, OpenCode selected with the model dropdown open
Screenshot 2026-08-17 at 5 10 21 PM
  • Session inspector — a DEBRIEF card generated by the OpenCode backend
Screenshot 2026-08-17 at 5 11 57 PM

@milanshen milanshen added the enhancement New feature or request label Aug 17, 2026
@milanshen
milanshen force-pushed the feat/synthesis-opencode branch 4 times, most recently from 63df8e6 to 520f3e0 Compare August 18, 2026 00:09
@milanshen
milanshen marked this pull request as ready for review August 18, 2026 20:59
@milanshen
milanshen force-pushed the feat/synthesis-opencode branch from 520f3e0 to d1b766a Compare August 18, 2026 21:17
Comment thread collector/internal/synthesis/opencode.go Outdated
Comment thread collector/internal/synthesis/opencode.go Outdated
Comment thread collector/internal/settings/settings.go Outdated
Comment on lines +24 to +35
// Passes no model, leaving OpenCode to resolve one: its configured model
// key, else the model last selected in the CLI, which varies between runs.
OpenCodeDefaultModel = "default"

// The one free model with a reasoning variant, which the runner turns up.
OpenCodeSynthesisModel = "opencode/deepseek-v4-flash-free"

// The default Codex model, which the runner runs at high reasoning effort.
CodexSynthesisModel = "gpt-5.6-luna"

// The default Claude model, likewise run at high reasoning effort.
ClaudeSynthesisModel = "claude-haiku-4-5"

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

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

may become outdated upon new model releases, but only might become an issue if model is deprecated

Copy link
Copy Markdown
Contributor Author

Choose a reason for hiding this comment

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

Agreed, and it applies to all three constants equally. Leaving them pinned: each one also gates the reasoning-effort flag, so it has to name a specific model rather than track whatever the CLI currently defaults to. A deprecation shows up as a run failure with the CLI-auth-or-model message, and since validation is now a shape check, the fix is a one-line constant bump or a user-set model in settings.json.

@milanshen
milanshen force-pushed the feat/synthesis-opencode branch from 0ac69f4 to 8fc0d6e Compare August 18, 2026 21:54
@milanshen
milanshen requested a review from calvintvu August 18, 2026 22:18
Comment thread collector/internal/vendors/opencode/models.go Outdated
Comment thread collector/internal/synthesis/runner.go
Comment thread collector/internal/synthesis/opencode.go
Comment thread collector/internal/synthesis/runner.go Outdated
Comment thread collector/internal/settings/settings.go Outdated
@milanshen
milanshen force-pushed the feat/synthesis-opencode branch from cebb081 to 4f91c2b Compare August 18, 2026 22:44
@milanshen
milanshen requested a review from calvintvu August 18, 2026 23:06
if err := json.Unmarshal(data, &fields); err != nil {
return fmt.Errorf("decode synthesis result: %w", err)
}
for _, name := range []string{"goals", "outcome", "keyDecisions", "nextStep"} {

@calvintvu calvintvu Aug 18, 2026

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

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

[P2] Validate required values, not only key presence

This loop proves only that the keys exist, and the later check validates only the pre-normalized goals length. encoding/json accepts null for string and slice fields and accepts null array elements as empty strings. As a result, {"goals":["ship"],"outcome":null,"keyDecisions":null,"nextStep":null} succeeds and is cached; keyDecisions then serializes as null while SessionInspector and handoffBrief call .length on it. {"goals":[null],"outcome":"done","keyDecisions":[],"nextStep":"ship"} also succeeds before the null goal normalizes away. Please reject null required values and null array elements before parsing and caching.

Copy link
Copy Markdown
Contributor Author

Choose a reason for hiding this comment

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

Fixed in ad60fd2 — OpenCode synthesis validation now rejects null required values and null array elements before parsing and caching.

@milanshen
milanshen force-pushed the feat/synthesis-opencode branch from d519e49 to 630b20d Compare August 18, 2026 23:34
@milanshen
milanshen merged commit 83355d9 into main Aug 18, 2026
3 checks passed
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

enhancement New feature or request

Projects

None yet

Development

Successfully merging this pull request may close these issues.

2 participants