Commit graph

100 commits

Author SHA1 Message Date
acamilo
e3132362cf Merge main at 5512900: STATE-01, and the flybus coalescing fix
The flybus session_over_one_router failure my workspace runs were
counting is already fixed on main: the coalescing branch forces the
coalescing deterministically instead of asserting that a slow consumer
must skip. Merging before the runs so they measure a tree that exists.

One conflict, in the crate README, and it was two sections both newly
added at the same anchor rather than two versions of one thing. Both are
kept: STATE-01's checkpoint and recovery section, then the guidance on
which replies permit a subset, which stays immediately above the
contract-narrowing section where someone adding a check will meet it.

coordinator.rs and the process tests auto-merged. Checked rather than
assumed: the acknowledge equality check is still gone, acknowledge_replies
and last_resolution_attempts are present, STATE-01's capture, durable
and rebase surfaces are present, and both test changes survived.
2026-09-22 19:49:14 +00:00
acamilo
c1a972548b session: the death rows kill once the victim provably has the work
Both death rows killed after a fixed sleep, so under load the kill could
land before the call was dispatched. The bus then reports not-dispatched
and MutationCertainty::None, which is correct -- the participant never
received anything -- while the test demanded unknown. A full-workspace
run caught it: "left: None, right: None" at the certainty assertion.

kill_once_it_is_working polls the victim's own Worker.Status until it is
provably inside the operation before killing: the agent until it is
Preparing with an active request id, the world until it has recorded the
batch, which the arena does before its injected delay. Dispatch has then
demonstrably happened and unknown is the only correct certainty.

The wall-clock boundedness assertions are gone with them. The suite's
own `within` is the bound, and the victim is five seconds slow against
its twenty, so returning at all is the claim.
2026-09-22 19:48:10 +00:00
acamilo
ef82a05a3a docs: which replies permit a subset, and which do not
The sweep behind this branch found one check demanding an exact match
where the contract permits a short answer, and four that were right to
demand one. The question that separates them belongs where the next
check gets written, not only in a run report: is the far side reporting
what it did, or being held to a requirement?

Worker.Acknowledge is the only reply of the first kind here, because
ipc-v1 section 5 makes it idempotent. The commit and batch checks are
the second kind and are named so nobody loosens them later in the name
of tolerance; they are what make a partial commit and an incomplete
batch fail.
2026-09-22 19:05:58 +00:00
acamilo
f6baeb5cfa session: a test for the acknowledge rule, not only for the flake
The short-list acknowledgment is a contract rule, so it gets a test that
says so rather than one that depends on a worker being slow.
acknowledge_replies carries the ipc-v1 section 5 sentence in its doc
comment and returns what the worker actually released;
acknowledge_lifecycle calls it, so bootstrap and the test exercise the
same path.

an_acknowledge_that_releases_nothing_is_not_a_failure drives the case
directly, once per execution mode: bootstrap releases every lifecycle
reply, the test asks for those ids again, the worker ignores them and
releases nothing, and the coordinator must accept the empty list, stay
unfenced, stay at its boundary and still play the next transition. It
fails if the equality check returns.
2026-09-22 19:03:18 +00:00
acamilo
55129002a4 tests: the placeholder fingerprint in the migration test no longer reads as a MAC address
Some checks are pending
ci / node 22 (test + typecheck) (push) Waiting to run
ci / rust stable (cargo test --workspace --release) (push) Waiting to run
ci / infra/tests/lint.sh (push) Waiting to run
ci / playwright apps/stage (allowed to fail) (push) Waiting to run
2026-09-22 18:59:17 +00:00
acamilo
6d67fa7ed2 session: a retried Acknowledge is not a failed epoch
The bound test failed two runs in thirty under load, and not on the
bound it was testing. Both failures were bootstrap: "a worker did not
acknowledge every lifecycle reply".

ipc-v1 section 5 says already released or unknown ids are ignored, and
the contract type already holds the acknowledged list to a subset of
the request. So the second Acknowledge of the same ids answers with an
empty list by design -- and the section 6 resolution produces exactly
that second Acknowledge whenever the first reply is slower than the
probe. Demanding the whole list back turned a safe, contract-sanctioned
retry into a failed epoch, which is a defect in the coordinator rather
than in the test: a slow lifecycle reply would do it to a real session
too.

The test made itself easy to hit by installing a fifty-millisecond
probe before bootstrap, so bootstrap's own lifecycle calls ran under a
budget meant for the step under test. It now bootstraps at ordinary
deadlines and tightens them afterwards.

The two bounds are also separated by construction rather than by clock.
Each half puts the bound it is not testing out of reach -- u32::MAX
attempts against a fifth of a second, three attempts against an hour --
so no scheduling delay can flip which one fires, and the silent
participant is ten minutes slow against a twenty-second test timeout,
so returning at all proves a bound ended it. The wall-clock assertion
is gone and the attempt count is asserted instead, which
last_resolution_attempts now records. One agent per composition, so the
participant the failure names is not a race either.
2026-09-22 18:47:42 +00:00
dev
388e112ad5 Merge main into feat/sf-state-01 2026-09-22 18:45:56 +00:00
dev
45621903be session: name the two ways a durable wait ends without an acknowledgment
Review follow-up on the checkpoint store.

