Skip to content

Observatory: provenance, rich rendering, evolution replay, descriptor atlas, live view - #197

Closed
lfnothias wants to merge 37 commits into
mimosa_v2from
mimosa_v2_lfx
Closed

Observatory: provenance, rich rendering, evolution replay, descriptor atlas, live view#197
lfnothias wants to merge 37 commits into
mimosa_v2from
mimosa_v2_lfx

Conversation

@lfnothias

@lfnothias lfnothias commented Aug 23, 2026

Copy link
Copy Markdown
Collaborator

Turns the Observatory into an examination instrument for self-evolving runs: provenance from the structured record, rich rendering of run content, an animated evolution replay, a descriptor-space atlas, and a live activity feed.

Provenance tab. A new GET /api/runs/{id}/provenance endpoint serves (a) the transparency exporter's ASTRA capsule for the run — decisions with their alternatives, rationale, and realised universes; a run without a capsule of its own links to the family members that have one — and (b) every asb_eval evaluation capsule (eval_astra.yaml) that names the run: executor verdicts over the ASB card's own criteria, workspace provenance flags, and the pinned-instrument judge layer. ProvenancePanel renders decision rows (chosen option highlighted, alternatives dimmed with exclusion reasons), evaluation cards with the score-over-decided headline, and the judge block kept visually separate from the executor score. Capsule and evaluation roots are overridable via MIMOSA_CAPSULE_DIR / MIMOSA_EVAL_DIR.

Rich rendering. A shared render/ module (syntax highlighting via shiki, collapsible JSON tree, GFM markdown, CSV preview, ANSI stripping) replaces raw <pre> dumps in the memory replay inspector, workspace previews, and run artifacts; every view keeps a raw toggle.

Evolution replay. GET /api/runs/{id}/evolution joins the lineage tree with per-node metrics, claim verdicts, costs, the selection log, and the textual gradient. The new Evolution tab animates the family generation by generation — play/step/scrub controls, ancestry highlighting, best-so-far marker, and a narration panel showing the score delta against the parent and the gradient that preceded each mutation.

Descriptor atlas. GET /api/atlas/{space} projects run descriptors by PCA, with lineage edges and per-family trajectory replay. Two spaces are exposed: the QD behaviour descriptor, and a TF-IDF genotype space over each run's evolved workflow code. The second exists because the QD descriptor proved byte-identical within a family (it embeds the task, not the evolved workflow), so within-family trajectories are degenerate in that space by construction; the UI states the limitation and offers the switch rather than masking it.

Live view. A filesystem watcher over the workflow, memory, capsule, and evaluation roots streams semantic events over WS /api/live (iteration completed, gradient written, ASTRA capsule updated, evaluation updated, …). Running runs get a live feed that diffs the ASTRA decision layer on each capsule write and auto-refreshes the workspace and provenance panels. Granularity is per-iteration, matching when the exporter writes; mid-iteration decision streaming would need a hook in astra_exporter and is left out of scope here.

Verified against live runs: the capsule decisions and asb_eval verdicts of 20260822_183340_88cdd1fe render end to end; the evolution replay and both atlas spaces were exercised on the run-7 families (113 runs in genotype space); live events were confirmed by touching real artifacts and observing the corresponding feed lines. Backend tests 52 → 67; tsc, oxlint, and vite build clean.

lfnothias and others added 30 commits June 28, 2026 12:57
- factory.py: replace infinite MCP discovery loop with 5-retry exponential backoff
- orchestrator.py: classify sandbox errors as TIMEOUT/SYNTAX_ERROR/RUNTIME_ERROR
- workflow_factory.py: validate Python keyword identifiers, atomic state_result.json write, post-assemble compile check
- workflow_runner.py: delete temp exec script in finally block
- evolution_engine.py: ghost WorkflowInfo guard (generation_failed uuid); ASTRA export returns bool + .export_status sidecar; clear VariationEngine score/gradient/agent-count histories on session start
- selection.py: refresh member metrics before eviction check (not after)
- planner.py: verify expected outputs of completed dependencies before marking step executable
- smolagent_factory.py: file lock on save_memories; explicit TimeoutError re-raise before generic handler; restore exception capture in _run_agent retry loop; max_steps=35 + context-window guard callback
- astra_exporter.py: fail-fast on empty trace
- llm_provider.py: strip temperature entirely for Opus 4.x (invalid_request_error); broaden temperature error detection to message string
- variation_engine.py: cap variation temperature at 1.0 for Claude models
- csv_mode.py: parse SUCCESS_LEVEL from LLM response; pass scenario_rubric filename; resolve runs_capsule_dir to absolute path
- config.py: add max_steps, max_context_tokens, export_astra fields
- workflow_v11.md: add MCP response schema guidance, absolute-path rule, env-setup via shell tool, no built-in file I/O rule

Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
The retry loop in LLMProvider.__call__ was `while True`, and the
`self.max_retries` field (set in __init__) was never read. A persistently
overloaded or rate-limited provider therefore retried indefinitely,
re-sending the full prompt on every attempt — unbounded latency and, on
providers that bill partial/streamed output, unbounded token spend.

Both retry paths (timeout and generic retryable errors) now raise once
`attempt >= max_retries` instead of looping forever. The context-window
shrink path keeps its existing independent 3-step cap. Default max_retries
raised 3 -> 6 so exponential backoff (capped at 500s) still rides out
transient blips while guaranteeing termination.

