Let a run revise its own configuration, within bounds - #1
Conversation
Document the adaptation behavior and requirements to provide a shared reference for implementation and review. Auto-committed-on: dragonfly Co-authored-by: Medulla <medulla@tinyhumans.ai>
Add the adaptation specification to the documentation index and relate it to the loop kernel spec so its role in proposing invariant changes is clear. Auto-committed-on: dragonfly Co-authored-by: Medulla <medulla@tinyhumans.ai>
Document the decision to determine threshold addressing from run state, clarifying the behavior and rationale for future implementation. Auto-committed-on: dragonfly Co-authored-by: Medulla <medulla@tinyhumans.ai>
Update the loop-kernel specification to require routing thresholds to be read from shared run state rather than rendered into graph JSON. This preserves parity between the graph and Rust implementations while allowing threshold revisions without changing graph topology or preventing run resumption. Auto-committed-on: dragonfly Co-authored-by: Medulla <medulla@tinyhumans.ai>
Document that re-planning cadence is governed by threshold intervals rather than a separate profile field. Clarify that presets own amendment bounds while embedders may only narrow them, with preset bounds used when deployment bounds are absent. Auto-committed-on: dragonfly Co-authored-by: Medulla <medulla@tinyhumans.ai>
Document parity as coverage over a declared threshold box rather than a proof over the full tunable space. Update the planned validation to use a pinned compile-fail doctest and remove the resolved question about bounds ownership. Auto-committed-on: dragonfly Co-authored-by: Medulla <medulla@tinyhumans.ai>
Document that the loop head uses the run budget's maximum iteration cap as a fixed runaway backstop rather than the mutable attempt threshold. This explains why changing max attempts mid-run does not affect the compiled loop limit. Auto-committed-on: dragonfly Co-authored-by: Medulla <medulla@tinyhumans.ai>
Document exhaustive parity checks across preset tuples and bounds corners, along with validation that attempt limits do not exceed the loop iteration cap. Link the adaptation specification to ADR 0006. Auto-committed-on: dragonfly Co-authored-by: Medulla <medulla@tinyhumans.ai>
Document the adaptation plan to clarify the intended approach and guide future work. Auto-committed-on: dragonfly Co-authored-by: Medulla <medulla@tinyhumans.ai>
Document the planned approach for tuning adaptation behavior and provide guidance for future implementation. Auto-committed-on: dragonfly Co-authored-by: Medulla <medulla@tinyhumans.ai>
Add `LoopProfile` to the public policy API so each run can carry its configuration and the ladder can address it directly. Auto-committed-on: dragonfly Co-authored-by: Medulla <medulla@tinyhumans.ai>
Presets now serialize and deserialize using stable lowercase names for use in run profiles. Balanced is also the default preset for callers that need an implicit choice. Auto-committed-on: dragonfly Co-authored-by: Medulla <medulla@tinyhumans.ai>
Re-export LoopProfile from the tinyloops crate so consumers can use the loop profile API directly. Auto-committed-on: dragonfly Co-authored-by: Medulla <medulla@tinyhumans.ai>
Include the loop profile in cloned loop state so checkpointed and folded runs retain the configuration used for routing thresholds. yaml Auto-committed-on: dragonfly Co-authored-by: Medulla <medulla@tinyhumans.ai>
Allow callers to initialize loop state with a chosen profile while preserving the balanced profile as the default. Document the new constructor and verify its preset-specific thresholds in an example. Auto-committed-on: dragonfly Co-authored-by: Medulla <medulla@tinyhumans.ai>
Generate a constant ladder that reads thresholds from the accumulator instead of embedding preset values. Use a maximum-value sentinel for missing thresholds so unprofiled states fall through to retry. Auto-committed-on: dragonfly Co-authored-by: Medulla <medulla@tinyhumans.ai>
Make `terminal_condition` generate a constant expression that reads thresholds from the run state. This allows each profile to apply its own limits without passing thresholds during expression construction. Auto-committed-on: dragonfly Co-authored-by: Medulla <medulla@tinyhumans.ai>
Simplify ladder and terminal condition evaluation by removing the caller-supplied thresholds parameter. Update the examples to use the streamlined evaluator APIs. Auto-committed-on: dragonfly Co-authored-by: Medulla <medulla@tinyhumans.ai>
Route, terminal, outcome, and termination logic now read thresholds from the loop state profile instead of accepting them separately. This keeps policy decisions consistent with the state’s configured profile and simplifies the APIs. Auto-committed-on: dragonfly Co-authored-by: Medulla <medulla@tinyhumans.ai>
Simplify termination, routing, and outcome APIs to evaluate loop state without passing thresholds. This removes redundant context from policy decisions and their callers. Auto-committed-on: dragonfly Co-authored-by: Medulla <medulla@tinyhumans.ai>
Auto-committed-on: dragonfly Co-authored-by: Medulla <medulla@tinyhumans.ai>
Use the thresholds stored in the loop state when running registered steps instead of accepting caller-provided values. This ensures step routing uses the configuration associated with the current state. Auto-committed-on: dragonfly Co-authored-by: Medulla <medulla@tinyhumans.ai>
LoopBuilder now seeds runs with a LoopProfile and accepts configurable Caps while keeping routing thresholds out of the emitted topology. Only the iteration limit is embedded in the graph as the runaway backstop. Auto-committed-on: dragonfly Co-authored-by: Medulla <medulla@tinyhumans.ai>
Update examples to use the simplified `LoopBuilder` constructor and run registry steps without passing thresholds. This keeps execution APIs consistent with the revised loop configuration. Auto-committed-on: dragonfly Co-authored-by: Medulla <medulla@tinyhumans.ai>
The merge step now receives only its state and arms payload, avoiding an unnecessary threshold argument. Auto-committed-on: dragonfly Co-authored-by: Medulla <medulla@tinyhumans.ai>
Clean up unused `Thresholds` imports from termination and step modules without changing behavior. Auto-committed-on: dragonfly Co-authored-by: Medulla <medulla@tinyhumans.ai>
Adjust step and tool tests to use the execution API without threshold arguments. Keep coverage aligned with the simplified invocation signatures. Auto-committed-on: dragonfly Co-authored-by: Medulla <medulla@tinyhumans.ai>
Add coverage confirming that a step receives the threshold configuration carried by its loop state. This prevents routing decisions from using thresholds that are not configured for the current run. Auto-committed-on: dragonfly Co-authored-by: Medulla <medulla@tinyhumans.ai>
Stamp tuner amendments with the current arm and pass before recording them. This prevents custom tuner implementations from misattributing revisions in the run record. Auto-committed-on: dragonfly Co-authored-by: Medulla <medulla@tinyhumans.ai>
Update the observation type definitions to support the latest changes in the tinyloops observation API. Auto-committed-on: dragonfly Co-authored-by: Medulla <medulla@tinyhumans.ai>
Use the default `Rules` configuration when the test preset proposes changes, keeping it compatible with the current rule definition. Auto-committed-on: dragonfly Co-authored-by: Medulla <medulla@tinyhumans.ai>
Explain that the silence rule uses the assembled muting window, how research loops configure it, and how the default preserves the shipped preset behavior. Auto-committed-on: dragonfly Co-authored-by: Medulla <medulla@tinyhumans.ai>
Clarify that tuned research loops use each preset's muting window consistently for proposing and folding mutes, while direct tuner wiring falls back to the shipped default. Auto-committed-on: dragonfly Co-authored-by: Medulla <medulla@tinyhumans.ai>
Reuse the existing proposer string allocation when recording amendments to reduce unnecessary allocations without changing behavior. Auto-committed-on: dragonfly Co-authored-by: Medulla <medulla@tinyhumans.ai>
Use the collection’s `contains` method to make the declared arm membership check more direct. Auto-committed-on: dragonfly Co-authored-by: Medulla <medulla@tinyhumans.ai>
Update tuner rule helpers to take ownership of the rules value without changing their behavior. Auto-committed-on: dragonfly Co-authored-by: Medulla <medulla@tinyhumans.ai>
Extend observer coverage to verify that amended and amendment_refused events are recognized as spine events. Auto-committed-on: dragonfly Co-authored-by: Medulla <medulla@tinyhumans.ai>
Add tests confirming that muting the concluding arm or an undeclared arm is rejected during assembly with the appropriate error. Auto-committed-on: dragonfly Co-authored-by: Medulla <medulla@tinyhumans.ai>
Update the checkpoint signature invariant to hash node configuration and addressing rather than rendered thresholds, reflecting the amended threshold model. Explain why stable graph shape and declared identity prevent corrupted resume state while allowing runs to revise their thresholds. Auto-committed-on: dragonfly Co-authored-by: Medulla <medulla@tinyhumans.ai>
Clarify that invariant 9 still hashes the emitted graph and rejects mismatches, while its rationale is amended because invariant 7 no longer renders every threshold. Auto-committed-on: dragonfly Co-authored-by: Medulla <medulla@tinyhumans.ai>
Clarify that threshold changes no longer alter the routing program while different presets may still produce distinct GraphSignatures through their seed accumulators. This distinguishes the shared compiled routing behavior from each run’s starting profile. Auto-committed-on: dragonfly Co-authored-by: Medulla <medulla@tinyhumans.ai>
Add coverage ensuring a cap amendment that makes the combined run budget unreachable is refused without changing the profile, while recording the appropriate history message. Auto-committed-on: dragonfly Co-authored-by: Medulla <medulla@tinyhumans.ai>
Add coverage for the concluding arm error message to ensure it renders the expected text for readers. Auto-committed-on: dragonfly Co-authored-by: Medulla <medulla@tinyhumans.ai>
There was a problem hiding this comment.
💡 Codex Review
Here are some automated review suggestions for this pull request.
Reviewed commit: 8bb33e683e
ℹ️ About Codex in GitHub
Your team has set up Codex to review pull requests in this repo. Reviews are triggered when you
- Open a pull request for review
- Mark a draft as ready
- Comment "@codex review".
If Codex has suggestions, it will comment; otherwise it will react with 👍.
Codex can also answer questions or update the PR. Try commenting "@codex address that feedback".
| let budget = self.budget.narrow(state.profile.caps)?; | ||
| if let Some(tripped) = budget.tripped(&meter) { |
There was a problem hiding this comment.
Apply amended caps to the work that consumes them
When Rules halves max_model_calls, this narrowing does not actually reduce spend: drive only calls meter.pass(...), so the meter's model-call and token counters remain zero, and the emitted graph path never reads state.profile.caps at all. A blocked run can therefore record the cap amendment as applied while continuing under the original provider/role budgets, potentially spending twice the newly reported allowance. Propagate the amended caps into call accounting and the graph harness rather than only checking an unchanged meter.
AGENTS.md reference: AGENTS.md:L13-L15
Useful? React with 👍 / 👎.
| /// [`Self::apply`] carries it through the fold untouched and neither | ||
| /// [`Delta`] nor [`Contribution`] has a slot that reaches it, so no arm can | ||
| /// move it however it is wired. | ||
| pub profile: LoopProfile, |
There was a problem hiding this comment.
Protect live profiles from ordinary step mutation
For a custom assembled loop, every advancing Step receives an owned LoopState and returns the whole state, so making profile public lets an ordinary attempt or planning step directly assign thresholds, caps, muted, revision, or history. That modified profile becomes the merge base and survives the pass without an Amendment, bounds check, revision record, or event, defeating the guarantee that only the tuner can move configuration within declared bounds. Expose read access while requiring live mutations to go through the amendment fold.
AGENTS.md reference: AGENTS.md:L13-L15
Useful? React with 👍 / 👎.
| if let Some(amendment) = state.proposed.take() { | ||
| state.profile.fold(amendment, &self.bounds); |
There was a problem hiding this comment.
Emit amendment events from graph-run pass steps
When the emitted WorkflowGraph executes STEP_PASS through run_loop_step, this fold updates the profile history but emits neither Event::Amended nor Event::AmendmentRefused: those events are synthesized only by AssembledLoop::drive, while run_loop_step has no recorder and RunObserver reports only engine node lifecycle events. Consequently, durable graph runs—the shipping execution path—quietly retune even though their final state records a revision, leaving observers without the audit event promised for every amendment and refusal.
AGENTS.md reference: AGENTS.md:L13-L15
Useful? React with 👍 / 👎.
Include an amendment allowance in the bounds so the test verifies that combined budget constraints reject an unsafe cap change. Auto-committed-on: dragonfly Co-authored-by: Medulla <medulla@tinyhumans.ai>
Update the test to verify that a refused cap amendment leaves the caps unchanged without requiring the entire profile to match. Auto-committed-on: dragonfly Co-authored-by: Medulla <medulla@tinyhumans.ai>
Apply consistent Rust formatting to policy test assertions without changing their behavior. Auto-committed-on: dragonfly Co-authored-by: Medulla <medulla@tinyhumans.ai>
There was a problem hiding this comment.
tinysweeper found nothing blocking. Approving.
$0.0904 · 752,894 in / 54,696 out · 360,609 cached (48%) · openrouter/openai/text-embedding-3-small, deepseek/deepseek-v4-flash, z-ai/glm-5.2 · 769 embedded
critique: $0.0291 · 250,902 in / 10,776 out · 23,627 cached (9%) · deepseek/deepseek-v4-flash, z-ai/glm-5.2
security: $0.0147 · 154,109 in / 2,191 out · 27,009 cached (18%) · deepseek/deepseek-v4-flash, z-ai/glm-5.2
tests: $0.0205 · 237,746 in / 26,438 out · 213,248 cached (90%) · z-ai/glm-5.2
description: $0.0260 · 110,137 in / 15,291 out · 96,725 cached (88%) · z-ai/glm-5.2
| state.steer = "narrow the claim".to_owned(); | ||
|
|
||
| let once = Advance | ||
| let once = Advance::default() |
There was a problem hiding this comment.
Advance::default() may no longer exist
The diff replaces Advance::new(…) with Advance::default() at line 815, but Advance is a struct holding Bounds and a reference to ArmSet — it is not obvious that it has a Default impl, and if it does, the default parameters (empty bounds? no arms?) would break the test because the test relies on Advance actually folding the profile amendment. If Default does not exist or produces a no-op step, the test will fail at runtime. The earlier finding that recommended Advance::default() may have been about a different Advance or a different context; here the constructor signature matters. The author should verify that Advance has a Default impl and that its semantics match what the test expects.
[RULE] api-change-check ·
| assert_eq!(twice, once); | ||
| assert!(once.steer.is_empty()); | ||
| assert_eq!(Advance.name(), crate::step::STEP_PASS); | ||
| assert_eq!(Advance::default().name(), crate::step::STEP_PASS); |
There was a problem hiding this comment.
Advance::default() referenced a second time without confirmation
Same concern as the previous finding: Advance::default() is used to get the step name. Even if Default exists, it is a stylistic choice that differs from the earlier pattern in the same file where Advance was constructed explicitly. If Default does not exist this will not compile.
[RULE] api-change-check ·
| /// | ||
| /// [`Bounds::muting_window`]: crate::Bounds::muting_window | ||
| #[must_use] | ||
| pub fn new(window: u32) -> Self { |
There was a problem hiding this comment.
Reject out-of-range window in the constructor instead of silently capping
Rules::new accepts a u32 but stores a usize. On a 32-bit target, usize is 32 bits and no conversion fails — but on a 64-bit target a u32 value above usize::MAX is impossible. The real problem is conceptual: the public window parameter is typed u32 while SILENT_SCORES (the default) is a usize. If a caller passes a value beyond what usize can hold on a hypothetical platform, unwrap_or(usize::MAX) silently caps it, turning an obviously wrong value into a mostly-right one without signalling the mistake. The method should document that window is in the same domain as SILENT_SCORES (which is usize) and accept usize directly, or return Result when the conversion fails.
[RULE] fallible-arg-boundary ·
Set the workspace MSRV to Rust 1.95 because the all-features CI build includes tinyagents code that uses if let guards unavailable on earlier versions. Auto-committed-on: dragonfly Co-authored-by: Medulla <medulla@tinyhumans.ai>
Use `Duration::from_mins(30)` to make the 30-minute default timeout clearer without changing its behavior. Auto-committed-on: dragonfly Co-authored-by: Medulla <medulla@tinyhumans.ai>
Replace manual second calculations with hour and minute duration helpers to improve readability without changing test behavior. Auto-committed-on: dragonfly Co-authored-by: Medulla <medulla@tinyhumans.ai>
There was a problem hiding this comment.
💡 Codex Review
Here are some automated review suggestions for this pull request.
Reviewed commit: 2afe3b337b
ℹ️ About Codex in GitHub
Your team has set up Codex to review pull requests in this repo. Reviews are triggered when you
- Open a pull request for review
- Mark a draft as ready
- Comment "@codex review".
If Codex has suggestions, it will comment; otherwise it will react with 👍.
Codex can also answer questions or update the PR. Try commenting "@codex address that feedback".
| /// [`Self::apply`] carries it through the fold untouched and neither | ||
| /// [`Delta`] nor [`Contribution`] has a slot that reaches it, so no arm can | ||
| /// move it however it is wired. | ||
| pub profile: LoopProfile, |
There was a problem hiding this comment.
Preserve the preset when decoding legacy states
When a serialized accumulator predates the profile field, the container-level #[serde(default)] initializes this field with LoopProfile::default(), which always selects Preset::Balanced; the old accumulator contains no information that can recover another preset. Consequently, a legacy Persistent, Exploratory, or Cautious checkpoint that reaches this decoder silently resumes with Balanced thresholds and may route differently. The migration path must inject the active preset's starting profile or explicitly reject such legacy state rather than defaulting it.
AGENTS.md reference: AGENTS.md:L28-L29
Useful? React with 👍 / 👎.
| Change::MuteArm { arm } | Change::UnmuteArm { arm } => { | ||
| if self.mutable_arms.contains(arm) { | ||
| Ok(()) |
There was a problem hiding this comment.
Enforce the muting window outside the tuner
With a custom or model-backed Tuner, a MuteArm proposed on the first pass is accepted solely because the arm is listed in mutable_arms; neither the amendment's stamped pass nor any silence evidence is checked against muting_window. Although Rules now waits for its configured window, this remaining path lets swapping proposers widen the behavior that Bounds is supposed to enforce, so the window must also be validated at the fold boundary.
AGENTS.md reference: AGENTS.md:L28-L29
Useful? React with 👍 / 👎.
There was a problem hiding this comment.
Requesting changes: 1 lane(s) blocking, worst finding is high.
Fix or reply to the findings below and push. The next review clears this automatically once they are gone — you should not need to dismiss anything by hand.
$0.0675 · 398,642 in / 24,175 out · 219,066 cached (55%) · openrouter/openai/text-embedding-3-small, deepseek/deepseek-v4-flash, z-ai/glm-5.2 · 774 embedded
critique: $0.0156 · 83,941 in / 7,594 out · 28,574 cached (34%) · deepseek/deepseek-v4-flash, z-ai/glm-5.2
security: $0.0078 · 83,481 in / 1,348 out · 12,872 cached (15%) · deepseek/deepseek-v4-flash, z-ai/glm-5.2
tests: $0.0263 · 119,967 in / 11,703 out · 93,891 cached (78%) · z-ai/glm-5.2
description: $0.0177 · 111,253 in / 3,530 out · 83,729 cached (75%) · z-ai/glm-5.2
| max_tool_calls: 600, | ||
| max_tokens: 2_000_000, | ||
| run_timeout: Duration::from_secs(30 * 60), | ||
| run_timeout: Duration::from_mins(30), |
There was a problem hiding this comment.
Replace Duration::from_mins with Duration::from_secs
Duration::from_mins does not exist in std::time::Duration. The previous code used Duration::from_secs(30 * 60). The standard library offers from_secs, from_millis, from_micros, and from_nanos, but no from_mins. This change will fail to compile.
| run_timeout: Duration::from_mins(30), | |
| run_timeout: Duration::from_secs(30 * 60), |
[RULE] unavailable-api ·
| /// Two assemblies of the same preset produce the same signature, and a | ||
| /// changed threshold produces a different one, which is what makes an | ||
| /// incompatible resume an error rather than silent corruption. | ||
| /// Two assemblies of the same preset produce the same signature, and so do |
There was a problem hiding this comment.
Fix signature doc: different presets produce different signatures
The doc comment on AssembledLoop::signature says "two assemblies of different presets [produce the same signature]," but the test a_different_preset_is_a_different_run_but_not_a_different_ladder asserts assert_ne!(GraphSignature::of(&balanced), GraphSignature::of(&persistent)). ADR 0006 confirms the test is correct: the graph carries the starting profile in its seed accumulator, so two presets emit two different graphs. What does not move the hash is a run revising its own thresholds mid-run — the revision lands in the accumulator, not in the topology. The comment conflates these two cases.
[RULE] doc-contradicts-test ·
Summary
A run can now revise its own configuration mid-run — its routing thresholds, its
spend, and which evaluation arms it is still paying for — within room its preset
declares and cannot widen. Every revision and every refusal is recorded, evented,
and rendered into the report.
Landed in two parts, and the first is worth reading on its own:
Thresholds moved out of the emitted graph and into the run's accumulator.
They used to be rendered into the routing switch's jq, the loop head's
until,and the head's
max_iterations.GraphSignature::ofhashes each node's configwhole, so a threshold change was a topology change and
verify_resumerefusedit — a run that retuned itself at pass 3 could not survive a crash at pass 4.
Both programs now address
.profile.thresholdsin the accumulator, so oneladder routes every preset and a revision leaves the graph untouched.
A
Tunerseam, bounded amendments, and the shipped rule tuner sit on top ofthat. One arm may propose;
Preset::boundsdecides what may be proposed; thepassstep folds it, so an amendment never takes effect in the pass thatproposed it.
Specs:
docs/specs/adaptation.md, withADR 0006 amending ADR
0004's "rendered from the Rust constant" bullet and
loop-kernel.mdinvariant 7.Plans:
docs/plans/adaptation.mdanddocs/plans/adaptation-tuning.md.Related issue
None.
API or behavior changes
Breaking (pre-1.0, so a minor bump):
route,is_terminal,Outcome::classify, andOutcome::successtake&LoopStatealone and readstate.profile.thresholds. A caller can no longerhand the router a threshold set the run is not using.
ladder()andterminal_condition()take no arguments and emit one constantprogram.
evaluate_ladder/evaluate_terminal_conditionlose their&Thresholdsparameter, as doTerminationCondition::{holds, evaluate, expression}.LoopBuilder::new(arms, registry)no longer takesThresholds; the profileand caps arrive through
.profile(..)/.caps(..).run_loop_stepandStepRegistry::run{,_with}read the thresholds off thedecoded state instead of taking them.
AssembledLoop::thresholds()is replaced byprofile().Advanceis a struct with bounds:Arc::new(Advance)becomesArc::new(Advance::default()).Unrelated pre-existing fix riding along — flagged rather than buried:
[workspace.package] rust-versionraised 1.88 → 1.95, which raises thepublished MSRV for downstream consumers. The CI
msrvjob builds--all-featuresat the declared version, andvendor/tinyagentsuses anif letguard (harness/providers/openai/transport.rs:2108), stable onlyfrom 1.95. Reproduced locally: 1.88 and 1.94 both reject it.
This break pre-dates this PR — the last three CI runs on
mainfailed thesame way, and this branch does not touch
vendor/. It is fixed here becausethe
msrvjob also runs on this PR, so the PR cannot go green without it.Behavior:
Caps::max_iterationsdefault raised 8 → 12. It was belowPreset::Persistent'smax_attemptsof 12, so a persistent run was truncatedfour attempts short of the ceiling it declares and nothing said so. The
relationship is now asserted rather than the number.
AssembledLoop::driveruns the registeredpassstep instead of inlining partof it. It was setting the pass counter by hand and never clearing the steer, so
the in-process driver and the graph had already diverged by one field.
carries the starting profile, beside the goal, so they are different runs.
What no longer differs is the routing.
Additive:
LoopProfile,Amendment,Change,ThresholdField,CapField,Bounds,Range,Verdict,Recorded,Muted,Tuner,TunerArm,Rules,Preset::bounds,LoopState::with_profile/proposed,Driven::profile,Event::Amended,Event::AmendmentRefused,tuned_research_loop.Validation
Commands actually run, with their outcome — all pass:
cargo fmt --all -- --checkcargo clippy --all-targets --all-features -- -D warningscargo build --all-targets --all-featurescargo test --all-features— 474 lib, 37 integration, 61 doc;cargo teston default features also green
RUSTDOCFLAGS="-D warnings" cargo doc --no-deps --all-features.github/scripts/check-file-coverage.sh 90 coverage.json— exit 0; the newfiles are at 100% (
amendment.rs,bounds.rs,profile.rs) and 98.8%(
tuner.rs)cargo run -p tinyloops --example basic | simple_loop | research_loopTests
Three existing tests asserted the behavior being removed and were rewritten,
not deleted — the guard is still wanted, against a different failure:
the_ladder_interpolates_thresholds_rather_than_hard_coding_them→..._addresses_thresholds_rather_than_rendering_them(and its terminal twin)changing_a_threshold_changes_the_signature→the_routing_programs_are_the_same_under_every_presetno_threshold_is_typed_into_the_builder→no_threshold_reaches_the_builder_at_allNew coverage of note:
revising_a_threshold_leaves_the_graph_untouched— the property the wholechange exists to buy, asserted directly rather than implied.
a_different_preset_is_a_different_run_but_not_a_different_ladder— pins theone place the change stops short of the slogan, rather than leaving a reader to
discover it.
the 32 corners of
{0,3}^5— a real widening over the four preset tuples ittested before. The two harnesses used different threshold sets and are now
unified. See the deviation note below.
an_amendment_does_not_change_the_route_of_the_pass_that_proposed_it,a_refused_amendment_leaves_the_profile_byte_identical,a_run_at_its_amendment_budget_refuses_the_next_and_carries_on,a_muted_arm_still_runs_its_node_and_still_converges,narrowing_only_ever_tightens,the_rule_tuner_proposes_on_exactly_these_passes.compile_faildoctests pinned toE0616prove animpl Armcannotpropose an amendment. This follows the existing
Observerpair rather thanadding a
trybuilddependency.Deliberately not tested to the letter of the spec: the spec asked the parity
sweep to cross the counter space with "the declared threshold space". Measured,
that is ~6×10⁶ jq compiles and would dominate the suite. It sweeps a declared box
instead, chosen to contain the boundaries where an operator bug (
>for>=)shows. The test's own doc comment and the spec now say that in those words rather
than claiming an exhaustiveness the suite does not deliver.
A gap found and closed while here: the
every_event()fixture was missingEvent::NoteDropped, so that variant's wire form was never pinned even though itis rendered and emitted. Added, with a count assertion so the next omission fails
loudly.
Documentation
docs/adr/0006-thresholds-addressed-from-run-state.md(new) — amends ADR 0004,which is immutable.
docs/specs/adaptation.md(new, Implemented) anddocs/specs/loop-kernel.mdinvariant 7 amended.
docs/plans/adaptation.mdanddocs/plans/adaptation-tuning.md, both with a"deviations this plan made, deliberately" section recording where the code
decided differently from the plan and why.
crates/tinyloops/src/presets/README.md(new) — the tuner's rules, eachpreset's bounds, and the operational constraints.
README.mdandAGENTS.mdupdated in the same change.Checklist
#[allow(...)],#[ignore], or relaxed lints.envcontents in the diff or the descriptionSummary by CodeRabbit
New Features
Improvements
Documentation