Skip to content

Let a run revise its own configuration, within bounds - #1

Merged
senamakel merged 141 commits into
mainfrom
loop-adaptation
Aug 29, 2026
Merged

Let a run revise its own configuration, within bounds#1
senamakel merged 141 commits into
mainfrom
loop-adaptation

Conversation

@senamakel

@senamakel senamakel commented Aug 29, 2026

Copy link
Copy Markdown
Member

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::of hashes each node's config
whole, so a threshold change was a topology change and verify_resume refused
it — a run that retuned itself at pass 3 could not survive a crash at pass 4.
Both programs now address .profile.thresholds in the accumulator, so one
ladder routes every preset and a revision leaves the graph untouched.

A Tuner seam, bounded amendments, and the shipped rule tuner sit on top of
that. One arm may propose; Preset::bounds decides what may be proposed; the
pass step folds it, so an amendment never takes effect in the pass that
proposed it.

Specs: docs/specs/adaptation.md, with
ADR 0006 amending ADR
0004's "rendered from the Rust constant" bullet and loop-kernel.md invariant 7.
Plans: docs/plans/adaptation.md and
docs/plans/adaptation-tuning.md.

Related issue

None.

API or behavior changes

Breaking (pre-1.0, so a minor bump):

  • route, is_terminal, Outcome::classify, and Outcome::success take
    &LoopState alone and read state.profile.thresholds. A caller can no longer
    hand the router a threshold set the run is not using.
  • ladder() and terminal_condition() take no arguments and emit one constant
    program. evaluate_ladder / evaluate_terminal_condition lose their
    &Thresholds parameter, as do TerminationCondition::{holds, evaluate, expression}.
  • LoopBuilder::new(arms, registry) no longer takes Thresholds; the profile
    and caps arrive through .profile(..) / .caps(..).
  • run_loop_step and StepRegistry::run{,_with} read the thresholds off the
    decoded state instead of taking them.
  • AssembledLoop::thresholds() is replaced by profile().
  • Advance is a struct with bounds: Arc::new(Advance) becomes
    Arc::new(Advance::default()).

Unrelated pre-existing fix riding along — flagged rather than buried:

  • [workspace.package] rust-version raised 1.88 → 1.95, which raises the
    published MSRV for downstream consumers. The CI msrv job builds
    --all-features at the declared version, and vendor/tinyagents uses an
    if let guard (harness/providers/openai/transport.rs:2108), stable only
    from 1.95. Reproduced locally: 1.88 and 1.94 both reject it.
    This break pre-dates this PR — the last three CI runs on main failed the
    same way, and this branch does not touch vendor/. It is fixed here because
    the msrv job also runs on this PR, so the PR cannot go green without it.

Behavior:

  • Caps::max_iterations default raised 8 → 12. It was below
    Preset::Persistent's max_attempts of 12, so a persistent run was truncated
    four attempts short of the ceiling it declares and nothing said so. The
    relationship is now asserted rather than the number.
  • AssembledLoop::drive runs the registered pass step instead of inlining part
    of 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.
  • Two graphs built from different presets still differ — the seed accumulator
    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 -- --check
  • cargo clippy --all-targets --all-features -- -D warnings
  • cargo build --all-targets --all-features
  • cargo test --all-features — 474 lib, 37 integration, 61 doc; cargo test
    on 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 new
    files are at 100% (amendment.rs, bounds.rs, profile.rs) and 98.8%
    (tuner.rs)
  • cargo run -p tinyloops --example basic | simple_loop | research_loop

Tests

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_preset
  • no_threshold_is_typed_into_the_builderno_threshold_reaches_the_builder_at_all

New coverage of note:

  • revising_a_threshold_leaves_the_graph_untouched — the property the whole
    change exists to buy, asserted directly rather than implied.
  • a_different_preset_is_a_different_run_but_not_a_different_ladder — pins the
    one place the change stops short of the slogan, rather than leaving a reader to
    discover it.
  • The parity harness now sweeps the counter space against every preset and
    the 32 corners of {0,3}^5 — a real widening over the four preset tuples it
    tested 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.
  • Two compile_fail doctests pinned to E0616 prove an impl Arm cannot
    propose an amendment. This follows the existing Observer pair rather than
    adding a trybuild dependency.

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 missing
Event::NoteDropped, so that variant's wire form was never pinned even though it
is 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) and docs/specs/loop-kernel.md
    invariant 7 amended.
  • docs/plans/adaptation.md and docs/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, each
    preset's bounds, and the operational constraints.
  • README.md and AGENTS.md updated in the same change.

Checklist

  • The change is focused on one logical change
  • No new #[allow(...)], #[ignore], or relaxed lints
  • No secrets, tokens, or .env contents in the diff or the description

Summary by CodeRabbit

  • New Features

    • Added bounded, mid-run configuration tuning with profile revisions, amendment history, arm muting, and configurable limits.
    • Added a rule-based tuner and a tuned research-loop example.
    • Added amendment and refusal events to run output and final reports.
    • Expanded public exports for tuning, profiles, bounds, and policy types.
  • Improvements

    • Routing and termination now use the run’s profile, keeping graph structure stable across threshold changes.
    • Increased the default iteration safeguard from 8 to 12.
  • Documentation

    • Added adaptation specifications, architectural guidance, planning notes, and tuned-loop documentation.

senamakel and others added 30 commits August 29, 2026 20:38
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>
senamakel and others added 15 commits August 29, 2026 22:40
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>

@chatgpt-codex-connector chatgpt-codex-connector Bot left a comment

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

💡 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".

Comment on lines +326 to +327
let budget = self.budget.narrow(state.profile.caps)?;
if let Some(tripped) = budget.tripped(&meter) {

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

P1 Badge 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,

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

P2 Badge 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 👍 / 👎.

Comment on lines +223 to +224
if let Some(amendment) = state.proposed.take() {
state.profile.fold(amendment, &self.bounds);

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

P2 Badge 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 👍 / 👎.

senamakel and others added 3 commits August 29, 2026 23:05
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>

@tinysweeper tinysweeper Bot left a comment

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

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()

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

priority medium critique likely

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);

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

priority medium critique likely

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 {

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

priority medium critique likely

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 ·

senamakel and others added 3 commits August 29, 2026 23:07
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>

@chatgpt-codex-connector chatgpt-codex-connector Bot left a comment

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

💡 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,

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

P2 Badge 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 👍 / 👎.

Comment on lines +188 to +190
Change::MuteArm { arm } | Change::UnmuteArm { arm } => {
if self.mutable_arms.contains(arm) {
Ok(())

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

P2 Badge 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 👍 / 👎.

@tinysweeper tinysweeper Bot left a comment

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

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),

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

priority high security confident

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.

Suggested change
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

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

priority medium tests confident

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 ·

@senamakel
senamakel merged commit b313d4a into main Aug 29, 2026
14 of 15 checks passed
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Projects

None yet

Development

Successfully merging this pull request may close these issues.

1 participant