A dropped reply channel and an expired caller budget were both reported as
ReplyLost. They are different facts -- the first means the write is over and its
outcome did not reach here, the second means the save is still going -- so they are
now separate outcomes, and the durable wait has its own budget rather than borrowing
the one that bounds a call to a participant. Both still leave durable metadata where
it was, and for both the resolution asks the store about the same checkpoint.

The writer's two bounds refuse at different moments and the comment claimed
otherwise: the outstanding-capture bound is taken before a capture is requested, and
the byte budget cannot be, because a capture's size is not known until it exists. The
byte check, the decision and the change to the byte total are now one critical
section, the peak is sampled after a superseded job's bytes are gone, and the writer's
own bookkeeping is over a type that holds only the outcomes a writer can produce.

The manifest's coordinator.eventWatermarks is {lastSourceStep, issued}; the fixture
illustrated {lastEventId, lastOrdinal}, and the illustration is what changed, because
an event id is derived from the epoch and cannot be compared across the restore that
gives the session a new one.

checkpoint-envelope-v1 section 3 also now says, under the same dated amendment, that
a required-manifest-field change must bump envelopeVersion once production files
exist: contractDigest is taken over the schema set and does not cover this manifest,
so the envelope version is the only thing that can carry such a change.
2026-09-22 18:45:51 +00:00
acamilo
a8afd8f448 Merge feat/catch-reward: a catch reward, adapter v6 with a v5 migration, and a rung restart 2026-09-22 18:41:15 +00:00
dev
2cc8d0294d docs: the two sweep flakes are fixed, and one assertion that could not fail
bus-conformance.md still listed the example_demo root count and
session_over_one_router as not fixed here. Both are fixed on main now, so
the rows say what each test does instead: the example waits for the
producer's hold release before reading the counts, with its printed
output unchanged, and the integration renderer is held until the
publisher's twentieth receipt has returned, so its coalescing is forced
and the assertions are order, freshness, acceptance under a stalled
spectator and the replacement accounting, with the before and after
counts.

The delivery check in that test compared (step, sequence) against
(step, step), whose first element can never fail. It compares sequence
against step.
2026-09-22 18:28:54 +00:00
acamilo
882980823c Merge fix/flybus-coalescing-flake: the flybus suite asserts guarantees, not the machine's timing 2026-09-22 18:23:06 +00:00
dev
db30708c3f Merge main into feat/sf-state-01 2026-09-22 17:55:03 +00:00
acamilo
f223702085 Merge fix/chat-ring-persist: the on-screen chat ring survives a sim restart via a hot-dir sidecar 2026-09-22 17:45:59 +00:00
acamilo
629aff8537 Merge fix/loop-row54: the walk anchor is measured from the screen, landing tiles retire, errands arrive inside, the errand ledger survives a restore 2026-09-22 17:45:59 +00:00
acamilo
ad632621c1 macros: say when the step reading narrows to None 2026-09-22 17:44:53 +00:00
dev
6655a1b1c6 session: coherent all-participant checkpoint and recovery
STATE-01 over the FLYSESS1 envelope CONTRACT-01 specified.

fly-session gains a `state` module: the durable store with its generations, its
rotation and the commit order of checkpoint-envelope-v1 section 5, where the store
manifest rename is the durable commit point; a compatibility block whose comparison
names the identity that differs rather than one opaque digest; and a bounded writer
that owns its payload handles until the bytes are committed or the job fails.

The writer's queue slot is taken before the first State.Capture, so a saturated
writer refuses a capture rather than queueing it without bound, and the refusal is a
BUSY the stepping session survives. Capture and durability are two events: a capture
completes when an immutable capture exists, and only the store manifest rename moves
the durable mark. A lost save reply is an outcome, and the resolution asks the store
about the same checkpoint instead of saving again.

Both worker roles implement State.Capture, State.StageRestore and
State.ActivateRestore, with once-only restore tokens bound to checkpoint, scope,
payload and incarnation. A restore selects a complete compatible generation, imports
every payload as a fresh artifact, stages the group, validates the coordinator's own
ledgers, and only then activates; a failure anywhere leaves the fence closed and
records every participant that staged as one that must be replaced. The fence lifts
at Failed -> Restoring(k) -> Paused(k) and nowhere else.

The task and the action executor gain the capture/validate_restore/install_restore
interfaces workers-v1 section 4 lists, and the ledger can re-derive the event
identities it issued under another epoch, which is what lets a resumed run's
behaviour trace be compared with an uninterrupted one.

media: check_required_audio now takes the observation's provenance instead of
exempting boundary 0. A chunk is the audio of an interval, and the observation
ActivateRestore installs covers none.

checkpoint-envelope-v1 section 3 gains a dated amendment adding `environment` to the
manifest, the holder of the world's own payload, which the table named for every
other participant; `helperState`, which that table already listed, joins the
required-field set in Rust and TypeScript. The fixture was regenerated by the
existing example; the schema set and contractDigest are unchanged.

