Skip to content

fix(discovery): the by-value fallback is unreachable and no payload value expresses "complete but unpersisted" #2203

Description

@kyle-sexton

The defect

discovery has a correct by-value fallback for a worker that cannot persist, and no path reaches it. Two halves, one change.

Half 1 — the fallback is unreachable. plugins/discovery/reference/topic-docs.md:34-39 prescribes a by-value return for a worker dispatched into its own checkout:

The contract's by-value boundary is the checkout, not the process: the `-deep` dispatch resolves to
`research-deep`, whose isolated subagent runs in the parent's checkout and writes `RESEARCH.md`
there directly (already visible to the parent), returning a summary by value; a worker dispatched
into its **own** checkout (worktree or background session) returns findings by value instead, and
the parent writes the memory slice.

"background session" is named literally. No agent definition references that file as a persistence mode — grep -rn "topic-docs" plugins/discovery returns, in the agent tier, only the slice-path bullet (agents/explorer.md:27, agents/researcher.md:30). Neither recovery ladder mentions it: skills/explore/reference/dispatch.md:78-99 and skills/research/context/dispatch.md:142-172 carry rungs for exit 2 (envelope), exit 1 + live agent (resume), and refused resume (discard + re-dispatch). There is no rung for "the worker could not write."

Following the rule guarantees the acceptance gate halts the workflow. Reproduced empirically at HEAD against an empty slice holding only the .explore-dispatch baseline — the by-value end state, work complete, nothing on disk:

$ bash plugins/discovery/scripts/check-dispatch-artifact.sh <slice> \
    --index-name EXPLORE.md --newer-than <slice>/.explore-dispatch
unusable: no EXPLORE.md in <slice> or in a sub-slice one level below it
index=<none> sidecars=0 missing=0 freshness=unchecked pointer=unchecked status=unusable
---exit:1---

Source of that path — scripts/check-dispatch-artifact.sh:248-251. The absolute it collides with — skills/explore/SKILL.md:59 / skills/research/SKILL.md:60:

**Any non-zero exit halts the workflow.** Report it, and do **not** proceed to research, planning,
or an edit on the strength of an exploration that did not happen

Half 2 — no payload value expresses "work complete, persistence failed." The return-payload enums at agents/explorer.md:152-158 and agents/researcher.md:136-142 offer status: complete | truncated and coverage: complete | partial. truncated is reserved for the turn-budget stop (agents/explorer.md:167, agents/researcher.md:151), and its ladder consequence is discardskills/research/context/dispatch.md:181-184: "the parent discards the partial slice rather than resuming it". Meanwhile gate step 1 (skills/explore/SKILL.md:44, skills/research/SKILL.md:44) requires an artifact: pointer, so status: complete + artifact: NONE is also a contract violation. Both available values misdescribe the run, and the only honest one routes the parent to throw away completed work.

Adjacent, and closed by the same change: exit 1 reads identically for "never ran" and "ran well, could not persist" (ledger I1 F10). The script is right to grade disk state only; the gap is that no contract text tells the parent to branch on the payload after an exit 1, even though gate step 1 already put the payload in its hands.

Severity and provenance

HIGH · AUDITOR_VERIFIED · PRESENT_AT_HEAD. The plugin's entire routing rationale is "the artifact carries the handoff"; the one situation where it cannot is the one situation the plugin routes to a halt, and the parent's only compliant options are re-run-into-the-same-wall or discard good work. A correct gate whose only compliant outcome is discarding good work is a gate that gets trained out of existence — and that is exactly what was observed: the human-directed parent reinvented the by-value rule ad hoc in a resume message because the rule was unreachable from the failure path.

The fix

One change across both families:

  1. Add persistence: written | by-value to both agents' payload contracts as its own axisstatus: truncated keeps meaning the budget stop, so the discard rung stays correct. Split coverage: partial, which today conflates "unexplored areas remain" with "everything was explored, nothing was saved."
  2. On persistence: by-value, artifact: names the path the parent must write to — a destination, not a claim the file exists. Gate step 1 stays satisfiable and --expect-index stays meaningful.
  3. Add the missing rung to both recovery ladders: exit 1 with persistence: by-value → the parent writes the slice from the payload's verbatim bodies, then re-runs the gate.
  4. Give the "any non-zero exit halts" absolute its one named exception — and scope it explicitly to the halt, not to the gate. The workflow proceeds only on a subsequent exit 0 from the unchanged gate. persistence: by-value is a routing value, never an acceptance value: the payload must not carry a research fallback the gate then grades, which would reintroduce the Tier-3 laundering SKILL.md correctly forbids.

Regression test: extend scripts/check-dispatch-artifact.test.sh with a paired case asserting the gate still exits 1 on the empty by-value slice and exits 0 after the parent writes that slice from the payload — proving the exception routes through the gate rather than around it.

Provenance

  • Inbox item: 20260810-225904-discovery-dispatch-persistence-contract (I1 F1 + F3, with F10 closing as a side effect).
  • Corroborating items, merged here rather than filed separately: 2026-08-10-plugin-quality-audit-four-components D-F1 (deferred to this row explicitly) and the 023241Z four-components packet B-F5 ("the exit-1 ladder rung dead-ends on a nonexistent index" — the rung says read the index for the pointer, and a write-blocked worker leaves no index).
  • Evidence source is the packet's evidence-1.md, which governs over evidence.md on conflict.

Activity

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Metadata

Metadata

Assignees

No one assigned

    Labels

    Type

    No type

    Projects

    No projects

      Milestone

      No milestone

      Relationships

      None yet

      Development

      No branches or pull requests

      Issue actions