Commit graph

144 commits

Author SHA1 Message Date
dev
5e61c50728 session: an unreadable event batch is not the end of the stream
take_events kept returning Option<EventBatchView> and defaulting through the
question-mark operator, so a batch missing a field read as end of stream and the
ConsumerEvents enum added in the previous round described nothing. It returns
Batch or Unreadable now, and a test publishes a batch with no droppedBefore,
asserts it is reported as unreadable naming the field, and asserts the next real
batch still reads.

The checkpoint-envelope-v1 amendment cites the rule that lets a required
manifest field land with envelopeVersion still 1 while no production file
exists.
2026-09-22 21:03:59 +00:00
dev
305a50d8df session: a graph identity does not cross a recovery
The index an agent attested to at Agent.Initialize joins its compatibility
identity and its checkpoint manifest row, so a replacement fly that built
another graph -- the same dataset, the same neuron count, another index --
cannot install a checkpoint taken under the first one. It was accepted before,
because agent_compatibility digested a dataset digest recomputed from a free
function rather than what the worker attested to, and the restored composition
was then published under its predecessor's indexDigest.

The refusal names what the worker is, not just that two digests differ. Dated
amendment to checkpoint-envelope-v1's agents row; no wire type and no schema
text change, so the contract digest is unchanged.
2026-09-22 20:36:51 +00:00
dev
6bf5687aca session: a snapshot publishes the telemetry of the transition that just ended
AgentSlot.telemetry was written only by the Agent.Initialize handler, so every
CommittedSnapshot carried warm-up telemetry labelled as boundary k while each
AgentCommitResult.telemetry was validated and dropped. The commit result is now
stored on the slot beside the committed step, and the media test asserts that
published telemetry advances across boundaries instead of merely being nonzero.

A refused snapshot is recorded and sequenced like a refused descriptor revision,
because the repair path exists for the consumer that did not receive it; the
sequence advances with the value rather than with the delivery, so two snapshots
can never share one. The query service counts an answer it could not deliver
rather than discarding the result, and an unreadable event batch is distinct
from the end of the stream.
2026-09-22 20:18:49 +00:00
dev
7ce645f1dc session: a restored boundary is an installed one, and the revision follows the composition
A group restore re-establishes a committed boundary this epoch did not run a
transition into, so its snapshot carries no decision and no controls for any
agent, and a fresh epoch is a new compositionDigest, so the descriptor takes the
next revision rather than republishing revision 1 with different contents.

CommittedSnapshot's rule becomes: null at boundary 0 and at an installed
boundary, always together, and for every agent or none -- a snapshot where one
fly acted and another did not would be two boundaries in one value. Dated
amendment to publishing-v1 section 3, with the schema set, the fixtures and the
TypeScript package moved together and the digest regenerated.
2026-09-22 19:24:44 +00:00
acamilo
4f2d4a5848 Merge main into feat/sf-publish-01 2026-09-22 19:03:01 +00:00
dev
e27306f171 Merge main into feat/sf-publish-01
# Conflicts:
#	services/flysim/crates/fly-session/README.md
#	services/flysim/crates/fly-session/src/coordinator.rs
#	services/flysim/crates/fly-session/src/harness.rs
#	services/flysim/crates/fly-session/src/launcher.rs
2026-09-22 19:02:57 +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
083abe5f1f Merge feat/sf-state-01: a coherent all-participant checkpoint, group restore and a liftable fence 2026-09-22 18:57:18 +00:00
acamilo
2237e9a1a2 Merge main into feat/sf-publish-01 2026-09-22 18:54:26 +00:00
dev
f43fd4d9ff session: supportedStimuli is enforced, and the query method names are provisional
An undeclared stimulus kind is refused before the model is touched, proved by an
injection through Agent.Commit rather than by a unit call, so the declaration a
descriptor publishes is the thing the worker enforces.

The publishing-v1 section 2 amendment now says in its own words that
Session.GetDescriptor and Session.GetSnapshot are internal and provisional names,
which the later public v2 step may rename or supersede.
2026-09-22 18:47:10 +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
55221a7c77 docs: v0.5.0 status 2026-09-22 18:41:17 +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
acamilo
537cdd8ad9 Merge fix/flybus-conformance-rows: the conformance rows name the tests that now prove them
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:29:12 +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
acamilo
ed4cffcf59 Merge main into feat/sf-publish-01 2026-09-22 17:59:43 +00:00
dev
db30708c3f Merge main into feat/sf-state-01 2026-09-22 17:55:03 +00:00
acamilo
e76b3d1063 docs: v0.4.7 status 2026-09-22 17:46: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
dev
4effc6020c session: the publication boundary, observer isolation and the repair path
publishing-v1 over the same bus: a declared delivery policy per topic, named
publication outcomes, the bounded event batch, application-owned state and cues,
a read-only descriptor query service and a fake multi-agent consumer.

The session publishes the contract types rather than an ad-hoc payload, so a
descriptor says what the workers attested to and a snapshot is checked against it
before it is published and again when it is read: every frame comes from the
boundary its declared delay implies, every handle is the artifact its reference
names, and an observer's refusal takes no world step and fences no epoch.

AgentInitializeResult gains graph (datasetDigest, indexDigest, neuronCount,
rateRoles, supportedStimuli), without which no AgentDescriptor field in
publishing-v1 section 3 had a source. Dated amendments to workers-v1 section 2,
publishing-v1 section 2 and state-media-v1 section 3.
2026-09-22 17:41:07 +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
95d3d56d3f docs: the rung-eleven ROM run, measured 2026-09-22 17:24:27 +00:00
acamilo
bffbaca394 docs: row 54, the mid-step survey and both arms of the hunt
macros.md gains section 12.17; macros-wram.md section 9 gains the frame-by-frame trace of a step,
the three readings that come out of it and the one byte that tracks a step exactly and is recorded
unused; macros-traps.md closes rows 54 and 54b, strikes the two residuals they worked, and prints
both checkpoints' before and after whole -- including the arm where the ethos check's "fewer
flagged windows, more distinct tiles" does not hold, and why.
2026-09-22 17:19:25 +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
56db91cd9b docs: record the session framework wave 2
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 16:42:17 +00:00
acamilo
a301527b09 Merge feat/sf-media-01: native view and audio observations, the shared frame, and the spectator path 2026-09-22 16:30:04 +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
533cf82ac8 infra(05-deploy): FLY_RESET_STATE=1 clears the chat ring too
The deliberate reset already clears the tmpfs hot ring; the on-screen chat
ring's sidecar lives there now, so it goes with it.
2026-09-22 16:26:29 +00:00
acamilo
403038aa42 docs(control-api): the chat ring's sidecar, in the [chat] section
Dated 2026-09-22: where the file lives, what is dropped on load, what an
unreadable one does, that it is not in the checkpoint envelope or the
compatibility string, that FLY_RESET_STATE=1 clears it, and that the bridge
resends nothing on reconnect.
2026-09-22 16:26:29 +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
ba545d9225 Merge feat/sf-session-02: per-fly processes, a launcher with thread budgets, and the fault behaviour 2026-09-22 16:00:57 +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
a48e0ace4c docs: the fresh-epoch discontinuity reason rests on section 6 alone 2026-09-22 15:18:47 +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