state-media-v1 section 5 gains a dated amendment for three readings this slice
enforces: the State RPCs' compatibilityDigest is the participant's, not the
manifest's composition-level block; a restored observation carries no audio chunk;
and a participant that staged into an abandoned install must be replaced.
2026-09-22 17:43:43 +00:00
alex
23a4d7379b rewards: a catch reward, adapter v6 with a v5 migration, and a rung restart
The operator's decision of 2026-09-22: pay the fly for keeping a wild Pokemon,
bump the adapter properly, and restart the live run from an early checkpoint
rather than from scratch.

The rule. `catch` is the catalog's ninth kind, appended so the key order
`counts` serializes in does not move. 0.30 for a species this run had never
owned, 0.10 for a repeat, three payouts per species for the lifetime of the
ledger; the `species` rule is untouched, so a first catch of a new species pays
0.80 across two kinds. The catch is read from `wCapturedMonSpecies` ($d11c),
whose comment in ram/wram.asm is "0 if no mon was captured": ItemUseBall zeroes
it before every throw and writes wEnemyMonSpecies into it only on the branch
that keeps the Pokemon, and UseBagItem's `.returnAfterCapturingMon` zeroes it
again and sets wBattleResult to 2 -- a value written on exactly two paths in
the game, that one and a link battle whose opponent ran. Both are required, so
a byte read out of a half-initialised battle cannot pay. Not wPartyCount: a
catch with a full party raises wBoxCount instead, and wPartyCount also rises
for a gift, a trade and a PC withdrawal.

"Never owned this run" is the `species` payout inside the same battle, because
nothing else can set a Pokedex bit during one. It is not read off the captured
species byte: that is the cartridge's internal index while the owned bitset is
by Pokedex number, and nothing in WRAM converts between them.

The address was resolved by tools/resolve_wram.py, not written by hand. The
tool needed NUM_TMS and NUM_HMS, which the decomp defines through its `const`
enumeration, so it now counts them from the file's own add_tm/add_hm
definitions and cross-checks NUM_TMS against the literal the same file
declares.

The feed's kinds are closed, so `catch` publishes on `wildwin` and nothing in
packages/feed or apps/stage changed. Deliberately not `pokedex`: the `species`
rule already pays for the bit the same catch sets. The stage's ticker copy is
keyed on the feed kind, so a catch row reads "wild win" -- stated in
docs/rewards-learning.md rather than left to be discovered.

v5 -> v6. STATE_VERSION stays 4: the rule adds one counter, `catchCounts`, and
changes nothing else, so a v5 state restores with it empty. That migration is
opt-in and needs all three of: the adapter segment being the only difference
between the two compatibility strings, the running adapter listing the
checkpoint's adapter in `migrates_from()`, and the deploy naming it in
FLY_ACCEPT_ADAPTERS. flysim applies the rule at restore and 05-deploy's gate
applies the same rule before it flips the symlink, writing the variable into
fly.env so the two cannot disagree.

The restart. infra/bin/fly-reset-to-milestone <N> archives both stores to a
dated directory, rewrites milestone-<N>.checkpoint with the ratchet's attempts
and recoveries at zero, installs it as the newest generation of both stores,
clears the milestone archives above N and the event log, and prints what it
did. It refuses while flysim is running and refuses a rung the run never
reached. The envelope work is in flysim::reset (`flysim
--reset-to-milestone N`); the shell script is the operator's wrapper.

Tests: catalog values and order; a synthetic WRAM trace of a catch (new,
repeat, cap, already-owned species, trainer/Safari/old-man/missed-ball
negatives, rollback replay); a v5 state restoring with the counter at zero; a
v5 checkpoint fixture accepted with the opt-in and refused without it; the
reset tool against copies of a state dir in temp directories; and a ROM-gated
catch from a rung-9 forest checkpoint, driven by the shipping THROW BALL macro.

The compatibility string differs from main's in exactly one segment, checked by
splitting both on `/`: pokered-unique8-v5 -> pokered-unique8-v6.
2026-09-22 17:30:00 +00:00
acamilo
d637b8f4c9 macros: a second ROM run, from the checkpoint the stream fell into after the badge
Forty minutes after the Boulder Badge the overworld pad had shrunk to `GO ROUTE` alone and it was
completing every 420 ms at a net of zero tiles. The trap hunt from that checkpoint on main is row
54 verbatim, one town further on: 14 distinct tiles in six brain minutes, 17 of 17 windows flagged,
`GO FRONTIER` 122 / `GO HEAL` 129 / `GO ROUTE` 126, every one done at a mean net of 0.0.

The run claims the fly leaves Pewter City for Route 3, that no chain of walks completes at a net of
zero tiles more than three times in a row, and that neither errand is offered in a town the run has
already shopped and healed in.
2026-09-22 17:19:25 +00:00
acamilo
c42255e199 macros: an edge the map graph cannot name stops being somewhere new once it is stood on
The rung-11 reading of row 54. Route 3's connections read north and west off the cartridge
(`wCurMapConnections`, and it has no warps at all) while the geography table carries west and east,
so the seven walkable tiles of its north edge had an unnameable destination -- and an unnameable
destination counted as unvisited, which made them first-tier for `GO ROUTE` on every hold for ever,
with `GO OBJECTIVE` off the pad beside them because nothing on that map leads to the objective.

