0.4.0
Added
-
A turn's
delegatecalls now run side by side (design 0002 § 2, milestone M4). A run that
asked for three sub-tasks in one turn paid three whole child runs of latency back to back,
because the loop resolved delegates strictly one at a time. Nothing about them required it: a
delegate starts from an empty conversation, so no child can read what another produced and there
is no ordering between them to preserve.Neighbouring
delegatecalls of one turn form a group, capped by the new
Delegation::max_parallel(--delegate-parallel <N>, default 4, minimum 1). Neighbouring rather
than gathered from the whole turn, exactly as a batch of pure tool calls is: a call between two
delegates is a barrier, because the second child may be there to look at what that call did.Each child gets its own model port and tool port, from two new defaulted trait methods —
ModelPort::forkandToolPort::fork, both answeringNone(cannot be run beside itself)
unless a port says otherwise.MessagesClientandResponsesClientfork by sharing: one
endpoint, one credential source, one connection pool, and — on the Responses wire — one request
counter, so two siblings cannot mint the samex-client-request-id.FlatandVerbsfork by
sharing the catalogue itself, so a fork publishes exactly what its parent publishes, entry for
entry. A fork that published one tool more would be a way to widen a run by delegating.Three things are not forked, because there is one of each by nature: the approver (one
person, asked one question at a time), the operator's hooks (how many copies of my guard are
running must not depend on how many sub-tasks a model asked for) and the event sink (the record
is one ordered stream). A child on a worker thread reaches all three by asking the run's own
thread, which sits answering for exactly as long as any child is running. Each proxy fails
closed: an approval nobody gave is a denial, and a hook that could not be consulted did not say
yes. A child that panics comes back as a failed tool result naming what happened, and its
siblings finish.This is an optimisation and never a difference in what a run can do. Where a port will not
fork, where--delegate-parallel 1is set, or where the run's remaining token budget will not
divide between the children, the same delegates run in order — the same children, the same
gate, the same results in the same order. Order is in fact the more accurate accounting: each
child is carved on what the one before it actually spent, where a group has to divide the
remainder up front. Tokens are divided because they add up; the wall clock is not, because
four children running at the same moment take one child's worth of it — the same figure a batch
of tool calls is handed.A reader of the record tells the two apart with no new event: two
DelegateStartedbefore either
DelegateFinished, and the children'sDelegatedevents interleaved, cannot happen in a run
that delegated in order.The
delegatetool's description now says several calls in one turn run at once — only on a
run that can actually do it, because a model told something false about what its next turn costs
is worse than one not told at all, and a model that is not told does not ask.Evidence is
provider_emulated: a newdelegate-pairscenario on both emulators, driven
through the shipped binary, whose record shows twodelegate-startedbefore either
delegate-finished— a bracketing a run that delegated in order cannot produce. Timing proves
nothing here; a fast serial run and a slow parallel one look alike. -
contracts/cli/b10x-harness/2026-08-30.1, cut for--delegate-parallel. Strictly additive:
every flag of2026-08-30keeps its spelling, itstakes_value, its default, its conflicts and
its requirements, and no subcommand changed. A consumer pinned to2026-08-30is correct against
this binary.2026-08-30is released and reachable onmain, so it was cut beside rather than
edited (AGENTS.mdinvariant 13), and a second cut on the same date takes the.1suffix. -
A tracked file carrying an absolute home directory now fails the gate.
scripts/check-no-home-paths.pyjudges the index, read withgit cat-file --batchover
git ls-files -s, because a commit records the index and not the working copy — content staged
with a leak and tidied afterwards would otherwise be committed by a green check; the worktree copy
is judged too, becausegit commit -astages it. Every file is searched as bytes, with a
second pass over the same bytes with NULs removed, so a path inside a committed.pycor UTF-16
text is visible: that.pycwas the one file of twenty the cross-repository audit found that no
text grep would have caught.A home directory needs no trailing separator —
HOME=/home/<name>publishes the account
exactly as a subpath does — and the account class admits non-ASCII, so a contributor named
mülleris protected like the author; a candidate is then trimmed to the name at its head, so an
elided path in a doc comment names nobody. One account,you, is a documentation placeholder in
every file type;userandusernameare not, because real machines have them. Two
planning-store paths are exempt with the reason in the script: the journal is append-only and
committed, and editing it would forge the record.--self-test(52 cases) is a gate step of its
own, because a check that passed everything would look exactly like a green one.
Changed
- A
delegatecall that names an agent this run does not have no longer emits a
DelegateStartedfor a child that never starts. The name was resolved after the event was
emitted, so the record carried a delegation that began and never finished — the one shape a
reader cannot interpret. Everything a child needs is now worked out before anything is emitted
and before any budget is carved, which is also what lets a group divide its remainder by the
number of children that will actually run. The refusal the model reads is unchanged; what moved
is that an unresolvable agent name is now reported instead of an exhausted budget when a call
would have failed both checks.
Fixed
-
A subscription token now reaches
claude-opus-5andclaude-sonnet-5on the Messages wire.
Both answered429 rate_limit_erroron every request, at any hour, against an account measured
at 8% of its five-hour window — and the refusal carried noanthropic-ratelimit-*headers at
all, so nothing downstream could tell it apart from an exhausted quota. The transport did what
it is supposed to do with aRateLimitedthat says it is retriable: four attempts, a back-off,
and a reported rate limit that was not one.The cause is a condition the documented API has no field for. Under a token obtained on a
person's behalf, this route serves a request only whensystemopens with a fixed block:"system": [ {"type": "text", "text": "You are Claude Code, Anthropic's official CLI for Claude."}, {"type": "text", "text": "<the run's own instruction>", "cache_control": {...}} ]This harness sent one block — its own instruction — which is one of the shapes that is refused.
Measured on 2026-08-30 againsthttps://api.anthropic.com/v1: the match is exact and positional.
Dropping the trailing full stop, adding a leading space, adding a trailing newline, or merging
the instruction into the same block each answer429; extra blocks after it are served.
claude-haiku-4-5-20251001is not gated at all, which is why the failure looked model-shaped and
not request-shaped. Theanthropic-betavalue makes no difference.harness_messages::SUBSCRIPTION_CLIENT_PREAMBLEholds the string, andrequest_bodynow takes
the credential presentation as an argument — on this route how the credential is presented
changes the body, so the projection has to be told. A key issued to a program sends exactly
what it sent before: one block, the instruction, breakpoint on it. The breakpoint stays on the
last block under both, so the cached constant head still covers the instruction rather than only
the preamble.Contract
contracts/provider-wires/anthropic-messages/2026-08-30.1cuts a version for it and
pins two request fixtures, one per presentation, both built by the one function in
crates/harness-messages/tests/contract.rs.2026-08-30stays as released.This is the harness naming itself as another vendor's client to that vendor. It is recorded
here rather than buried in a constant, and it is the operator's decision to send it, taken on
2026-08-30 against their own subscription credential. -
The CLI contract version in force,
2026-08-30.1, now says what actually moved, and a test
holds it to that.2026-08-30claimed "strictly additive" while measuring itself against
2026-08-29.1— two cuts back — and nine fields had moved against its real predecessor
2026-08-29.3:--modeland--base-urlstopped beingrequiredand--wirelost its
"openai-responses"default, onrun,chatandworkflow run. That directory is released and
immutable (AGENTS.mdinvariant 13), so the correction is carried in2026-08-30.1/README.md,
which a consumer of the current pin reads.2026-08-30was also dated the day after the commit
that cut it (719f6e3, 2026-08-29 23:08);2026-08-30.1was cut on 2026-08-30. No flag,
takes_valueor default moved in this change —ARGV_CONTRACT_VERSIONis unchanged and the
command line is untouched.the_version_in_force_names_every_field_that_moved_between_pinned_versionsdiffs every
consecutive pair of pinnedargv.jsonfiles and fails when a moved field is named by no README a
consumer of the version in force reads. It matches an ordered sequence scoped to the section
naming that pair, so a document cannot satisfy it by reversing the direction of every row, by
denying the change in so many words, by listing the tokens on one junk line, or by attributing the
moves to a different pair — all four passed the first version of the guard. It iterates the
union of flag names, so a renamed or removed flag is a departure rather than a silent skip:
that is the failure the CLI contract exists for,--substrate-embeddedhaving changed shape and
refused a pinned consumer at clap. Version order is compared numerically on the.Nsuffix, so a
tenth cut in one day does not sort before the ninth. -
A named agent now narrows what a run can reach under
--surface verbs, not only what it names
directly.0.2.0recorded the opposite — "an entry reached through a verb" was not narrowed,
and "named agents are a flat-surface feature until that is answered". That was true then and this
is the answer; the0.2.0entry stands as the record of what shipped that day.The narrowing is written in the names of what a run can reach, not of what it publishes.
Behind three verbs the call namestool_invokeand the entry is an argument, so the gate asks
the port what the call invokes and refusesfile_writeby the entry's own name, with the same
message a flat surface gives. The verbs are routes and not capabilities and are not narrowed away
— an agent grantedReadand refusedtool_invokewould have been granted nothing at all — but
a grant that is empty admits nothing, routes included, because a route to no granted entry
leads nowhere and can only enumerate a catalogue the child was admitted none of.It is enforced at every site a call can reach the port, which is more than one. A turn's
neighbouring pure reads are handed to the port as a batch, and a batched call does not pass the
single-call gate: checked in one place, that surface let an agent grantedGrepread any file by
asking for the read beside a search.batchable,run_batchandinvokenow ask one predicate,
and so does publication.A delegate is also always given its parent's admitted set. An agentless child used to be handed
the whole port, so a narrowed run could climb back out by delegating again and naming nobody —
unreachable from the command line, where depth is never above one, and reachable by any library
consumer.ToolPort::reachableis a new defaulted trait method, and the direction of its failure is now
the one its documentation claims: a grant is an intersection with what the port reports, so a
port that under-reports costs a run reach and never boundary. It was subtraction before, which
grew the exempt set as a port said less — the inverse — and a port answering with nothing turned
every tool it published into a route.