No behaviour change on the success path or for non-retryable errors.

Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
The repo ships a tests/ suite using the `*_test.py` pytest convention, but
pytest was not declared anywhere, so `uv sync` produced an env that could not
run the tests (No module named pytest). This adds a PEP 735 dev group so the
suite is runnable with:

    uv sync --dev
    uv run pytest tests/

No runtime dependency change (dev group only); uv.lock is gitignored in this
repo, so it is intentionally not committed.

Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
The verifier is the dominant LLM cost: a single evaluation fires 50-100+
judge calls (claim extraction across sources, dedup, importance rating,
per-claim file selection, verifier-script generation, package checks), and
every one ran on judge_model. Most of those are mechanical structured-JSON
extraction, not calibrated judgement — they do not need the strong/expensive
judge.

This adds an optional `judge_extraction_model` config field. When unset
(default None) it falls back to judge_model, so behaviour is unchanged. When
set, the six mechanical call sites route through it via a new
`use_extraction_model` flag on `_call_judge` / `_call_judge_for_json`:
  - claim extraction (per source)        verifier_claims._extract_claims
  - near-duplicate dedup                 verifier_claims._run_dedup_pass
  - importance rating (per batch)        verifier_claims._rate_one_importance_batch
  - per-claim file selection             verifier_per_claim._llm_select_files
  - verifier-script generation           verifier_per_claim._call_and_parse_verifier
  - missing-package check                verifier_per_claim._llm_packages_needed_for_claim

The two calls that are genuine judgement stay on judge_model: the per-claim
soft verdict (_score_soft) and the mutation textual-gradient
(_build_abstractec_textual_gradient).

Expected saving: with judge_extraction_model pointed at a cheap tier, the
large majority of judge calls per evaluation (and per evolution iteration)
drop from the strong model to the cheap one, while final scoring quality is
preserved. Judge-LLM construction is centralised in _build_judge_llm_config so
both tiers are configured identically (temperature, routing). The importance
test's judge monkeypatch gains **_kwargs to match the new signature.

Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
Adds a `model_tiers` map ({"heavy": ..., "light": ...}) so any *_model role
(planner, workflow, smolagent, judge, judge_extraction, capsule_namer) may be
set to a tier alias instead of a concrete id. Aliases are resolved to the
tier's model at load time via Config.resolve_model, so the whole fleet's cost
profile can be switched by editing two lines, e.g.:

    "model_tiers":      {"heavy": "openrouter/z-ai/glm-5.2",
                         "light": "openrouter/minimax/minimax-m3"},
    "planner_llm_model":      "heavy",
    "judge_model":            "heavy",
    "judge_extraction_model": "light",
    "smolagent_model_id":     "light"

Purely additive and backwards compatible: a concrete model id (or None) is
returned unchanged, and the default role values are concrete ids, so the tiers
are inert until a role opts in. This pairs with the judge_extraction_model tier
(extraction -> "light") and the OpenRouter caching/routing already in place.

Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
Agent and verifier both grounded via Perspicacite with a hardcoded
kb_name=None (web search). For a fair benchmark these must differ: the
verifier may use full ground truth, but the agent must use only a leak-free
tier — and unrestricted web search can surface the source paper, leaking
answers even with the browser MCP disabled.

This threads kb_name through query_perspicacite (and the streaming/non-
streaming transports + the on-disk cache key, which now separates per-KB
entries while keeping the existing kb_name=None keys valid), and adds three
config fields:
  - perspicacite_agent_grounding_enabled (default True) — agent-side opt-out
  - perspicacite_agent_kb_name   (default None) — KB the agent grounds from
  - perspicacite_verifier_kb_name(default None) — KB the verifier grounds from

Agent callers (orchestrator + planner) honour the enable flag and pass the
agent KB; the verifier passes its own KB to get_perspicacite_grounding. All
defaults reproduce the current behaviour, so this is opt-in. It enables the
grounding ablation: agent = none / leak-free brief KB, verifier = ground-truth
KB.

Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
…ing)

The verifier graded every extracted claim (default cap 100) with grounding
always on; on a real run this expanded to ~80 per-claim gen/exec/score loops
and dominated wall-clock and OpenRouter spend. Both were constructor-only and
not reachable from config.