A warp's destination is a byte the cartridge publishes, so `None` there is the `LAST_MAP` case
already handled. An edge's comes only from `geography::connected`, so `None` there means the table
cannot name the map on the other side and never will, and the only record left is the adapter's own
boundary ledger. It narrows: a genuinely new edge is still first-tier until the fly reaches it.

Which map is north of Route 3 is not guessed at here. That is a survey -- walk the fly off the edge
with real presses and read the map id back -- and it is recorded as a residual instead.
2026-09-22 17:19:25 +00:00
dev
9bed780cb9 flybus: wait for the pending-connection events instead of timing them
pending_connections_are_bounded_and_hello_expires gave the router 20 ms
to register a connection and then slept 80 ms past a 40 ms hello timeout
before reading the count once. Both are claims about how fast this box
is, not about the router: the first failed once in 20 whole-crate runs
beside four busy loops. Both now wait for the event they are about, under
the suite's ordinary ten-second bound, so the test still fails if a
pending connection never registers or a pending Hello never expires.
2026-09-22 17:12:46 +00:00
dev
2af1189ac3 Merge branch 'main' into fix/flybus-coalescing-flake 2026-09-22 17:05:29 +00:00
dev
f2dc5d434a flybus: assert the latest-mode guarantee, not the machine's timing
session_over_one_router asserted that a latest subscriber must drop
snapshots. bus-v1 section 7 says a latest subscription replaces an
undelivered value; it lets a consumer miss values, it does not oblige it
to. Under contention the publisher was slow enough that the renderer kept
up, saw all twenty snapshots and the test failed on conforming behaviour:
16 of 40 runs beside four busy loops, and 9 of 20 whole-crate runs.

The renderer is now held until the publisher's completion is observed
rather than until a timer expires, so the coalescing is forced instead of
raced for: the subscription keeps the one delivery in flight and one
replaceable queued value, and the renderer receives snapshots 1 and 20 of
20. The assertions are the guarantees that hold -- what arrives is in
publication order, the last value is the latest published, the stalled
spectator never refuses a publication or drops out of the fan-out, and
every snapshot the renderer missed is counted as a replacement to the
publisher at admission and to the renderer on delivery, so nothing is
lost silently.

Three more tests in the crate asserted the same kind of race:

