* 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:
5
.changeset/humble-deer-travel.md
Normal file
5
.changeset/humble-deer-travel.md
Normal file
@@ -0,0 +1,5 @@
|
||||
---
|
||||
type: Fixed
|
||||
pr: 2682
|
||||
---
|
||||
**OpenCode no longer declares background subagent dispatch it does not have** — `capabilities/opencode/capability.json` advertised `dispatch.background` and `dispatch.backgroundDispatch` as `true`, but OpenCode's native subagent dispatch is synchronous: the Task tool's `background` parameter is hidden from the model behind the opt-in `OPENCODE_EXPERIMENTAL_BACKGROUND_SUBAGENTS` flag, which defaults to false, and the session loop still handles one subtask at a time. Since `negotiateHostCapabilities` and every `degradationFor` consumer trusts these per-field values, declaring an absent capability overstated it — the opposite of the fail-closed posture the negotiation exists to enforce. Both fields are now `false`, and the host-integration capability matrix carries the corrected values with current upstream citations. (#2598)
|
||||
@@ -75,9 +75,9 @@
|
||||
"namedDispatch": true,
|
||||
"nested": "undocumented",
|
||||
"maxDepth": "undocumented",
|
||||
"background": true,
|
||||
"background": false,
|
||||
"subagentToolkit": "full",
|
||||
"backgroundDispatch": true,
|
||||
"backgroundDispatch": false,
|
||||
"isolation": "orchestrator-worktree"
|
||||
},
|
||||
"modelMode": "active",
|
||||
|
||||
@@ -142,9 +142,9 @@ Documentation gaps:
|
||||
| dispatch.namedDispatch | true | https://opencode.ai/docs/agents | "\"Subagents can be invoked: Automatically by primary agents for specialized tasks based on their descriptions. Manually b" |
|
||||
| dispatch.nested | undocumented | no authoritative doc — searched: https://opencode.ai/docs/agents | — |
|
||||
| dispatch.maxDepth | undocumented | no authoritative doc — searched: https://opencode.ai/docs/agents | — |
|
||||
| dispatch.background | true | https://github.com/anomalyco/opencode/blob/dev/packages/opencode/src/tool/task.ts (v1.15.0, commit 22de34c4d) + src/effect/runtime-flags.ts (v1.17, commit 81f6e0668) | "New in v1.15.0: experimental background subagents — the Task tool gains a `background` parameter (`Schema.optional(Schema.Boolean)`) that launches subagents asynchronously with completion notifications. v1.17: `BACKGROUND_SUBAGENTS_ENABLED = true` (\"feat: enable background subagents by default\") — default-on, concurrent execution in all modes. (#2087, superseding the stale sst/opencode#5887 snapshot)" |
|
||||
| dispatch.background | false | https://github.com/anomalyco/opencode/blob/dev/packages/opencode/src/effect/runtime-flags.ts ; https://github.com/anomalyco/opencode/issues/29638 | "`experimentalBackgroundSubagents: enabledByExperimental(\"OPENCODE_EXPERIMENTAL_BACKGROUND_SUBAGENTS\")` — `enabledByExperimental` falls back to the `experimental` flag, and `bool()` defaults to `false`, so the Task tool's `background` parameter is hidden from the model unless the operator opts in by env var. #29638 (OPEN) confirms the session loop still `tasks.pop()`s one subtask at a time. (#2598 — corrects #2087, whose \"v1.17 default-on in all modes\" reading does not hold against current `dev`)" |
|
||||
| dispatch.subagentToolkit | full | https://opencode.ai/docs/agents | "The 'general' subagent \"Has full tool access (except todo), so it can make file changes when needed.\"" |
|
||||
| dispatch.backgroundDispatch | true | https://github.com/anomalyco/opencode/blob/dev/packages/opencode/src/effect/runtime-flags.ts (v1.17, commit 81f6e0668) + src/server/routes/instance/httpapi/handlers/experimental.ts | "v1.17 `BACKGROUND_SUBAGENTS_ENABLED = true` enables background subagent execution by default in all modes; the experimental capabilities endpoint exposes `{ backgroundSubagents: true }`. Background-spawned subagents run concurrently without blocking the main interaction flow. (#2087)" |
|
||||
| dispatch.backgroundDispatch | false | https://github.com/anomalyco/opencode/blob/dev/packages/opencode/src/effect/runtime-flags.ts ; https://github.com/anomalyco/opencode/issues/29638 ; https://github.com/anomalyco/opencode/issues/14195 | "Concurrent dispatch requires the opt-in `OPENCODE_EXPERIMENTAL_BACKGROUND_SUBAGENTS` flag (default `false`), so it cannot be relied on. #14195: \"the session loop does `tasks.pop()` to grab a single subtask, `await`s it, then `continue`s the loop — so even 3 simultaneous Task calls run sequentially.\" Declaring `true` would overstate the capability, against the fail-closed posture negotiation is built for. (#2598 — corrects #2087)" |
|
||||
| dispatch.isolation | orchestrator-worktree | https://opencode.ai/docs/cli ; opencode.ai/docs/plugins ; opencode issues #14195/#29638/#5887 | "`opencode run --dir <path>` sets an explicit working root at the process level" — native subagent dispatch is synchronous-only, so GSD creates + manages the worktree and process-spawns the executor into it via `--dir` (#2584) |
|
||||
|
||||
Sources consulted:
|
||||
|
||||
@@ -2174,9 +2174,9 @@ const capabilities = {
|
||||
"namedDispatch": true,
|
||||
"nested": "undocumented",
|
||||
"maxDepth": "undocumented",
|
||||
"background": true,
|
||||
"background": false,
|
||||
"subagentToolkit": "full",
|
||||
"backgroundDispatch": true,
|
||||
"backgroundDispatch": false,
|
||||
"isolation": "orchestrator-worktree"
|
||||
},
|
||||
"modelMode": "active",
|
||||
@@ -5411,9 +5411,9 @@ const runtimes = {
|
||||
"namedDispatch": true,
|
||||
"nested": "undocumented",
|
||||
"maxDepth": "undocumented",
|
||||
"background": true,
|
||||
"background": false,
|
||||
"subagentToolkit": "full",
|
||||
"backgroundDispatch": true,
|
||||
"backgroundDispatch": false,
|
||||
"isolation": "orchestrator-worktree"
|
||||
},
|
||||
"modelMode": "active",
|
||||
|
||||
91
tests/fix-2598-opencode-background-dispatch.test.cjs
Normal file
91
tests/fix-2598-opencode-background-dispatch.test.cjs
Normal file
@@ -0,0 +1,91 @@
|
||||
/**
|
||||
* #2598 — the OpenCode descriptor declared background/concurrent subagent
|
||||
* dispatch that OpenCode does not actually provide by default.
|
||||
*
|
||||
* `capabilities/opencode/capability.json` carried
|
||||
* `runtime.hostIntegration.dispatch.background: true` and
|
||||
* `dispatch.backgroundDispatch: true`. OpenCode's native subagent dispatch
|
||||
* (Task tool / `@`-mention / `subtask`) is synchronous: the `background`
|
||||
* parameter is hidden from the model behind the opt-in
|
||||
* `OPENCODE_EXPERIMENTAL_BACKGROUND_SUBAGENTS` flag, which defaults to false
|
||||
* (`enabledByExperimental(...)` over a `bool()` that defaults false), and the
|
||||
* session loop still `tasks.pop()`s one subtask at a time (upstream #14195,
|
||||
* #29638 — the latter still open).
|
||||
*
|
||||
* `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
|
||||
* exists to enforce.
|
||||
*
|
||||
* History note: these fields were flipped to `true` by #2087 citing a reading of
|
||||
* OpenCode v1.17 as "background subagents enabled by default in all modes".
|
||||
* That reading does not hold against current upstream `dev`, where the flag is
|
||||
* opt-in. This test pins the corrected values so a future descriptor edit cannot
|
||||
* silently re-assert an unsupported capability.
|
||||
*/
|
||||
|
||||
// allow-test-rule: runtime-contract-is-the-product #2598 — the descriptor JSON and the
|
||||
// host-integration matrix ARE the negotiated contract; asserting their values is behavioral.
|
||||
|
||||
'use strict';
|
||||
|
||||
process.env.GSD_TEST_MODE = '1';
|
||||
|
||||
const { test, describe } = require('node:test');
|
||||
const assert = require('node:assert/strict');
|
||||
const fs = require('node:fs');
|
||||
const path = require('node:path');
|
||||
|
||||
const ROOT = path.join(__dirname, '..');
|
||||
const DESCRIPTOR = path.join(ROOT, 'capabilities', 'opencode', 'capability.json');
|
||||
const MATRIX = path.join(ROOT, 'docs', 'reference', 'host-integration-capability-matrix.md');
|
||||
|
||||
function opencodeDispatch() {
|
||||
const parsed = JSON.parse(fs.readFileSync(DESCRIPTOR, 'utf8'));
|
||||
return parsed.runtime.hostIntegration.dispatch;
|
||||
}
|
||||
|
||||
describe('#2598: OpenCode does not declare background/concurrent subagent dispatch', () => {
|
||||
test('descriptor declares background: false', () => {
|
||||
assert.equal(
|
||||
opencodeDispatch().background,
|
||||
false,
|
||||
'OpenCode subagent dispatch is synchronous unless an experimental opt-in flag is set',
|
||||
);
|
||||
});
|
||||
|
||||
test('descriptor declares backgroundDispatch: false', () => {
|
||||
assert.equal(
|
||||
opencodeDispatch().backgroundDispatch,
|
||||
false,
|
||||
'concurrent dispatch requires an opt-in flag, so it must not be declared as available',
|
||||
);
|
||||
});
|
||||
|
||||
test('the capabilities that ARE real are left intact', () => {
|
||||
// Narrow the blast radius: this fix must not quietly downgrade neighbouring
|
||||
// sub-fields that were never in question.
|
||||
const d = opencodeDispatch();
|
||||
assert.equal(d.namedDispatch, true, 'named subagent dispatch is genuinely supported');
|
||||
assert.equal(d.subagentToolkit, 'full', 'the general subagent has full tool access');
|
||||
assert.equal(d.isolation, 'orchestrator-worktree',
|
||||
'isolation is orchestrator-managed via `opencode run --dir`, unaffected by #2598');
|
||||
});
|
||||
|
||||
test('the host-integration matrix agrees with the descriptor', () => {
|
||||
// ADR-1239 designates the matrix the deployment source-of-truth; a
|
||||
// descriptor/matrix disagreement is how this defect survived in the first
|
||||
// place (the matrix said true, the ADR binding table said false).
|
||||
const matrix = fs.readFileSync(MATRIX, 'utf8');
|
||||
const section = matrix.slice(matrix.indexOf('## opencode'));
|
||||
const end = section.indexOf('\n## ');
|
||||
const opencodeSection = end === -1 ? section : section.slice(0, end);
|
||||
|
||||
for (const field of ['dispatch.background', 'dispatch.backgroundDispatch']) {
|
||||
const row = opencodeSection.split('\n').find((l) => l.startsWith(`| ${field} |`));
|
||||
assert.ok(row, `matrix must document ${field} for opencode`);
|
||||
const value = row.split('|')[2].trim();
|
||||
assert.equal(value, 'false', `matrix ${field} must match the descriptor`);
|
||||
}
|
||||
});
|
||||
});
|
||||
@@ -268,8 +268,8 @@ describe('ADR-1239 Phase A: hostIntegration descriptors', () => {
|
||||
|
||||
// ─── shouldFlattenDispatch per-host (#853 discriminator) ─────────────────────
|
||||
|
||||
// Expected: false (may background) for codex, cursor, kimi, and opencode;
|
||||
// true (must inline) for the other 13.
|
||||
// Expected: false (may background) for codex, cursor, kimi, and kimi-code;
|
||||
// true (must inline) for the other 14.
|
||||
const EXPECTED_FLATTEN = {
|
||||
antigravity: true,
|
||||
augment: true,
|
||||
@@ -290,9 +290,12 @@ describe('ADR-1239 Phase A: hostIntegration descriptors', () => {
|
||||
// kimi-cli per Kimi Code docs (dispatch.background/backgroundDispatch both
|
||||
// true) → NOT force-flattened.
|
||||
'kimi-code': false,
|
||||
// #2087: OpenCode background subagents (v1.15 param, v1.17 default-on) →
|
||||
// dispatch.background/backgroundDispatch true → NOT force-flattened.
|
||||
opencode: false,
|
||||
// #2598: OpenCode's background subagents sit behind the opt-in
|
||||
// OPENCODE_EXPERIMENTAL_BACKGROUND_SUBAGENTS flag (default false), and the
|
||||
// session loop still handles one subtask at a time (upstream #29638, OPEN).
|
||||
// #2087 read v1.15/v1.17 as default-on; that does not hold against current
|
||||
// `dev`, so dispatch.background/backgroundDispatch are false → force-flattened.
|
||||
opencode: true,
|
||||
// #2102: pi's dispatch.background/backgroundDispatch are both false
|
||||
// (undocumented background-subagent primitive) → force-flattened.
|
||||
pi: true,
|
||||
|
||||
@@ -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)', () => {
|
||||
|
||||
Reference in New Issue
Block a user