fix(harness): classify OpenCode v2 errors and retain snapshots

Merged
TB
8d ago
+321-20

Summary

Follow up on the merged OpenCode V2 adapter (#3367) with typed 404 handling and changed-file timeline preservation, adapted to current main after #3456.

Changes

  • Decode V2 _tag errors on session routes, including message history. Only SessionNotFoundError means the session is unavailable; missing commands, models, locations, permissions, and forms retain execution-error diagnostics. Malformed or oversized error bodies cannot establish session absence.
  • Preserve typed V2 errors during resumed-session setup and reconciliation, while retaining unavailable/waiting handling for host outages and missing sessions. Regression tests check that setup errors do not interrupt or replace user-supplied sessions.
  • Normalize assistant snapshot.files into a stable patch event. Append it after usage normalization so a snapshot appearing during refresh does not duplicate the usage timeline event. Repeated refresh tests verify updated files, stable sequence/timestamp, and no redundant progress update.
  • Extend real OpenCode 2.1.4 validation with an unknown command, a real file write and snapshot, and missing-session responses. Existing question/resume and cancellation-settlement checks remain in that test.
  • Link managed OpenCode sessions from the README action table.

Scope boundary

Command timeout policy is deliberately not changed. The old handoff removed the HTTP timeout using the caller context; current upstream instead uses context.WithoutCancel to learn the outcome of potentially accepted submissions. Removing its remaining bound would require a separate decision. Admission, deduplication, interruption, and settlement logic from #3456 is not replaced.

A separate temporary diagnostic against the new patch and OpenCode 2.1.4 confirms the remaining gap: a command containing a 31-second shell substitution returned after 30.568 seconds with unavailable, PromptSent=true, and zero provider calls. This diagnostic is not part of the submitted patch. The preexisting command-admission failure path's unconditional interrupt also remains outside this PR's scope.

Validation

New regressions were run before their fixes and failed for typed 404 classification, snapshot omission, resumed-host routing, swallowed history errors, malformed trailing JSON, and snapshot/usage timeline duplication.

Verified on Linux with Go 1.27.3 and OpenCode 2.1.4, using isolated temporary state and a local deterministic HTTP provider:

go test -race -count=1 ./internal/runtime/builtin/harness/... ./internal/opencodehost/...
go test -race -run '^TestV2' -count=20 ./internal/runtime/builtin/harness/
DAGU_TEST_OPENCODE_V2_EXECUTABLE=/path/to/opencode-2.1.4 \
  go test -race -run '^TestRealV2Run$' -count=2 ./internal/runtime/builtin/harness/
go vet ./internal/runtime/builtin/harness/... ./internal/opencodehost/...
go fix -embedlit=false -diff ./internal/runtime/builtin/harness/... ./internal/opencodehost/...

The affected package race suite passed; the real-V2 test passed twice; scoped vet/fix and formatting checks produced no findings. Repository-configured golangci-lint 2.13.2 reported zero issues. Independent review findings were addressed and re-reviewed.

Validation limitations: directory-boundary-corrected lint exposes the same 22 findings on untouched upstream and this patch; the changed-lines comparison reports zero new issues (see #3465). Windows/Darwin lint attempts exceeded three-minute package-loading budgets. Full make fmt/make check, repository-wide Go tests, frontend/API suites, and conformance were not run; this is a package-scoped harness change with no schema, frontend, workflow-language, or CLI changes.

Related Issues

Closes #3467 (tracking issue for the fixes in this PR).

Replaces the remaining applicable work from philo-x/dagu#1. Related: #3367, #3456, and the lint-exclusion follow-up #3465. This does not reopen or claim to resolve the closed cancellation issue #3448.

Checklist

  • Code follows the project style guidelines
  • Self-review of the code has been performed
  • Tests have been added or updated as needed
  • Documentation has been updated as needed
  • Changes have been tested locally

Summary by cubic

Classifies OpenCode v2 HTTP 404 responses so a session is marked unavailable only when the error is SessionNotFoundError. Missing commands, models, locations, permissions, and forms keep execution-error diagnostics instead, and typed errors survive resume and history reconciliation.

  • A run that cannot reach the host before it has a session now fails immediately, so the step can fall back to the CLI or retry; only resumed sessions wait for the host to return.
  • History polling fails a step only on typed 404s naming a missing resource; transient errors such as 5xx or unreadable pages retry on the next poll as with v1 hosts.

Assistant snapshot.files are normalized into a stable patch timeline event appended after usage normalization, so a snapshot appearing during refresh does not duplicate the usage event. Regression tests cover 404 classification, malformed and trailing error bodies, host-down and resume routing, transient vs. typed history errors, and snapshot/usage timeline stability. Real OpenCode 2.1.4 validation now includes an unknown command, a real file write with snapshot, and missing-session responses, and the README action table links managed OpenCode sessions via a relative doc link. Command timeout and admission/interruption policy are unchanged.

Summary by CodeRabbit

  • New Features
    • Assistant messages now include file changes from content snapshots as patches.
  • Bug Fixes
    • Managed OpenCode sessions now report unavailable or missing sessions more clearly, while preserving other session and history errors.
    • Failed message refreshes on newer OpenCode sessions now surface their errors.
  • Documentation
    • The harness.run action description now links to documentation for managed OpenCode sessions.