- latest_replay_is_ordered_ahead_of_a_racing_publish demanded the
  non-coalesced outcome of a race section 7 allows either way ("bounded
  mode preserves that order, while latest mode may coalesce it"). It now
  puts one racing publication to a bounded and a latest subscription at
  once: bounded must deliver the replay and then the publication, and the
  latest branch is chosen by that publication's own replaced count.

- collection_waits_for_every_retained_owner, and the two disconnect
  cleanup tests beside it, read the store directory for the unlink that
  follows the registry update outside the router lock. They use
  settle_files, like every other unlink check in the suite.

- The demo example printed a root count taken before the producer's own
  release had reached the router, so the line the guide quotes was a
  race. It waits for the release, the same way it already waits for
  collection; the printed output is unchanged.

Nothing under flybus/src is touched: no routing defect was found. The
conformance rows for the credit/queue split, the replaced count, the
replay ordering and the latest spectator that cannot refuse a publication
now cite what each rewritten test actually varies.
2026-09-22 17:05:25 +00:00
acamilo
66ec2e5c6b session: an unknown launch option is an error, not a no-op
The command line between a launcher and a participant in its own process
was coupled by string convention on both sides and checked by neither: the
parser accepted any --flag it did not know, so a renamed or dropped option
would have become a fault that never fires or a delay that is never
applied, with nothing failing.

Every flag now has one name, in launcher::flags, written by the launcher's
argv and read by the parser. Options::parse takes the sets its command
allows and refuses anything outside them by name, so a mismatch fails the
launch. measure and measure-row use the same constants for their own
flags.

Three tests hold the two sides together: every flag a launch writes is one
its command accepts, every media option is written for a separate process,
and an unknown or wrong-command flag is refused by name. The four media
faults now also run end to end in all three execution modes, each failing
its own transition, so the wiring across a process boundary is proved
rather than assumed.
2026-09-22 16:28:59 +00:00
acamilo
01088d9479 feat(flysim): the on-screen chat ring survives a restart
Six deploys in a day left the CHAT panel empty every time, because the ring
was in-memory session state and nothing on disk held it.

Every accepted line now rewrites <hot_dir>/chat-ring.json through the same
atomic sequence a checkpoint commit uses (tmp, fsync, rename, directory
fsync), and Sim::boot reads it back before the first publish. Lines older
than 24 h are dropped on load, only the newest [chat] ring of them are kept,
and an absent, unreadable, unparseable or unknown-version file is an empty
panel and a logged warning rather than a startup failure -- which is exactly
what a restart gave before this existed.

The sidecar is deliberately not part of the checkpoint: no new chunk in the
FLYSIM01 envelope, nothing added to the compatibility string, so
--print-compatibility is byte-identical and a build that refuses every
checkpoint in a directory still brings the panel back. It sits beside the hot
checkpoints because it has their lifetime, and it is written from the sim
thread on the command path, so it cannot refresh that directory's mtime --
the watchdog's liveness signal -- while the loop is wedged.

Tests: the ring round-trips through the sidecar, a corrupt or future-version
file is ignored, lines older than a day are dropped, and the integration
test's SIGKILL restart now proves the panel comes back with the lines it had,
that the durable store holds no copy, and that no chat text is in the
envelope.
2026-09-22 16:26:28 +00:00
acamilo
f456fe9522 Merge main: the per-fly processes, the launcher and the thread budgets
Keep-both everywhere the two slices met. lib.rs takes both module sets.
AgentConfig keeps worker_threads and the sensor log; EnvironmentConfig
keeps worker_threads, the render delay and the render counter.
coordinator.rs keeps the two-stage resolution and its blame() beside the
media split of the Advance reply's attachments, and its imports take both.
harness.rs is main's launcher-based file with this slice's media
instrumentation re-applied on top.

The media instrumentation is shared memory, so it now follows the
launcher's own rule for the progress counter: sensor_log and renders
return None for a participant with a process of its own rather than a
misleading zero. The launcher carries the sensor log and the render
counter to a participant in this process and the render delay and the four
media faults on the command line to one in another process, where the
child builds its own log and counter.

The media path itself is mode-agnostic and is now tested as such: one
image per boundary, forwarded to every agent and published once, asserted
over the bus in all three execution modes, with the shared-memory
assertions made only where those participants live.
2026-09-22 16:07:30 +00:00
acamilo
a3c1c125cd session: negative fixtures for the allocation, and honest resolution bounds
Two review notes.

The launcher allocation became a required field of HelloResult.limits
with no negative fixture behind it. Three rows now cover it: missing,
above maxWorkerThreads, and zero. Both readers reject all three, and no
derived fixture moved, because invalid.json is not one of them.

The resolution's two bounds disagreed. 512 attempts at a 2 ms pause
give up near 1.5 s, so the attempt count silently pre-empted the 8 s
budget the doc comment advertised. The budget is now the working limit
and says so: the guard is 8192 attempts, over sixteen seconds of pauses
against an eight-second budget, so at the default values the budget is
always what fires. Which one did is no longer arithmetic either --
ResolutionEnd records it, the failure message names the bound and its
size, and the code, the doc comment and the README all state the same
numbers. a_resolution_says_which_of_its_two_bounds_ended_it drives each
bound to the end in every execution mode.
2026-09-22 16:00:13 +00:00
acamilo
77c8ee4558 session: resolve uncertain calls, and measure each mode in its own process
Review fixes for SESSION-02.

An expired caller deadline was becoming a failed epoch without the
ipc-v1 section 6 resolution. That procedure existed and was correct and
had exactly one caller, a test injection, so the deadline this slice
introduced bypassed it and a merely slow participant lost its epoch.
Deadlines is now the two-stage shape section 6 describes -- a probe,
then a bounded resolve budget and attempt count -- call_owned returns a
typed CallOutcome so an expiry is distinguishable from a refusal, and
Prepare, Commit, Advance and the lifecycle calls all query the same
request id against the same incarnation before the epoch can fail. This
is also step-v1 section 7's Advance row, which was imperative about it.

The coordinator peak-RSS column was measuring the measuring process.
VmHWM never falls and every row shared one process, so the column was
cumulative and the mode ranking reversed when the rows were reordered.
Each row now runs in a measure-row child of its own. The corrected
numbers say the opposite of what the first report claimed: the
coordinator's own peak is roughly flat across the modes and lowest in
process mode, and the cost of the split is the children.

workers-v1 section 2 bounded Agent.Initialize's workerThreads by
"within launcher allocation" and named no wire for it. Dated amendment:
HelloResult.limits gains workerThreads, the worker reports what its
launcher gave it, and the launcher refuses one that disagrees. The
schema set, the shared fixtures and the TypeScript package move
together; contractDigest changes, which ipc-v1 section 4 provides for.

Also: the stale-epoch row now reaches the stale-epoch path against a
live agent process and asserts exact codes on both halves; the
router-restart row asserts the handle drop it claimed; frames are
counted from the behaviour trace instead of calculated; the README says
which suites run over which transports; bootstrap is fence-guarded; the
shutdown reason is an Id rather than a silent fallback; and
agent_mutations returns None rather than zero where the counter lives
in another process.
2026-09-22 15:41:42 +00:00
acamilo
079842f818 media: no best-effort defaults in the audio path
Review round two.

A restored timeline refuses a declared stream with no recorded sample
position instead of resuming it at zero, which would have restarted the
episode's audio clock silently.

A declared stream that produces no chunk for a transition now fails the
step. The contract does not say a chunk must exist; this slice requires
one, because a silently missing chunk cannot be told from a lost one.
Boundary 0 is exempt: no interval precedes it.

The fresh-epoch discontinuity refusal is dropped rather than written into
the amendment. Only the restore direction is stated, and sections 6 and 7
both have a fresh timeline publishing a discontinuity after a recovery or
a reset, so refusing the flag at an origin contradicted them. The dated
amendment now says the requirement is one-directional.

A snapshot with an unreadable chunk, or one whose handle is not attached,
is now unreadable as a whole rather than quietly carrying less audio than
was published. ViewPipeline::is_bootstrap_repeat is removed.

New tests: a missing chunk fails the step, restored timelines need every
declared stream's position, and a 7 Hz world's seven chunks sum to exactly
one second of 48 kHz samples. The bus-conformance reconnect row also cites
the test that actually replaces an incarnation.
2026-09-22 15:15:06 +00:00
acamilo
8ef5dc321d macros: the Pewter run bounds the walks that complete without moving
Section 12.2's rule as a number on the cartridge: the longest chain of `GO ...` macros that
completed at a net of zero tiles. One is ordinary -- a walk that arrives by turning, a walk cut
short by a battle, a walk that finds its goal underfoot -- and a chain of them is the loop the
v0.4.5 after arm spent brain minutes 1.0 to 8.5 in. Three is the bound.
2026-09-22 14:57:34 +00:00
acamilo
611fe9e108 macros: an errand arrives inside the building, and a building already entered has none
Two rules for section 13's errands, both from row 54's `GO HEAL`: 204 starts at a mean net of 0.0
tiles and a mean reach of 0.0, cycling with `GO ROUTE` and `GO FRONTIER` over five tiles for seven
and a half brain minutes.

An errand's aim at a door carries no press -- the warp fires when it is stepped on -- so an aim on
the tile the fly is already standing on settles for `SETTLE_FRAMES` and reports `done` with the
world exactly as it was. A completed errand walk writes the reached ledger, which `goals_toward`
does not filter, so the same button was dealt on the next hold and the same nothing happened again.
`exit_goals` has excluded a settled goal underfoot since row 13; this is the one walk that did not
have the rule.

And the errand ledger is session state, so a restore re-armed every errand in the town and walked
the fly back to a counter it had already used. `map_visited` is the adapter's lifetime answer to
the same question and it does survive, so both are asked and either pays the errand.
2026-09-22 14:57:34 +00:00
acamilo
895fb52cda macros: the tile a step is landing on is ground the run has covered
The other half of the same measurement. `wXCoord` and `wYCoord` are the tile the step began on
until the frame it ends, so the stood ledger recorded ground the fly had already left and the tile
under it stayed unstood: `path::frontier` kept offering it, `GO FRONTIER` was dealt aiming one tile
away, and `Arrival::Step` reported `done` the instant the step it did not make landed. A macro that
completes without changing anything, which is section 12.2's trap.

A step that has begun always finishes -- the cartridge owns the animation and no press stops it --
and the screen has already centred on the tile, so it is ground this run has covered. It clears the
map's frontier mark on the same rule the coordinates do: only ground never stood on before.
2026-09-22 14:57:34 +00:00
acamilo
31f35c6f2f macros: the whole-map grid is read from the tile the screen is centred on
Measured on the cartridge from the rung-10 checkpoint: `wXCoord` and `wYCoord` change at the *end*
of a sixteen-frame step while the background scrolls throughout it, so for fifteen frames of every
sixteen the screen buffer is centred one tile ahead of the coordinates. The cross-check compared
the decode of the fly's own tile with the screen's reading of the tile ahead and refused. Pewter
City decoded on 118 of 120 standing frames and on none of the moving ones, so every walk the fly
actually took was re-planned over the ten-by-nine window, which is the oscillation of row 23.

Nothing in the pinned symbol table says "a step is in progress" and a new address cannot be pinned
without the disassembly `gen_symbols.py` reads, so the anchor is measured rather than named: the
screen is centred on the fly's tile or on one of its four neighbours, and the one it is centred on
is the one whose whole neighbourhood agrees with the decode. A decode with a wrong stride, a wrong
quadrant or a half-loaded map agrees with none of the five, and neither does the mid-warp tear the
per-serve check was added for, so both refusals stand.

`state::step_destination` names the tile the step is landing on, for the ledger the next commit
writes. The town fixture gains two landmarks beside the fly, because a neighbourhood that is the
same tile id in every direction cannot tell one anchor from another.
2026-09-22 14:57:34 +00:00
acamilo
ed1d321b02 probes: a mid-step survey, and the errands on the pad dump
`FLY_PROBE_CATCH=step` holds one direction from a checkpoint and prints, per frame, the
coordinates, the grid's verdict, the tiles the cross-check disagreed on and every candidate for
"a step is in progress". It is what row 54 turns on: whether the coordinates change at the start
of a step or at its end.

The pad dump gains section 13's two errands -- which building each names, whether the ledgers have
paid it, and what the walk would aim at -- because an aim with no press settles where it stands.
2026-09-22 14:57:33 +00:00
acamilo
903629db00 docs: what MEDIA-01 enforces in state-media-v1 section 2
Two readings of the section that are now checked rather than assumed: the
bootstrap window is exactly the boundaries where max(0, boundary - delay)
is zero and its repeated O[0] is the same artifact, and a chunk that
continues the previous one exactly cannot claim a discontinuity, which is
what keeps the restore rule checkable instead of advisory.
2026-09-22 14:54:48 +00:00
acamilo
438eaf3130 Merge main at cb9a88c: the Pewter macro work, which this slice does not touch 2026-09-22 14:33:30 +00:00
acamilo
7b7ebcdf28 session: parallel processes, a launcher and the fault behaviour
SESSION-02: one agent process per fly and one environment process under
the coordinator over the Unix-socket transport, compared against the
in-process composition and a dedicated-thread variant. The mode is the
only thing that changes; the composition, the coordinator, the workers
and the router are the same code in all three.

The launcher is the configured supervisor. It owns a total thread budget
with one allocation per participant, refused as BUSY before anything
starts when the total cannot cover it; the configured client, service,
worker and port identities, proved in Worker.Hello before the
coordinator pins a registration; Worker.Status health on the
supervisor's own monotonic clock at the ipc-v1 section 6 budgets; and
reaping, where Worker.Shutdown is the request and the operating system
is the guarantee.

The worker executable is a subcommand of this crate's one binary, which
is what implementation.md section 2 allows in place of a separate
worker crate.

The coordinator's fault behaviour: every failure names the participant
it is attributed to, every domain call has a caller-side deadline so a
dead participant is a diagnosed outcome rather than a hang, and failing
fences the epoch -- the boundary stops, the handles drop, and no further
transition or publication is allowed. Agent.Initialize now carries the
launcher's allocation, and an agent refuses one asking for more.

tests/processes.rs proves every acceptance bullet once per execution
mode, and the two section 4 rows SESSION-01 could not reach in one
process: a router restart during a world advance, and an old worker's
reply after a restart. measure compares the three modes at one, two and
four agents; its table is in the crate README, and it is not a capacity
claim.
2026-09-22 14:33:25 +00:00
acamilo
8d999b2881 Merge branch 'main' into feat/sf-media-01 2026-09-22 14:30:34 +00:00
acamilo
8afeaf6c92 bus: wait for the teardown the disconnect test asserts
caller_disconnect_cleanup_works_before_and_after_consumption failed 37 times
in 240 runs under load: the reply raced the router's own teardown. op_reply
reports routed:false only once disconnect has marked the call detached, and
the router runs that when its connection task reads EOF, while Client::close
waits for this client's reader only. The reply could therefore reach a
connection that was still closing and be routed. Nothing escaped -- teardown
releases those roots -- but the flag was read one step early.

The test now settles on the caller's connection being gone before asserting
the section 6 sentence, which is the poll the rest of the file already uses.
360 runs after the fix, 0 failures.

bus-conformance.md records the mechanism, the counts and the two other
intermittent failures seen in the same sweep, which are left to the bus slice.
2026-09-22 14:29:57 +00:00
acamilo
40c6a88949 media: native observation schemas and the presentation handoff
Puts MEDIA-01 on top of the bus ArtifactRef rather than beside it.

fly-session-types gains the two validators a descriptor and a reference
cannot carry on their own: AudioTimeline, which holds one stream chunk
sequence for one epoch (no overlap, no backwards, the first chunk after a
restore marks the discontinuity), and check_imported_asset, which checks
that a transient artifact carries an installed asset content without ever
converting one identity into the other.

fly-session gains a media module: a ViewPipeline that renders one native
frame per boundary and serves the frame the declared observationDelaySteps
requires (so bootstrap repeats O[0] exactly while the pipeline fills), an
AudioSource with an exact rational sample budget and a deterministic
integer-phase waveform, the Phase C acceptance checks, a latest-subscription
Spectator with finite credits, a SensorLog recording what each agent read,
and an AssetRegistry for installed persistent content.

The counter arena now emits a real 32x24 RGBA8 pattern per boundary and one
audio chunk per transition; the coordinator forwards the one owned view
handle to every agent Commit and publishes the same handle, with the audio
handles, for presentation.

36 new tests: 12 shape rules against the contract crate and 24 session-level
acceptance tests over both transports.
2026-09-22 14:28:58 +00:00
acamilo
d2e2d2e51d Merge fix/loop-20260922T1217: readable YES/NO prompts deal their answers, the nurse enters the talked ledger, a prompt that reopens unchanged is excluded 2026-09-22 14:25:21 +00:00
acamilo
e80df561dd session: take Worker.Acknowledge's bound from the contract, not a local copy
The CONTRACT-01 amendment narrows Worker.Acknowledge ids to the req-<U64> form, which this
crate already read as DomainRequestId. The 1..=16 bound now comes from the contract's
MAX_ACKNOWLEDGE too, so there is one definition of it rather than two.
2026-09-22 13:36:42 +00:00
acamilo
ea4935ade2 Merge feat/sf-contract-01 at 57de812: the checked rational addition, the schema set held to the readers list and the Worker.Acknowledge id amendment 2026-09-22 13:17:35 +00:00
acamilo
0ace07c57c Merge main: the bus slice, whose write-gate fix removes the flybus teardown flake under parallel load 2026-09-22 13:17:30 +00:00
acamilo
d5a21e9a5d traps: rows 51 to 53, the museum's missing rows and a BACK with no box under it
The reproduction, the three surveys and the before/after for the rung-10 Pewter stall, with the
ROM-gated run's table. Rows 51 (UNKNOWN's two states), 52 (a building with no row on the map
graph) and 53 (a frontier excluded by a window that lapses) each with their tests.
2026-09-22 13:14:47 +00:00
acamilo
57de8126d5 fix(session-types): check the rational addition, and hold the schema set to the readers list
Two fixes from the independent review of f043cd7.

