fix(#2598): declare OpenCode subagent dispatch synchronous, not background (#2682)

* fix(#2598): declare OpenCode subagent dispatch synchronous, not background

capabilities/opencode/capability.json advertised dispatch.background: true and
dispatch.backgroundDispatch: true. negotiateHostCapabilities and every
degradationFor / shouldFlattenDispatch consumer trusts these per-field values, so
declaring a capability the host lacks OVERSTATES it — the opposite of the
fail-closed posture the negotiation is built for.

The issue's own citations needed checking before acting: the host-integration
matrix (ADR-1239's designated deployment source-of-truth) documented `true` with
NEWER evidence than the issue cited, and explicitly marked the issue's
sst/opencode#5887 reference as a stale snapshot superseded by #2087. git log
confirms #2087 deliberately flipped these from false to true, citing OpenCode
v1.15.0/v1.17 as "background subagents enabled by default in all modes". Applying
the issue as filed would, on that evidence, have REGRESSED a deliberate update.

So the claim was verified against current upstream rather than either document.
`packages/opencode/src/effect/runtime-flags.ts` on `dev` today reads:

    experimentalBackgroundSubagents: enabledByExperimental("OPENCODE_EXPERIMENTAL_BACKGROUND_SUBAGENTS")

`enabledByExperimental` falls back to the `experimental` flag and `bool()`
defaults to false — the parameter is hidden from the model unless an operator
opts in by env var. Upstream #29638 is still OPEN and confirms the session loop
`tasks.pop()`s one subtask at a time. #2087's "default-on in all modes" reading
does not hold against current dev.

The issue's CONCLUSION is therefore right even though part of its evidence was
superseded: concurrent dispatch cannot be relied on, so both fields are false.

The matrix rows are corrected with the verified citation rather than reverted to
the old #5887 quote, so the record shows why the value is false TODAY rather than
re-asserting evidence that was legitimately superseded. Neighbouring sub-fields
are untouched and pinned by test: namedDispatch, subagentToolkit, and
isolation:'orchestrator-worktree' (which works via `opencode run --dir` at the OS
process level and is unaffected — #2584 does not depend on this value either way).

Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_015TCwhbMuY37DzRMCfzTABJ

* fix(#2598): re-pin the dispatch contract tests to synchronous OpenCode dispatch

gsd-test on the descriptor change came back FAILED (5 unique, both node
versions). The failures were not incidental — they were deliberate contract-pin
tests encoding #2087's decision, one named literally "background UPGRADE":

  tests/host-integration-descriptors.test.cjs
    - EXPECTED_FLATTEN[opencode] === false (background-eligible)
    - the derived background-eligible set pin
  tests/opencode-imperative-reference.test.cjs
    - "descriptor declares background dispatch true/true (v1.15/v1.17 upgrade)"
    - "background UPGRADE changes shouldFlattenDispatch: false now"

So this is a recorded decision being reversed, not drift being corrected, and it
is reversed on evidence: current upstream `dev` gates the capability behind
OPENCODE_EXPERIMENTAL_BACKGROUND_SUBAGENTS (default false) and upstream #29638
(OPEN) confirms the session loop still handles one subtask at a time. The issue
is filed by the maintainer and explicitly directs "update golden-parity /
validator fixtures as needed", which sanctions re-pinning.

Behavioral consequence, verified: shouldFlattenDispatch(opencode) now returns
TRUE, so GSD serializes opencode dispatch instead of trusting concurrency it
cannot get. That is the correct fail-closed direction and is safe today — no
shipped GSD flow drives OpenCode background waves (per the issue), and
isolation:'orchestrator-worktree' is unaffected because it works at the OS
process level via `opencode run --dir`, not via the native subagent.

Each re-pinned test now asserts the retracted contract in the opposite
direction — feeding the #2087 axes back in must still yield "would not flatten" —
so a silent re-flip of either field is caught rather than merely un-asserted.

lint:ci exit 0.

Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_015TCwhbMuY37DzRMCfzTABJ

* chore(#2598): backfill changeset pr number (#2682)

---------

Co-authored-by: Claude Opus 5 (1M context) <noreply@anthropic.com>
This commit is contained in:
Tom Boucher
2026-07-26 21:47:54 -04:00
committed by GitHub
parent a5633bb32f
commit a3853472de
7 changed files with 141 additions and 28 deletions

View File

@@ -6,9 +6,10 @@
*
* Proves opencode is driven through the PUBLIC Host-Integration Interface (the
* imperative adapter), that its negotiated axes classify + negotiate correctly,
* that negotiation fails CLOSED on a corrupted descriptor, that the Context7-
* verified dispatch UPGRADE (background subagents, v1.15/v1.17) changes
* `shouldFlattenDispatch`, and that the migration retired the hardcoded
* that negotiation fails CLOSED on a corrupted descriptor, that opencode's
* SYNCHRONOUS dispatch force-flattens (#2598 retracts #2087's background
* "upgrade" — the capability is behind an opt-in flag, not default-on), and that
* the migration retired the hardcoded
* `runtime === 'opencode'` / `isOpencode` branches (folded into descriptor-driven
* `runtime.hostBehaviors` + the combined-family engine install path).
*/
@@ -48,21 +49,34 @@ test('opencode axes classify as the programmatic-cli reference profile', () => {
assert.equal(profileOf(OC_AXES), 'programmatic-cli');
});
// -- AC4: the Context7-verified UPGRADE (background dispatch) -----------------
// -- AC4: dispatch is synchronous — the #2087 "upgrade" is retracted (#2598) --
test('opencode descriptor declares background dispatch true/true (v1.15/v1.17 upgrade)', () => {
assert.equal(OC_AXES.dispatch.background, true, 'background subagents (v1.15 param, v1.17 default-on)');
assert.equal(OC_AXES.dispatch.backgroundDispatch, true);
test('opencode descriptor declares background dispatch false/false (#2598)', () => {
// #2087 set these true, reading OpenCode v1.15/v1.17 as "background subagents
// enabled by default in all modes". That reading does not hold against current
// upstream `dev`, where the capability is opt-in:
// experimentalBackgroundSubagents: enabledByExperimental("OPENCODE_EXPERIMENTAL_BACKGROUND_SUBAGENTS")
// `enabledByExperimental` falls back to the `experimental` flag and `bool()`
// defaults false, so the Task tool's `background` parameter is hidden from the
// model unless an operator opts in. Upstream #29638 (OPEN) confirms the session
// loop still `tasks.pop()`s one subtask at a time.
assert.equal(OC_AXES.dispatch.background, false,
'background subagents are behind an opt-in experimental flag, not default-on');
assert.equal(OC_AXES.dispatch.backgroundDispatch, false,
'concurrent dispatch cannot be relied on, so it must not be declared');
});
test('background UPGRADE changes shouldFlattenDispatch: false now (may background), true for the old axes', () => {
// Post-upgrade: opencode may run subagents concurrently → NOT force-flattened.
assert.equal(shouldFlattenDispatch(OC_AXES.dispatch), false,
'with background:true+backgroundDispatch:true, GSD must NOT force-flatten opencode dispatch');
// Pin the behavioral change: the pre-#2087 axes DID force-flatten.
const preUpgrade = { ...OC_AXES.dispatch, background: false, backgroundDispatch: 'undocumented' };
assert.equal(shouldFlattenDispatch(preUpgrade), true,
'pre-upgrade (background:false) opencode was force-flattened — this is the behavioral change #2087 lands');
test('synchronous dispatch force-flattens; the retracted axes would not have', () => {
// Declaring a capability the host lacks is the failure mode #2598 closes:
// negotiation is built to fail CLOSED, so an unavailable concurrency
// capability must serialize rather than be trusted.
assert.equal(shouldFlattenDispatch(OC_AXES.dispatch), true,
'with background:false, GSD must force-flatten opencode dispatch (fail closed)');
// Pin the retracted contract so a silent re-flip is caught: had the #2087
// values been accurate, dispatch would NOT have been flattened.
const retracted = { ...OC_AXES.dispatch, background: true, backgroundDispatch: true };
assert.equal(shouldFlattenDispatch(retracted), false,
'the #2087 axes did not flatten — that is exactly the overstatement #2598 retracts');
});
test('opencode extension-event surface includes the #2087 additions (permission + session.error)', () => {