Adds two optional config fields, plumbed at the VerifierEvaluator construction
site:
  - verifier_max_claims    (default None -> verifier's own default of 100)
  - verifier_use_grounding (default None -> verifier's own default of True)

When unset they are omitted, so the verifier keeps its library defaults
(zero behaviour change). Lowering verifier_max_claims grades only the top-N
most important claims for a faster/cheaper run, trading coverage of
low-priority claims for speed.

Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
A fair run revealed the agent could not execute a data-analysis reproduction:
the smolagent sandbox authorized pandas/numpy but not sklearn or scipy (and
subprocess is blocked), so it could not train a RandomForest, run PCA/PCoA, or
compute distance metrics in-sandbox — it fell back without producing the
deliverables.

Adds scipy + scikit-learn (and common submodules) to additional_authorized_
imports in both agent factories, and scikit-learn/scipy to runner_requirements
(default config + the fair eval config) so they are installed in the runner
venv. This lets the agent perform ML/stats reproduction in Python without
shelling out, which is the general capability such tasks need.

General (any data-science reproduction benefits), not task-specific. These are
standard data libraries with no system-access surface beyond what is already
allowed.

Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
The 17 variation_engine_test failures were stale, not real regressions:
- VariationEngine() was called with no args, but the constructor gained a
  required `config` (it builds the mutation LLM config). Pass a real Config().
- Two assertions checked for old scope-label keywords ("EXPLOITATION",
  "RE-SPECIATION") that the mutation prompt no longer emits; the surrounding
  logic assertions (boldness, respeciation_gate_open) still pass, so only the
  display strings drifted — updated to the current labels ("Slight mutation",
  "Bolder mutation").
- test_mutation_prompt_does_not_touch_gradient_history made a live LLM call as
  a side effect; it verifies gradient-history invariance, not LLM behaviour, so
  the directive call is now stubbed (no network).

Full suite: 109 passed, 4 skipped, 0 failed (was 92 passed / 17 failed).
Test-only change; no production code touched.

Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
…fier knobs

Adds unit coverage for this session's cost/fairness config features (pure
config behaviour, no network/LLM):
- model_tiers + resolve_model: alias -> model, concrete passthrough, None
- defaults are concrete ids so tiers stay inert (zero behaviour change);
  judge_extraction_model defaults None (reuse judge_model)
- from_json resolves tier aliases on every model role
- per-side Perspicacite KB + verifier cost knobs: defaults reproduce current
  behaviour, and dump/load round-trips them

Locks in the opt-in/zero-default-change contract these features were built to
preserve. Suite: 114 passed.

Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
Documents the config-driven cost levers (model tiers, cheap judge-extraction
tier, prompt caching, verifier_max_claims/use_grounding, bounded retry) and the
leak-free fairness controls (per-side Perspicacite KB + agent grounding
opt-out, leak linting, internet isolation, the sandbox scientific-import
capability) added this cycle, plus the fair-run recipe. Emphasises that every
knob is opt-in with behaviour-preserving defaults. Linked into the Evaluation
nav.

Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
…g is off

make_scientific_grounded_prompt printed the "🔬 Querying Perspicacite-AI…"
phase banner (and the retrieved-context line) before calling
perspicacite_grounding(), which already returns a no-context sentinel when
agent-side grounding is disabled. So a fair run with
perspicacite_agent_grounding_enabled=false logged a query it never made.

Guard both prints behind the enabled flag; the grounding call (and its
internal gate) are unchanged. Logging-only.

Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
Regression guard for feat(sandbox): a fair run proved the agent cannot execute
a data-analysis reproduction (RandomForest, PCoA) without scipy/sklearn. These
tests assert both agent factories keep sklearn/scipy in the sandbox's
authorized imports and that the runner requirements install scikit-learn/scipy
(plus pandas/numpy), so the capability cannot be silently dropped.

Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
…ervable

Found while running Mimosa against ASB capsules on OpenRouter's stealth tier.

Temperature. workflow_factory samples random.uniform(0.7, 1.3) for every
workflow generation, clamping to 1.0 only for Anthropic. Backends that cap at
1.0 refuse the rest. The retry path already knew how to recover — drop to 1.0,
retry — but _is_temperature_error only read error.param, which OpenAI-style
APIs set and gateways do not: OpenRouter forwards the refusal as a bare 400
whose metadata.raw is the string "ERROR". So the recovery never fired and the
task aborted instead. Measured against stealth/ox-alpha: temperature 1.0 and
0.7 succeed, 1.3 returns that 400 — roughly half of all generations died.
Detection now also treats a 400 on a request above the 1.0 ceiling as a
temperature rejection; a 400 at or below it is left alone so real bad-requests
are not masked.

Grounding. Every failure path in query_perspicacite returns None and each
caller substitutes a "no relevant scientific context" string, so a run where
grounding failed outright produces the same artifacts as a grounded one.
Added a per-attempt ledger and grounding_stats(), folded into run_metrics.json,
so a run can be shown to have been grounded rather than assumed to have been.

Also:
- perspicacite_kb_name / _mode / _max_papers config knobs. kb_name was
  hardcoded None, so retrieval always took the web-search pipeline and the
  local knowledge bases were unreachable. Default stays None — behaviour
  unchanged unless set. For benchmarks, never scope it to a KB built from the
  paper under reproduction; that hands the agent the graded values.
- Persist reasoning_effort only when it was actually sent. It is gated to the
  o1/o3/gpt-5 families, but the configured value was recorded for every model,
  putting a parameter in the run's provenance the provider never received.
- Precheck now names save_logprobs when it is on and every probe failed. The
  probe pairs logprobs with require_parameters, so an endpoint that simply
  does not advertise logprobs fails with a routing-shaped 404 and the generic
  advice sent operators to the provider allowlist instead.
- Drop _OVERALL_TIMEOUT: defined, documented, never passed to httpx.

Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
The flag was read in one place — orchestrator._ground_with_perspicacite.
planner.py and the judge's grounding evaluator (via verifier.py and
generic.py) call query_perspicacite directly, so turning it off still left two
of the four call sites querying the literature. The recorded workaround was to
point PERSPICACITE_API_URL at an unreachable host, which is also what the
fair-eval notes in the dev hub prescribe for benchmark runs.