RationalNs::checked_add cross-multiplied into u128 and then added without a check. Each
product fits, but their sum does not: two reduced fractions near the U64 maximum add to about
2^129, which panics in debug and wraps in release, after which the reduction returns a
confidently wrong rational. Every multiplication and the addition now go through a checked
path, as ipc-v1 section 2 requires, with a fixture row and a test that reaches the overflow
rather than the "does not fit U64" reduction limit.

The schema-set test checked fourteen hardcoded type names, so a new payload type could have
shipped outside contractDigest. It now iterates the readers own list, all fifty of them.

Also amends ipc-v1 section 5, dated: Worker.Acknowledge carries req-<U64> request ids, not
arbitrary Ids, because the serial watermark rule in that same section cannot reject reuse
after acknowledgment unless the acknowledged id carries its serial.
2026-09-22 13:12:59 +00:00
acamilo
4edc4c9bcf tests: ROM-gated, the fly gets out of the museum and into the gym
From the live rung-10 checkpoint with the stub rotation: map 0x34 to map 0x36 on frame 2423, on
15 macros, against never in five and a half live hours. TALK is on the pad inside the gym -- the
fly standing in front of one of the people rung 11's place names -- and it presses it 15 times
there. BACK on a dialog or unknown frame is 0 against the 189 the hunt measured on Pewter City
alone, no frame deals a pad on an unknown with nothing drawn on it, and GO FRONTIER starts 0
times on the museum's two floors.

