fix(discovery): an agent's own subtree is state, not more catalog - #355
fix(discovery): an agent's own subtree is state, not more catalog#355schickling-assistant wants to merge 1 commit into
Conversation
6dc82cd to
b7c677b
Compare
Discovery descended into every subdirectory of a declaration directory and
treated any `*.{toml,json,kdl}` it found there as a spec candidate. Agents
routinely write JSON evidence under their own directory — launch receipts,
captured output, scratch — so that produced two failures from one cause.
A malformed evidence file fails the whole catalog. One agent's
`[{"run":1},{"run":2}]` makes `st2 validate` report an error and
`st2 up --materialize-only` refuse for every unrelated agent on the host.
A well-formed one is worse, because it is silent. A launch receipt carrying an
`argv` field lowered into a real agent with a real task, and the catalog reported
`0 errors, 0 warnings`. `st2 ls` listed a phantom agent whose argv came straight
out of the receipt.
A declaration directory holds exactly one agent, so nothing nested below it is
another declaration. Stop descending once the current directory anchors one. The
existing `resources`/`archive`/`inbox` exclusions were an incomplete enumeration
of this same rule, and `is_catalog_path` and `src/watch.rs` already document the
intent.
The catalog root is excluded from that rule. A root `catalog.kdl` may declare an
agent inline beside a profile, and the root is still the catalog envelope rather
than that agent's own directory — without the exclusion, one such file makes the
walk refuse to descend into `agents/` and the entire catalog disappears from
discovery. Measured against the live 664-agent catalog on this host, this branch
and clean `main` discover an identical roster.
Since this branch was first written, a601331 added
`is_canonical_bundle_descendant`, which skips any path more than three components
below `agents/`. That already covers the canonical `agents/<host>/<identity>`
bundle positionally, and it is why the original fixture stopped discriminating.
It does not cover a declaration directory placed anywhere else, which discovery
still supports: `team/agent.kdl` beside `team/evidence/…/run-1.json` still
lowered the receipt into a phantom `phantom.receipt` agent with no error. The
tests pin that case and the root-envelope case, and keep the bundle case as a
regression guard.
agent-identity: dev3.dotfiles.fb-batch1.st2.worker
agent-persona: worker
agent-supervisor: dev3.dotfiles-lead
agent-tool: Claude Code
agent-tool-version: 2.1.250
agent-runtime: Claude Code 2.1.250
tooling-profile: dotfiles@1258828
b7c677b to
df4cec7
Compare
Closing unmerged — superseded in intent, and it does not fix the row it was filed forDecision by 1. Correction to this PR's own body: the reported case is fixed by neither
|
| fixture | clean main |
this branch |
|---|---|---|
root catalog.kdl declaring an agent, beside agents/h/real.worker/ |
real.worker, root-agent |
root-agent — subtree lost (fixed by dir != root) |
agents/legacy.kdl beside agents/h/real.worker/ |
real.worker, legacy.worker |
legacy.worker — subtree lost |
agents/h/legacy.kdl beside agents/h/real.worker/ |
legacy.worker, real.worker |
legacy.worker — subtree lost |
The only formulation with no false positives is positional — skip only at exactly
<root>/agents/<host>/<identity> — but for spec collection that is already what
is_canonical_bundle_descendant does. So the safe version adds nothing, and the version that adds
something is not safe. That is the whole argument, and it is why this needs a design decision rather
than another predicate.
A read-only check against a live 664-agent catalog returned an identical roster for this branch and
clean main, so the non-canonical shape this PR defends against does not occur there either.
3. Three distinct classes — please keep them apart
Anyone reopening this should not collapse these:
- Canonical bundle descendants (
agents/<host>/<identity>/…, more than three components below
agents/) — fixed onmainbya601331, positionally. - A spec-shaped file in a directory that is neither a canonical bundle descendant nor a
declaration directory — this is FB-397's actual recorded case (.state/<identity>/…). Open
onmain, and not fixed by this PR. - A spec-shaped file sitting directly in a declaration directory
(agents/h/real.worker/receipt.json) — open onmainand on this branch, because the guard
proposed here is onft.is_dir()and never inspects files. Silent:rc=0, no error, phantom
listed beside the real agent. Filed separately; closing it is a behaviour change, not a bug fix.
Disposition
Closed, not merged. Nothing on main changes. The branch is left in place; FB-397 stays open as
needing a design decision rather than a predicate, and class 3 is filed as its own row.
Posted on behalf of @schickling
| field | value |
|---|---|
agent_identity |
dev3.dotfiles.fb-batch1.st2.worker |
session |
dev3.17884542 |
agent_persona |
worker |
agent_supervisor |
dev3.dotfiles-lead |
agent_tool |
Claude Code |
agent_tool_version |
2.1.251 |
agent_runtime |
Claude Code 2.1.251 |
tooling_profile |
dotfiles@6048b77 |
Note
CLOSED, not merged. This PR does not fix the row it was filed for, and the guard it proposes
has a false-positive class that can silently empty a catalog. The closing comment carries the
measurements, including a correction to the claim below about
a601331.Correction: the section "Scope changed under this branch" originally stated that
a601331already fixed FB-397's reported case. That was an inference and it is wrong — measured, FB-397
still reproduces on current
main, and this branch does not fix it either. See the closingcomment.
What
Discovery descended into every subdirectory of an agent's declaration directory and treated any
*.{toml,json,kdl}it found there as a spec candidate. Agents routinely write JSON evidence undertheir own directory — launch receipts, captured output, scratch — so one cause produced two
failures.
[{"run":1},{"run":2}]makesst2 validatereport an error andst2 up --materialize-onlyrefuse for every unrelated agenton the host.
argvfieldlowers into a real agent with a real task while the catalog reports
0 errors, 0 warnings.A declaration directory holds exactly one agent, so nothing nested below it is another declaration.
This stops the walk descending once the current directory anchors a declaration. The existing
resources/archive/inboxexclusions were an incomplete enumeration of that same rule, whichis_catalog_path's own doc comment andsrc/watch.rsalready describe as the intent.Feedback row: FB-397 (friction, 2 occurrences) —
https://app.notion.com/p/st2-validate-recursively-parses-every-json-under-an-agent-dir-as-catalog-config-so-agents-storing--3ace3d41f4a3817db354eac8d8b1529d
Changed lines: 105 (105 insertions, 0 deletions) across 2 files —
crates/agent-spec/src/discovery.rs(11),crates/agent-spec/tests/discovery.rs(94).Scope changed under this branch — read this before reviewing the fixture
This branch was written against
19485cf. Since then,a601331(#351) addedis_canonical_bundle_descendant, which skips any path more than three components belowagents/.That covers the canonical
agents/<host>/<identity>bundle positionally.It is the shape FB-397 was originally filed against, so the row's literal reported case is alreadyThat was wrong. FB-397's only recorded path isfixed on current
main, not by this PR..state/<identity>/axe/agent-launch-receipts/*.json, whose first component is notagents, sois_canonical_bundle_descendantcannot cover it. Measured: the reported case still reproduces onmainand is not fixed by this branch. See the closing comment. The original fixture here stopped discriminating, and the earlierrevision of this PR body claimed evidence that no longer reproduced. Both have been re-measured
against
4c4e7f8.What
a601331does not cover is a declaration directory placed anywhere other than a canonicalbundle, which discovery still supports. That is the case this PR now closes, and the case the test
now pins. The two rules are complementary, not competing:
is_canonical_bundle_descendantispositional, this one is semantic (this directory anchors a declaration, so its subtree is that
agent's state).
Not addressed here, and left deliberately: a spec-shaped file sitting directly in a declaration
directory (
agents/h/real.worker/receipt.json) still lowers as a phantom on bothmainand thisbranch. Closing that requires guarding files, not just directory descent — a behaviour change beyond
this PR's scope. Flagged rather than folded in.
The catalog root is excluded from the rule
Worth reviewing closely, because the naive form of this change is a catalog-wiping regression.
is_declaration_parentis asked about every directory the walk visits, including the catalog rootitself. A root
catalog.kdlmay declare an agent inline beside a profile — that is a live surface,and
root_catalog_envelope_allows_a_profile_beside_an_agentalready covers it. Without an explicitexclusion, that one file makes the root "hold a declaration", the walk refuses to descend into
agents/, and the entire catalog disappears from discovery:So the guard is
dir != root && is_declaration_parent(root, dir), anda_root_envelope_declaration_does_not_hide_the_agents_subtreepins it. That test fails with theexclusion removed, naming exactly the collapse above.
Checked at real scale as well, read-only against this host's live 664-agent catalog:
Fixture
Three agents. One is a canonical bundle that has written evidence under its own directory; one is a
non-canonical declaration directory (
team/) that has done the same.Before — clean
main@ 4c4e7f8h.phantom.receiptis the silent failure: a receipt file listed as a real agent with a real task,and
4 agentswhere the catalog declares three. Theaxe/…artifacts under the canonical bundleare already ignored here — that is
a601331working.After — this branch
(
0 operationsin both cases because no fixture agent declares arenderblock. The change beingdemonstrated is
1 error/Error: materialization failedbecoming0 errors, and the phantomdisappearing from
st2 ls.)Both binaries were built from this worktree in the same target directory, and each build was
confirmed to recompile
agent-specandst2rather than reuse a cached rlib.Tests
One regression test, asserting on
found.specsand not only onfound.errors— the phantom-agentcase produces zero errors, so an error-only assertion would pass vacuously. It runs under both
discoveranddiscover_strict, and covers the non-canonical declaration directory (thediscriminating case) plus the canonical bundle (a regression guard if the positional rule ever
moves).
Confirmed failure-capable against current
main: with the test in place anddiscovery.rscopiedfrom
origin/main, it fails and names the phantom.With the fix:
cargo fmt --checkreports no diff inside either hunk this PR adds. (The crate does carrypre-existing formatting drift from
a601331atsrc/discovery.rs:10,src/discovery.rs:292,tests/discovery.rs:{13,33,1298},src/lib.rs,src/profile*.rsandexamples/wasm_bench.rs;none of it is touched here, and
nix flake checkdoes not gatecargo fmt.)CI attribution
mainis green in CI, so any suite failure seen locally on the development host isenvironment-specific to that host, not an upstream regression. Four such pre-existing failures were
confirmed on clean
main— three disclosed in the first revision of this PR, plus a fourth thatneither producer disclosed at the time:
These were confirmed on clean
mainat19485cf0. This branch is now rebased onto4c4e7f8; theauthoritative check for whether any of them still fail is this branch's own CI run, not that earlier
local measurement.
Posted on behalf of @schickling
agent_identitysessionagent_personaagent_supervisoragent_toolagent_tool_versionagent_runtimetooling_profile