Gating in the client instead covers every caller from one place. Skipped calls
are recorded as "disabled" and excluded from the hit-rate denominator, so a
deliberately ungrounded run is not reported as a run whose grounding failed.

Default stays enabled; behaviour is unchanged unless the flag is set.

Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
Observed running ASB capsules against stealth/ox-alpha: the planner failed all
three attempts with "Failed to extract valid JSON from LLM response" / "Invalid
control character at …" and the task was abandoned. The model had pasted a
multi-line span straight into a string value.

_extract_json_from_code_block called bare json.loads on the fenced block, so an
otherwise complete plan was discarded over an unescaped newline. The repair
ladder that already exists in onboard_cli.py never covered this path.

Adds sources/utils/llm_json.loads_llm_json, ported from ASB's llm_pipeline
helper, which solved the same problem against the same model family. It repairs
raw control characters, bare interior quotes, and trailing prose after a
complete object — but only after a strict parse fails, so valid JSON is
returned byte-for-byte unchanged, and it re-raises the original JSONDecodeError
when nothing parses so callers still see the true defect.

The per-claim verifier and workflow_info parse LLM JSON the same way and are
candidates for the same treatment; left alone here because neither was observed
failing.

Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
run_cached runs the agent on a worker thread, joins with a deadline, and treats
completed=False as a timeout. The retry loop caught every exception but the two
lines recording it were commented out:

    #result['exception'] = e
    #result['completed'] = True

So an agent whose three attempts all raised left completed=False and
exception=None. The thread exited, join() returned immediately, and the caller
raised

    TimeoutError: Agent 'reconstructor' execution timed out after 18000 seconds

about two minutes into the run, with the real exception discarded — visible
only as a bare print() inside the worker. Every agent-level crash was
misreported as a five-hour timeout.

The discriminator is whether the worker is still alive after the join: alive
past the deadline is a genuine timeout; finished without completing means every
retry raised, so re-raise the last one. A successful attempt clears any earlier
retry's exception, since the caller re-raises whatever is left in the slot.

Observed against stealth/ox-alpha running ASB capsules, where the underlying
cause was the temperature-400 fixed in 6499f03 — invisible behind the timeout.

Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
…nstall

Exit code. run_single_thread_eval_loop catches per-row exceptions and continues
— right for a batch — recording success_level: "Error" in execution_history.
Nothing read it, so a run whose every row failed exited 0. Observed: a task
logging "Error in csv row 1: Planner: Execution failed" twice, exit 0. Any
harness scoring by exit code counts that as a pass. papers_mode now reports the
count and exits 1.

Test isolation. Writing the test above surfaced a worse problem. pyproject
force-includes main.py and config.py as top-level modules, so installing the
project drops copies into site-packages, and under pytest those copies won
every import:

    import config  ->  .venv/lib/python3.11/site-packages/config.py  (Aug 14)

sources/ is not shipped that way and correctly resolved to the tree, which is
why only main/config were affected — and why it went unnoticed. The effect is
that config_roundtrip_test.py, whose whole purpose is to check which fields
survive dump/load, was checking an install-time snapshot: fields added in the
working tree were invisible to it and it passed regardless.

conftest.py now puts the repo root at the front of sys.path. config_roundtrip
consequently exercises real code for the first time in a while, and still
passes, including over the perspicacite_* fields added in 6499f03.

Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
lane1_t0 (p_iimn) ran with the control-character repair already in place and
still lost its plan after three attempts:

    Expecting property name enclosed in double quotes: line 47 column 64

That message reads like a quoting fault; it is a trailing comma — the one JSON
forbids and both JavaScript and Python allow. Six occurrences in that one run.