The unit test beside it is the other half of "the objective reaches the leader": on the gym's
own map the rung's place resolves to the person standing in it, the walk aims at the four tiles
around them, the door is not a candidate while they are there, and talking to them empties the
list. It also pins what the ROM run showed: the town's errands come first for every map in the
area, the gym included, so both have to be paid before the objective is the leader.
2026-09-22 13:12:31 +00:00
acamilo
574e9fec0c Merge feat/sf-contract-01 at f043cd7: packages/session-types, the canonical number rule and the seed/envelope specifications 2026-09-22 13:03:56 +00:00
acamilo
0d098ee408 session: both transports for every integration test, and the mid-step pause written into step-v1
Review found two Memory-only tests behind a README claim that every integration test runs
twice. Both are now generated by both_transports!: the one-agent composition, and the
old-epoch refusal, which is one of the three tests the old-worker-replies row cites. The
README says which single test walks both transports inside itself instead, because it
compares their traces against each other.

step-v1 section 2 gains the mid-step pause line and a dated amendment saying why it is not
a new edge: a pause requested during a transition is the ordinary
Committing(k) -> Ready(k+1) edge followed by Ready(k+1) -> Paused(k+1), because section 6
requires the transition to finish first, so the only boundary such a pause can land on is
the one the transition just committed.
2026-09-22 13:00:14 +00:00
acamilo
e0285016aa ratchet: getting nearer the objective is progress the coverage figure cannot carry
Two Stuck rollbacks fired on rung 10 inside half an hour, both on a fly that was walking, and
both were the ratchet working to contract: the stall window is reset by exploration -- one tile
the run has never stood on -- and a fly crossing a town it has already covered to reach the
rung's own door earns none of it. Entering a map for the first time already counts, because a
new map is ground nobody has stood on; re-entering one does not, which is what the museum was.

