Skip to content

fix(opencode): upgrade manifests written before general_agent existed - #571

Merged
chubes4 merged 1 commit into
mainfrom
fix/manifest-general-agent-upgrade
Sep 3, 2026
Merged

fix(opencode): upgrade manifests written before general_agent existed#571
chubes4 merged 1 commit into
mainfrom
fix/manifest-general-agent-upgrade

Conversation

@chubes4

@chubes4 chubes4 commented Sep 3, 2026

Copy link
Copy Markdown
Member

Regression shipped in v1.21.0

manifest() has two return paths:

def manifest(path, root):
    if not path.exists():
        return {..., "general_agent": None}   # <- new key added here in v1.21.0
    ...
    return data                               # <- the file, exactly as written

v1.21.0 added general_agent to the fresh-install branch, then subscripted it directly in main():

if current_general_agent not in (None, previous["general_agent"]):

A manifest on disk was written by whatever version wrote it. v1.20.2 persisted:

{"sentinel", "agents", "artifacts", "task_permission", "skill_permission"}

No general_agent. So every install upgrading from <= v1.20.2 raises KeyError: 'general_agent' — which is every install that had ever run. Only a genuinely fresh install takes the branch that has the key.

Impact

The projection is a hard step in upgrade.sh, so the whole upgrade aborts and the host stops applying managed configuration partway through. Observed on a live VPS immediately after taking v1.21.0:

File "/root/wp-coding-agents/lib/project-opencode-subagents.py", line 284, in main
  if current_general_agent not in (None, previous["general_agent"])
KeyError: 'general_agent'
[wp-coding-agents] OpenCode subagent projection failed (projector); correct the reported failure before retrying.

The fix

Normalize on read, so both return paths have the same shape and the existing direct subscripts stay valid:

for key, default in (("agents", []), ("artifacts", []), ("task_permission", None),
                     ("skill_permission", {}), ("general_agent", None)):
    data.setdefault(key, default)

Adding the key to the fresh-install default is not a migration — that branch is precisely the case a migration cannot affect. Defaulting on read is what makes a new manifest key backward compatible. The keys are defaulted as a set rather than one-off because the next key added will have exactly this problem again, and the function's contract is that its result is safe to subscript.

Note the file was already defensive in validation (data.get("general_agent") on the line above) and then direct in use. Same split that caused this.

Test

Writes the real v1.20.2 on-disk state — no general_agent in the manifest, no agent.general in opencode.json, and no task.general in either, since v1.20.2 wrote none of them — then asserts the projection upgrades and adopts the native subagent.

Baselined against the unfixed projector rather than assumed:

# unfixed
KeyError: 'general_agent'
FAIL a pre-general_agent manifest must upgrade, not abort the projection

# fixed
ok   a pre-general_agent manifest upgrades instead of raising KeyError
ok   the upgrade adopts the native general subagent it previously lacked

Full tests/opencode-subagents.sh passes, exit 0.

Note on how this was found

Caught by running the real nightly maintenance job against a live host after cutting v1.21.0, rather than by CI. CI only ever exercises the fresh-install branch, so no existing test could see it — which is the general shape worth noting: a new key in a persisted structure is an upgrade-path change, and the upgrade path is the branch tests do not take by default.

manifest() has two return paths. The fresh-install branch was given the new
general_agent key in v1.21.0, but the branch that loads an existing manifest
returns the file as parsed, and a manifest on disk was written by whatever
version wrote it. Every manifest already on disk therefore lacked the key,
and main() subscripts previous["general_agent"] directly, so the projection
raised KeyError on every install upgrading from <= v1.20.2 - which is every
install that had ever run.

That aborts upgrade.sh, so a host taking the release stops applying managed
configuration partway through. Observed on a live VPS immediately after
v1.21.0: 'OpenCode subagent projection failed (projector)'.

A default on the fresh-install branch is not a migration; it is the case
that cannot be affected by one. Normalize on read instead, so both return
paths have the same shape and the existing direct subscripts stay valid.
The keys are defaulted as a set rather than one-off, because the next key
added will have exactly this problem again.

The test writes the real v1.20.2 on-disk state - no general_agent in the
manifest, no agent.general in opencode.json, no task.general in either -
and asserts the projection upgrades rather than aborting. It reproduces
KeyError: 'general_agent' against the unfixed projector.
@chubes4
chubes4 merged commit c680a55 into main Sep 3, 2026
67 checks passed
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

1 participant