strip_trailing_commas drops a comma followed only by } or ], tracking string
state so commas inside values survive. The ladder now tries strict, then
string-repair, then comma-strip, then both — a real response can carry a pasted
multi-line span and a trailing comma at once.

Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
_extract_json_from_code_block scanned only for ```json fences and returned None
otherwise, which the caller turns into "Failed to extract valid JSON from LLM
response" and, after three attempts, abandons the task.

A model told to answer in JSON frequently just answers in JSON. Verified
against the persisted plan_creator response from the failed lane1_t1 run: a
valid 7558-character plan object, two top-level keys, three steps, no fence —
discarded. Same failure on lane2_t0. That made it the most common terminal
failure in the sample, ahead of anything model-quality related.

Now tries the fenced block first (unchanged when present), then the bare
response, then from the first brace so a leading "Here is the plan:" does not
cost the plan either. Still returns None when there is genuinely no JSON.

Re-running the extractor over that saved response now recovers the plan.

Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
Same behaviour, spelled out as the three cases it actually handles: opening
quote, closing quote, unescaped interior quote.

Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
A response cut off at max_tokens arrives with finish_reason="length" and no
exception, and the caller parses it as if it were complete. Reasoning models
make this routine: the budget is spent on reasoning before any content is
emitted, so a budget that would comfortably fit the answer still returns a
fragment.

Observed on the p_iimn re-run against stealth/ox-alpha, with every JSON repair
already in place: the planner call returned Completion: 8192 against a
max_tokens of exactly 8192, four truncation warnings in one run, and JSON that
ended mid-string. The parser failed six times with "Unterminated string" and
the task was abandoned. No repair recovers a document that was never finished.

The budget now doubles and retries, at most twice and never past 65536. The
config value is not mutated, so escalation is per-call. When it is still
truncated after the last escalation the warning says so explicitly instead of
suggesting the operator "consider increasing max_tokens" — by then that advice
has already been taken automatically.

This is the same shape as the existing context-window handler, which halves an
oversized prompt and retries; that one guards the input side, this one the
output side.

Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
Once the temperature fallback landed, every temp>1.0 generation recovered:
the log shows 1.20, 1.10, 1.24, 1.08 each falling back to 1.0 and then
"generated in Ns". The generation failures that remained were all at
temperature <= 1.0 — a different cause.

That cause is OpenRouter reporting an upstream fault as HTTP 400 with
message "Provider returned error" and metadata.raw "ERROR". The 400 makes it
look like a malformed request. It is not: six identical calls at temperature
0.85 with a full-size workflow prompt succeeded in isolation while the same
shape was failing intermittently under four concurrent lanes. Semantically it
is a 502.

_is_retryable_error did not match that wording, so it raised immediately with
no backoff and each occurrence cost a whole workflow generation — the single
largest remaining source of lost work in the sample.

The match is deliberately narrow, on the gateway's own phrasing rather than on
400s in general, so a genuinely malformed request is not retried in a loop; it
would keep failing and still exhaust the existing retry ceiling.

Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
The step-execution handler wrapped the cause as

    raise Exception(f"❌ Critical error in step execution: {str(e)}") from e

`from e` preserves the chain for a Python caller, but the operator only ever
sees the formatted string. A real run ended with

    ❌ Critical error in step execution: unhashable type: 'slice'

— no file, no line, no exception type, and nothing in the log to grep for. The
run had already produced its 13.9 kB deliverable and written its ASTRA capsule,
so the bare message made a late failure look like a total loss.

Now logs the traceback and names the exception type, keeping `from e`.

Planner had no logger at all — `self.logger` appeared nowhere in the file — so
logging the traceback needed one added to __init__ first. Without that the fix
would have raised AttributeError from inside an except block, which is a worse
failure than the one it was meant to explain.

The underlying TypeError is not fixed here: it is a dict indexed with a slice
somewhere under run_attempts, and it is not locatable from the message alone.
That is the point — this change is what makes the next occurrence diagnosable.

Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
The p_iimn run of 2026-08-22 (20260822_183340_88cdd1fe) scored 0.760,
wrote reproduction_iimn.md and exported its ASTRA capsule, then died with

    Critical error in step execution: unhashable type: 'slice'

and was reported as a 0% success rate with a non-zero exit.

The cause was the TTS summary at the end of run_attempts, which sliced
each element of final_answers. Agents do not answer with strings: every
entry of that run's state_result.json is a dict, so x[:128] indexed a
dict with a slice. Reproduced from the persisted answers on the project
interpreter. On 3.12+, where slices became hashable, the same line
degrades to a KeyError instead — the fix covers both.

Every other consumer already coerced first (planner.py ~397,
evolution_engine.py ~245); this one did not, and the annotations said
list[str] while the data said list[dict]. Corrected both.

The second defect is that the narration sat in the step's success path
at all. A line that is spoken aloud must never decide whether the work
counts, so it moves to _narrate_step_completion and is caught there —
logged with its traceback, not swallowed.

Also anchors the exception-chaining test on the raise rather than on the
message text, which now appears in prose earlier in the module.
The p_iimn re-run of 2026-08-22 reached step 4 of 6 and died with

    ❌ Planner: Execution failed: EOF when reading a line

Two defects, one behind the other.

request_user_exit called input() on the benchmark path. In a batch run
stdout is redirected and stdin is not a TTY, so the prompt printed into a
log and the read raised EOFError — a message naming neither the question
nor the step. It now raises UserInterventionRequired carrying the
question. Raising rather than exit(1) is deliberate: the CSV harness
counts the row and still prints its summary, which SystemExit from inside
the planner would skip. pricing.py and csv_mode._prompt_with_default were
already headless-safe; this was the last blocking prompt.

It should never have been asked. data_acquisition declared
/workspace/data/ as an output and wrote nine files into it, but
_verify_expected_outputs compares against a scan that yields files only,
so a directory could never match. The step was permanently "missing
outputs" and blocked every step downstream. A declared directory is now
satisfied by any file inside it, matched on the trailing directory name
because plans declare workspace-absolute paths while the scan returns
relative ones.

15 tests, including that an interactive yes still continues and an
interactive no still exits. Suite: 346 passed, the same 16 pre-existing
failures.
The rubric cache exists so "verifier scores stay comparable across iterations
of the same task" (verifier_claim_cache_test.py) — the first run freezes the
ranked claim list and later runs reuse it verbatim (VerifierEvaluator.evaluate).
It keys on sha256(goal). But the goal the verifier reads is the
knowledge-wrapped one, and the retry loop prepends the previous attempt's whole
answer dict to it before re-running the same step, so every retry hashes a
longer string, misses the cache, and writes a fresh rubric.

Measured on the run this came from (p_iimn task_001, step
task_and_resource_discovery, three attempts):

    original_task_<uuid>.txt   2657 B, md5 02b411d9…  — identical all three
    goal_<uuid>.txt            2657 → 6505 → 11353 B  — grows every attempt
    key(goal)      a19ee032 / d11aa80f / 3530d233     — 3 rubrics
    key(original)  a19ee032 / a19ee032 / a19ee032     — 1 rubric

The three attempts drew 21, 24 and 20 claims with disjoint ids and scored
0.000 / 0.649 / 0.557 — three numbers on three different scales, which
evolution_engine then compares with max() and reports as "Best run".

_extract_claims now takes cache_key_text separately from goal, so the claim
writer still sees the full wrapped goal (prior-attempt context is useful in the
prompt) while the cache keys on the invariant task. WorkflowInfo already keeps
that unwrapped task "for similarity matching", and record_lineage and the
variation prompts already prefer `original_task or goal`; the verifier's key was
the one place that did not. Default is falsy, so every other caller is unchanged.

The 16 failing tests in the suite (evaluation_cli, failure_fingerprint, pricing,
logprobs, csv_mode_logging) fail identically at 67b4251 with these two files
reverted.

Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
Issue #196. Step reproduction_spec_analysis declared
workspace/analysis/iimn_reproduction_plan.md, wrote iimn_reproduction_guide.md
at the workspace root instead, was scored 0.799 and accepted, and then killed
the run at the next step's dependency gate. None of the 26 claims in its frozen
rubric mentioned the declared path — claims are extracted partly from execution
text and workspace listing, so the rubric inherited the agent's choice of
deliverable and could not catch the agent choosing wrong.

expected_outputs is the anchor: the planner writes it before the step runs, so
it cannot have been shaped by execution. The verifier never saw it — it is
handed a uuid, and neither state_result.json nor the workflow folder carries the
declaration.

Threading it through start_workflow_evolution, the generation loop,
IndividualRun and the factory would touch five shared signatures. It is not
needed: the planner already passes original_task=step_task, and that is the same
string the verifier keys its rubric cache on. So the declaration is written
under that key and read back under it — one write in the planner, one read in
the verifier, no signature changed.

The claims are prepended, so claims[:max_claims] cannot drop them, and added
after _extract_claims has persisted the rubric, so they never enter the frozen
cache: the cache freezes what a run produced, these belong to the plan and must
follow it when it changes. Every failure path degrades to the previous
behaviour — no declaration, unreadable file, or an exploding wf_info costs the
extra claims, never the evaluation.

One design question is deliberately left open in #196 rather than settled here:
this makes the score partly a function of the planner's path conventions, so a
step that produces an equivalent artefact under another name now fails a
maximum-importance claim. That is the intended reading of "declared output",
but it is Martin's call.

17 tests, including the one that pins planner and verifier to the same key
function — if those drift the declaration is written where nothing reads it,
and a silent stop is the failure mode this exists to end.

Suite: 369 pass; the same 16 failures are pre-existing.

Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
Issue #196. Both inner branches of the acceptance block assigned
TaskStatus.COMPLETED and broke, differing only in print_ok against print_warn:

    outputs_produced, missing_outputs = self._verify_expected_outputs(step)
    step.status = TaskStatus.COMPLETED
    if outputs_produced:  print_ok(...);   break
    else:                 print_warn(...); break

So _verify_expected_outputs computed the right answer and nothing acted on it.
Observed on a real run: step reproduction_spec_analysis was accepted at 0.799
without writing workspace/analysis/iimn_reproduction_plan.md, and the run died
at the *next* step's dependency gate — with a third attempt still unspent.

Now: while attempts remain, spend one on producing the deliverable rather than
banking the miss. On the last attempt keep COMPLETED, deliberately, so the
dependency gate still reports which output is missing for which step instead of
a generic step failure replacing that precise message. PlanStep.missing_outputs
carries the miss so COMPLETED alone no longer implies the deliverable exists.

Checked the tests first, per the standing rule: headless_planner_test covers
_verify_expected_outputs and planner_test covers _can_execute_step, but nothing
asserted what run_attempts does with the result. Uncovered, not deliberate.

Interacts with 8be2b35, as noted on the issue: mandatory expected_outputs claims
make a name mismatch louder, this makes it consequential. A step that produced
the right content under the wrong name now retries. Whether that is the desired
reading of expected_outputs is Martin's call and is flagged there, not assumed
here.

9 tests. Suite: 378 pass; the same 16 failures are pre-existing.

Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
Caught in production on run 6. Step `data_acquisition` declared two outputs:

    workspace/p_iimn_task_001/data/
    workspace/p_iimn_task_001/data/dataset_inventory.csv

as_claims worded both identically — "A file exists at that path, in the
workspace, and is non-empty" — so the directory became a maximum-importance
claim no correct run could satisfy. A step that did exactly what the plan asked
would have been marked down for it, which is the opposite of what 8be2b35 is
for.

Planner._verify_expected_outputs already makes this distinction (it short-
circuits a trailing-separator output through _directory_output_satisfied). The
claim now makes it too: a directory output asks for a directory holding at
least one non-empty file.

The existing test only asserted the claim's id, which was correct for both
kinds and so said nothing about the description. Four tests now cover the
distinction, including the exact pair run 6 recorded.

Suite: 382 pass; the same 16 failures are pre-existing.

Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
lfnothias and others added 7 commits August 23, 2026 10:41
Brings 123 commits of mimosa_v2 into the personal branch, which had not been
synced since June. Ten files conflicted. Resolution rule: where Martin made an
architectural call or solved the same problem, his choice stands; only genuinely
additive, generalizable work is carried over from this branch.

Martin's, taken whole:
  llm_provider.py       prompt caching (_supports_prompt_caching/_apply_cache_control)
                        and _is_claude_model, which generalises this branch's
                        Opus-4-only _is_no_temperature_model — "omit temperature
                        for all Anthropic models rather than version-gate"
  factory.py            deadline-bounded MCP discovery (300 s, capped backoff)
                        supersedes the fixed 5-attempt loop
  csv_mode.py           sab_files_transfer is async with two await call sites;
                        _format_goal_mode_results, _format_task_mode_results,
                        _analyze_results and _analyze_results_isolated were
                        deliberately removed and are not resurrected
  smolagent_factory.py  model=None, temperature=1.0, max_steps=128
  evolution_engine.py   rewards_history guard
  config.py             model defaults (deepseek-v4-pro/flash), export_astra,
                        vision_judge_model
  planner.py            _verify_expected_outputs(dep_task) — this branch passed
                        a list where a PlanStep is expected, a latent bug
  pyproject.toml        dependency-groups pytest>=9.1.1; this branch's identical
                        pytest>=8.0 group was a duplicate and is dropped, its
                        comment kept

Carried over, because mimosa_v2 has no equivalent:
  config.py             judge_extraction_model, model_tiers + resolve_model,
                        per-side Perspicacite KB selection, verifier cost knobs
                        — all default to None/inert, so behaviour is unchanged
                        until set, and all are persisted so the config-roundtrip
                        guard stays green
  planner.py            don't announce "Querying Perspicacite" when agent-side
                        grounding is off; the call returns a no-context sentinel
  workflow_factory.py   MAX_CONTEXT_TOKENS/WORKSPACE_DIR alongside SAVE_LOGPROBS
  variation_engine_test stubs llm_think_mutation_directive so the gradient-
                        history test stops requiring OPENAI_API_KEY, keeping the
                        assertion identical — decided by running both versions

One defect introduced and fixed during the merge: dropping
_is_no_temperature_model left a live call at llm_provider.py:464, which the
variation-engine suite caught. It now calls _is_claude_model.

Suite on the merged tree: 16 failed, 225 passed.
Plain mimosa_v2:            17 failed, 217 passed.
No failure is added; one is fixed.

Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
Second half of the sync. The first merge used the stale local mimosa_v2
(0c35d67); this brings the 14 commits that were only on origin, including the
two that bear directly on the earlier resolution:

  64f9ceb  fix(config): restore fields dropped by the JSON round-trip
  3848248  Merge PR #187 propose/config-roundtrip-coverage
  17c7d30  test(planner): cover dependency-task expected-output verification
  c4747da  fix(pricing): fall back to default pricing instead of prompting

Two conflicts, both resolved to Martin:

  llm_provider.py  max_retries stays 100. This branch's bdba070 capped it at 6
                   on the rationale that "the retry loop was `while True` and
                   this value was never read" — no longer true: upstream reads
                   it at both retry sites, so the cap is Martin's and only the
                   value differed.
  config.py        his discovery_addresses round-trip fix (absent key keeps the
                   default rather than falling back to [], which left MCP
                   discovery with no address range). The model_tiers load line
                   is kept alongside it.

tests/config_roundtrip_test.py now runs against this branch and passes, so every
field carried over from the personal branch is properly persisted.

Suite: 16 failed, 237 passed.

Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
Confirmed against this fix in production, and it went the wrong way.

Run 6 step `feature_detection_and_processing` declared five outputs. No raw
LC-MS data was reachable, so the run wrote each file as a labelled placeholder
— line 2 of feature_table_qtof.csv reads

    # PLACEHOLDER: MZmine Processing Output for qTOF (MSV000080492)
    # Status: NO REAL DATA AVAILABLE

— and all five mandatory claims PASSED at importance 10, one on the detail line
"File exists and is non-empty (1462 bytes)". So 8be2b35 contributed 50
importance-points of passes for files that state in their own second line that
they contain no data.

The wording was the defect. "A file exists at that path and is non-empty" is
satisfied by a stub, which means the claim pressures an agent to create the file
without pressuring it to fill the file. Under five such claims a run that cannot
obtain its inputs is pushed toward writing something — here honest placeholders,
but fabrication would have satisfied the claim equally well, and this fix would
have been the cause.

The claim now asks for content and names the failure mode: not a placeholder,
stub, template, or a note recording that the data could not be obtained.

The step still failed overall (0.552, hard_fail_capped), so other claims caught
what these missed — but these were on the wrong side of it.

Suite: 384 pass; the same 16 failures are pre-existing.

Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
Third defect in this fix, and the first one that punished honesty.

696bae2 disqualified "a file whose body announces missing or unavailable data".
An A/B probe against run 6's archived workspace applied that to
processing_log.md — 292 lines, per-dataset MZmine parameters, a parameter
comparison table and a recovery path, the most substantive artefact in that
workspace — and failed it, because the log honestly recorded that its *sibling*
outputs were placeholders. The wording could not tell a file that IS a stub from
a file that REPORTS on one, so it penalised exactly the behaviour the mechanism
exists to protect.

Now split by suffix, the same discriminator asb_eval already uses:

  .csv .tsv .mgf .json .parquet .mzml .mztab
      "holds actual records — data rows, spectra, or entries — and not merely
       comments, headers, or placeholder text standing in for data"
  everything else
      "substantive content produced by this step... A report that documents what
       was attempted and honestly records missing inputs or limitations does
       satisfy this claim"

Verified against the shipped wording on run 6's real artefacts:

  stub .mgf   fail  "only placeholder text; no actual spectral records"
  stub .tsv   fail  "only header and/or comments, no actual records"
  stub .mgf   fail  "contains placeholder markers"
  real .md    pass  "substantive (size > 1KB, non-empty lines)"

and previously, on the .csv that started this: old wording pass, new wording
fail ("could not be parsed as CSV: No columns to parse from file").

Four tests added; two earlier ones updated, since they asserted the single
wording this deliberately splits. 27 on the module, suite 388 pass, the same 16
failures pre-existing.

Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
Brings the track3 work onto the personal branch now that it is synced with
mimosa_v2. Three conflicts.

llm_provider.py — track3's version of the temperature recovery, which turns out
to agree with Martin: mimosa_v2 already falls back to 1.0 rather than stripping
the parameter, and track3 does the same behind a named _SAFE_MAX_TEMPERATURE,
plus a heuristic for gateways that return a bare 400 with no `param` field (the
OpenRouter case). The "strip to None" variant on this branch predates both.
Martin's _is_claude_model gate is preserved alongside track3's truncation-
escalation state, since those are orthogonal.

smolagent_factory.py — track3's capture of the last exception. Without it an
agent that exhausts its retries leaves completed=False with no exception and the
caller reports a timeout that never happened.

perspicacite_client.py — a real hand-merge. This branch threads kb_name through
_cache_key/_write_cache, which per-side KB selection requires: without it two
knowledge bases share one cache entry. track3 instead carried a _SETTINGS global
and _record() telemetry. Kept the kb_name threading, added the telemetry on both
the success and failure paths, and took _SETTINGS["max_papers"] over the
hardcoded 5.

Suite: 395 pass; the same 16 failures are pre-existing.

Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
New run-detail tab rendering the run from its structured record rather
than its own reporting. Backend: GET /api/runs/{id}/provenance serves the
transparency exporter's ASTRA capsule (decisions with alternatives and
universes; runs without one link to the family members that have one)
plus every asb_eval evaluation capsule naming the run (executor verdicts,
workspace provenance flags, pinned-instrument judge layer). Follows
store.py's defensive contract; capsule/eval roots overridable via
MIMOSA_CAPSULE_DIR / MIMOSA_EVAL_DIR. Frontend: ProvenancePanel with
decision rows (chosen green, alternatives dim), evaluation cards
(score-over-decided headline, per-criterion verdicts, flags), and the
judge block kept visually separate from the executor score.
…n feed

Four surfaces on top of the Provenance work:

Rendering (frontend/src/render/): shiki-highlighted code, collapsible
JSON tree, GFM markdown, CSV table previews, ANSI-stripped logs — wired
into the replay inspector, workspace previews, and the artifacts tab,
each with a raw toggle.

Evolution tab: the family replayed as an animated SVG lineage (reveal
per tick, play/step/speed/scrub, best-so-far badge chasing the
frontier, ancestry glow) with a per-node narration panel — score delta
vs parent, claims, QD/novelty, cost, selection log, and the textual
gradient. Served by GET /runs/{id}/evolution.

Atlas (/atlas, GET /atlas/{space}): every run PCA-projected with
lineage trails, colour modes, family filter, fleet time-replay,
zoom/pan, and a per-family point-by-point trajectory mode. Two spaces:
qd (Mimosa's behaviour descriptor — measured byte-identical within a
family, i.e. task-level, families coincide by construction) and
genotype (TF-IDF of the evolved workflow code, 113 runs, where
within-family drift is real). The trajectory bar says so and offers
the switch when a family's points coincide.

Live: the watcher now covers workflows, memory, runs_capsule and
evaluations roots and emits step_appended / llm_call_logged /
gradient_updated / evaluation_updated / astra_updated /
evaluation_capsule_updated. The run page auto-refreshes affected
panels and shows a live feed that diffs the ASTRA decision layer on
every capsule update.

Backend tests 57 -> 67; tsc, oxlint, vite build clean; new deps
shiki/react-markdown/remark-gfm/diff (frontend), numpy (backend).
@lfnothias

Copy link
Copy Markdown
Collaborator Author

Split into four independent slices against mimosa_v2, each cherry-picked unchanged from this branch and tested on a fresh Python 3.11 environment (the same 17 pre-existing failures as the base, nothing else):

The June commits (model tiers, verifier cost knobs, per-side KB grounding, sandbox imports) are not re-proposed here: model tiers were declined in #180, and the verifier knobs are still #181. This PR stays open as the umbrella for #198 and will be closed once the slices are in.

@Fosowl

Fosowl commented Sep 3, 2026

Copy link
Copy Markdown
Member

Closing. This is 79 files and 37 commits mixing UI work, LLM fixes, verifier changes, and planner changes, it's unreviewable as a single unit, we will evaluate #209#212 independently.

@Fosowl Fosowl closed this Sep 3, 2026
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

2 participants