So the window gains a second signal, passed in by the caller and meaningless to the ratchet
itself, exactly as coverage is: the Pokemon loop answers with "nearer the objective, in map
hops, than this run has ever been", over the same map graph GO OBJECTIVE walks. It can fire at
most once per step of the road, it spends no budget, it captures nothing and it skips no
trigger. Nothing in the macro layer reads it back and no button is bound on it.

The checkpointed ratchet state is untouched: the signal is a level on one sample, not a counter.
2026-09-22 12:55:39 +00:00
acamilo
26f957e084 macros: a frontier no walk can reach is a fact about the map, not a window
GO FRONTIER was 1,235 of the rung-10 run's macro starts in 47 minutes, over two museum floors
and a town whose ground the fly had already covered. The tiles it aimed at were real and
unreachable: 98 walkable tiles on the museum's ground floor, 62 of them reachable from the door,
39 never stood on and almost all of those behind the admission desk. A walk that can reach none
of its goals refuses no route and writes every goal to the blocked ledger -- which is a ten
brain minute window, so all of it came back and the refusal happened again, once per hold.

A window is right for a target somebody is standing in front of and wrong for ground the map has
fenced off. So the refusal is remembered per map instead, with no window, and it is cleared by
the one event that can change the answer: the fly standing somewhere on that map it had not
stood on before -- a door opened, a script carried it through, somebody moved out of a doorway.
Re-entering the map clears nothing, which is the loop the window made.
2026-09-22 12:48:09 +00:00