Skip to content

refactor(core): drop dead optimizer-memory API + unused params; fix misleading cache docstring - #212

Open
OsherElhadad wants to merge 2 commits into
mainfrom
refactor/issue-114-drop-write-only-memory
Open

refactor(core): drop dead optimizer-memory API + unused params; fix misleading cache docstring#212
OsherElhadad wants to merge 2 commits into
mainfrom
refactor/issue-114-drop-write-only-memory

Conversation

@OsherElhadad

Copy link
Copy Markdown
Collaborator

Closes #114

The issue's premise is only PARTLY right — scoped accordingly

#114 says memory.py is write-only and the jsonl machinery can go. I checked empirically before deleting, and the issue itself asked to "confirm the rejected.jsonl/history.jsonl files aren't relied on by the dashboard before removing writes … (the dashboard reads events.jsonl, not these — verify)".

Verified: that parenthetical is wrong. The jsonl files have a live reader. So I removed only the genuinely-unread surface and kept the writes.

Write-only proof

The prompt-facing API — render(), entries(), _render_impact — has zero callers outside memory.py itself:

$ grep -rn "\.render()\|\.entries()\|_render_impact" --include="*.py" core/cap_evolve skills examples
core/cap_evolve/memory.py:40:def _render_impact(e: dict) -> Optional[str]:
core/cap_evolve/memory.py:86:        items = self.entries()[-limit:]
core/cap_evolve/memory.py:102:            imp = _render_impact(e)
core/cap_evolve/memory.py:133:        items = self.entries()[-limit:]
core/cap_evolve/memory.py:142:            imp = _render_impact(e)

Every hit is memory.py calling itself. LEDGER.md/JOURNAL.md/RUNMAP.md (via _augment_instructions) fully replaced this as the optimizer's cross-iteration channel. Dead → removed.

Same for the per-record note / broke / fixed fields — written on every iteration, read by nobody:

$ grep -rn "broke\|fixed" dashboard/frontend/src dashboard/backend --include="*.tsx" --include="*.ts" --include="*.py"
dashboard/frontend/src/test/insights.test.ts:36:  { candidate_id: 'c', summary: '', reason: 'broke correctness gate', val: 0 },
dashboard/frontend/src/components/Trajectories.tsx:115:  <div className="fixed inset-0 z-40 ...
dashboard/frontend/src/lib/api.ts:71: * first snapshot) from a genuinely missing/broken run. */
dashboard/backend/capevolve_dashboard/runs.py:19:  prefixed ``run_`` or lacking ``events.jsonl``. ...

Three CSS/prose coincidences, zero real readers. Field-level audit against the dashboard's consumers:

field readers in dashboard verdict
candidate_id 5 live
summary 9 live
val 9 live
reason 8 live
note 0 dead → removed
broke 0 dead → removed
fixed 0 dead → removed

What I did NOT delete, and why

$ grep -rn "rejected.jsonl\|history.jsonl" dashboard
dashboard/backend/capevolve_dashboard/memory.py:31:  "history": _read_jsonl(root / "history.jsonl"),
dashboard/backend/capevolve_dashboard/memory.py:32:  "rejected": _read_jsonl(root / "rejected.jsonl"),

read_memory() is served at GET /api/runs/{run_id}/memory (app.py:60), consumed by MemoryPanel.tsx (the Deep-Dive Memory tab) and insights.ts::deadEnds() (the Insights "what not to try" grouping), and baked into export_static.py:83. Deleting the writes would have blanked two shipped UI panels. The .add() writes and both jsonl files stay — they are audit/UI records, which is now what the module docstring says they are.

Deleted

removed why
RejectedMemory.render / .entries, History.render / .entries zero callers (grep above)
_render_impact, _store_impact only used by the above
note= / impact= kwargs + note/broke/fixed fields written, never read
harness._latest_journal_note sole caller was the dead note=
_candidate_task_impact call in run_step existed only to fill those dead fields — re-read rollouts from disk every iteration. _build_ledger and _reconcile_journal keep their own computations, so no signal is lost
rejected / history params on _augment_instructions + _build_ledger unused in both bodies

82 insertions, 177 deletions across 9 files (net −95). memory.py: 145 → 54 lines. No files removed (memory.py still carries the two live writers).

Algorithm-label stamp: preserved, untouched

_init_memory_store is not renamed, moved, or inlined — my change does not touch it at all, so PR #204's algorithm stamp and the dashboard badge are structurally unaffected. Proven on a locally-built #199 + #204 + this merge, all three deterministic loops:

### run_e2e_hill-climb
{"t": 1785367294.371619, "kind": "algorithm", "name": "hill-climb:all"}
### run_e2e_gepa
{"t": 1785367297.4603388, "kind": "algorithm", "name": "gepa"}
### run_e2e_skillopt
{"t": 1785367300.6982982, "kind": "algorithm", "name": "skillopt"}

and the dashboard summary is non-blank for each: 'hill-climb:all', 'gepa', 'skillopt'.

Corrected cache docstring

Was — describing a flag, a wiring, and a function that do not exist:

It is an optimization, not a source of truth: events.jsonl still records every evaluation. Wiring into evaluate_candidate is OFF by default and gated behind a flag (see maybe_cached_score) so it cannot silently change behavior.

There is no maybe_cached_score anywhere in the repo. Now:

Scope: GEPA only. The single consumer is gepa._eval_minibatch, where the same parent is re-sampled from the Pareto frontier across iterations and re-scored on overlapping minibatches — that repetition is what the cache pays for. harness.evaluate_candidate (every full-val and sealed-test eval, in every algorithm) does NOT consult it and always pays full price, so re-scoring an identical candidate on full val — common on --resume or a seed re-eval — is not deduplicated. Wiring it in there is a possible perf win, not existing behavior; there is no flag for it today.

#199's _IGNORE_DIRS / INJECTED_* change is untouched (it merges clean).

Note for #128 / #129

Those add a genuinely-read memory. Build on _augment_instructions, not on memory.py. That is the only function whose output reaches the optimizer prompt, and it is now 3 params instead of 5 — add one there and every algorithm (hill-climb, gepa, skillopt) picks it up, since all three route through it. memory.py's docstring now says this explicitly so nobody re-adds a render() expecting it to reach a prompt. I deliberately built no speculative scaffolding for them.

Expected merge order

#199#204 → this PR. Based on origin/main; #199 and #204 conflict with each other (both edit the _init_memory_store call sites) — that is pre-existing and independent of this PR. After both land, this PR conflicts on exactly one hunk in gepa.py:614 (#199's render_instructions vs my 2-arg _augment_instructions); resolution is mechanical — keep #199's call, drop , rejected, history:

instructions = render_instructions(
    parent_mb, mb, f"GEPA minibatch of {len(mb)} train task(s)", ctx=ctx,
    algorithm="gepa", run_dir=run_dir, parent_id=parent["id"],
    extra=_gepa_block(refl_summary, focus_label, mb, has_trajectories=has_traj))
instructions = _augment_instructions(instructions, workdir, run_dir)

I verified that resolution locally: 203 passed on the merged tree.

Verification

Full core suite on this branch (baseline 179)

$ PYTHONPATH=core python -m pytest core/tests -q
........................................................................ [ 40%]
........................................................................ [ 80%]
...................................                                      [100%]
179 passed in 59.77s

179 passed, 0 failed — no net test loss. One test was replaced, not dropped: test_rejected_memory_roundtrip_and_render asserted on the removed render(), so it could not survive. It is superseded by test_memory_jsonl_record_shape_matches_dashboard_contract, which pins the exact key set the dashboard reads ({candidate_id, summary, reason, val} / {candidate_id, summary, val}) — a stronger guard, since it fails if anyone renames or adds a field and silently breaks the Memory panel.

Dashboard backend (would have caught a removed write)

$ PYTHONPATH=core python -m pytest dashboard/backend/tests -q
42 passed, 1 warning in 1.88s

compileall

$ python -m compileall -q core skills && echo CLEAN
COMPILEALL CLEAN

Real e2e, all three deterministic algorithms, zero API cost

Every one reaches the sealed test number 1.0 ± 0.0 and the memory jsonl still round-trips through the real dashboard reader:

algorithm sealed test best_id dashboard algorithm history / rejected
hill-climb 1.0 (SE 0.0) cand_0001 hill-climb:all 1 / 2
gepa 1.0 (SE 0.0) gepa_0001 gepa 1 / 2
skillopt 1.0 (SE 0.0) so_e01s01 skillopt 1 / 2
--- memory jsonl still written + dashboard-readable ---
history  n=1 [{'candidate_id': 'cand_0001', 'summary': 'candidate cand_0001 (val 1.000, Δ +1.000)', 'val': 1.0}]
rejected n=2 [{'candidate_id': 'cand_0002', 'summary': 'candidate cand_0002 (val 1.000, Δ +0.000)', 'reason': 'paired Δ̄=+0.0000 <= 0 (SE=0 → STRICT fallback, warned; n=2)', 'val': 1.0}]

Records carry exactly the four live keys and no dead note/broke/fixed — the deletion is visible on disk.

Zero new runtime deps; tests stay in core/tests/.

…ache docstring

The issue's "memory.py is write-only" premise is only PARTLY right. The
render/entries API and the note/impact fields ARE dead, but the jsonl files
themselves have a live reader the issue told us to verify: the dashboard's
GET /api/runs/{id}/memory (dashboard/backend/capevolve_dashboard/memory.py),
which feeds the Memory panel and the Insights "dead ends" grouping. So the
WRITES stay; only the genuinely-unread surface goes.

Removed (zero readers, proven by grep):
  - RejectedMemory.render / .entries, History.render / .entries, _render_impact,
    _store_impact — the prompt-facing API. LEDGER/JOURNAL/RUNMAP replaced it.
  - the `note=` and `impact=` kwargs and the `note`/`broke`/`fixed` record fields.
  - harness._latest_journal_note — its only caller was that dead `note=`.
  - the per-iteration _candidate_task_impact call in run_step, which existed only
    to populate those dead fields (re-read rollouts from disk every iteration).
    The LEDGER and _reconcile_journal paths keep their own computations.
  - the unused `rejected` / `history` params on _augment_instructions and
    _build_ledger.

_init_memory_store is untouched, so PR #204's algorithm-label stamp and the
dashboard badge are unaffected.

cache.py's docstring claimed wiring into evaluate_candidate was "OFF by default
and gated behind a flag (see maybe_cached_score)" — no such wiring, flag, or
function exists. Replaced with what the cache actually does: GEPA-only, consumed
solely by gepa._eval_minibatch; evaluate_candidate always pays full price.

Test change: test_rejected_memory_roundtrip_and_render tested the removed
render(); replaced by test_memory_jsonl_record_shape_matches_dashboard_contract,
which pins the exact keys the dashboard reads — the contract that actually
matters. Net test count unchanged.

Closes #114
Copilot AI review requested due to automatic review settings July 29, 2026 23:26

Copilot AI left a comment

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

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

Copilot was unable to review this pull request because the user who requested the review has reached their quota limit.

@skillberry-bot

Copy link
Copy Markdown
Contributor

Automatic Labeling Failed

An error occurred while trying to automatically label this pull request. Please check the workflow logs for details and add labels manually.

@OsherElhadad

Copy link
Copy Markdown
Collaborator Author

🔬 Evidence

All commands run in a clean worktree at origin/main + this branch. Python 3.14 venv, zero API cost throughout (mock optimizer).


1. Write-only proof — the prompt-facing API has zero callers

$ grep -rn "\.render()\|\.entries()\|_render_impact" --include="*.py" core/cap_evolve skills examples
core/cap_evolve/memory.py:40:def _render_impact(e: dict) -> Optional[str]:
core/cap_evolve/memory.py:86:        items = self.entries()[-limit:]
core/cap_evolve/memory.py:102:            imp = _render_impact(e)
core/cap_evolve/memory.py:133:        items = self.entries()[-limit:]
core/cap_evolve/memory.py:142:            imp = _render_impact(e)

Every hit is memory.py calling itself. (dashboard.py:884/888 also match .entries( — that is JavaScript Object.entries in the embedded frontend, unrelated.)

2. Every WRITE site (all kept — they feed the dashboard)

$ grep -rn "rejected\.add(\|history\.add(" --include="*.py" core skills examples
core/cap_evolve/gepa.py:603:            rejected.add(cid, f"candidate {cid} (mb {child_mb.reward:.3f} vs parent "
core/cap_evolve/gepa.py:632:            history.add(cid, summary, cand_val.reward)
core/cap_evolve/gepa.py:637:            rejected.add(cid, summary, decision_dict.get("reason", "val gate"), cand_val.reward)
core/cap_evolve/gepa.py:768:        rejected.add(mid, f"merge {a['id']}+{b['id']}", "merge local gate: < max(parents)")
core/cap_evolve/gepa.py:790:        history.add(mid, summary, cand_val.reward)
core/cap_evolve/gepa.py:794:        rejected.add(mid, summary, decision_dict.get("reason", "val gate"), cand_val.reward)
core/cap_evolve/harness.py:1351:            history.add(cid, summary, cand_val.reward, note=note, impact=impact)
core/cap_evolve/harness.py:1354:            rejected.add(cid, summary, decision.reason, cand_val.reward, note=note, impact=impact)
core/tests/test_per_task_impact.py:52:    rejected.add("cand_0001", ...)

Note GEPA never passed note=/impact= at all — only harness.py did, which is exactly the redundant _candidate_task_impact path #114 flagged.

3. The live reader that contradicts the issue's parenthetical

$ grep -rn "rejected.jsonl\|history.jsonl" dashboard
dashboard/backend/capevolve_dashboard/memory.py:31:        "history": _read_jsonl(root / "history.jsonl"),
dashboard/backend/capevolve_dashboard/memory.py:32:        "rejected": _read_jsonl(root / "rejected.jsonl"),
dashboard/backend/tests/test_memory_api.py:14:    (rd.root / "history.jsonl").write_text(
dashboard/backend/tests/test_memory_api.py:18:    (rd.root / "rejected.jsonl").write_text(

Served + consumed:

$ grep -rn "memory" dashboard/backend/capevolve_dashboard/app.py
12:from . import memory as _memory
60:    @app.get("/api/runs/{run_id}/memory")
61:    def get_memory(run_id: str):
62:        return _memory.read_memory(_resolve_or_404(run_id))

$ grep -rn "api.memory\|memory?.rejected" dashboard/frontend/src
components/MemoryPanel.tsx:16:    queryFn: ({ signal }) => api.memory(runId, signal),
components/Insights.tsx:10:  const { data: memory } = useQuery({ queryKey: ['memory', runId], ... })
components/Insights.tsx:17:  const ends = deadEnds(memory?.rejected ?? [])
lib/api.ts:117:    getJSON<MemoryResult>(`/api/runs/${encodeURIComponent(id)}/memory`, signal),

4. Field-level dead/live audit

$ for f in candidate_id summary val reason note broke fixed; do
    echo -n "$f: readers in dashboard = "
    grep -rn "\b$f\b" dashboard/frontend/src/components/MemoryPanel.tsx \
      dashboard/frontend/src/lib/insights.ts dashboard/frontend/src/lib/types.ts | grep -c .
  done
candidate_id: readers in dashboard = 5
summary: readers in dashboard = 9
val: readers in dashboard = 9
reason: readers in dashboard = 8
note: readers in dashboard = 0
broke: readers in dashboard = 0
fixed: readers in dashboard = 0

5. No maybe_cached_score exists (the docstring's cited function)

$ grep -rn "maybe_cached_score" --include="*.py" core skills
core/cap_evolve/cache.py:14:    behind a flag (see ``maybe_cached_score``) so it cannot silently change behavior.

The only occurrence is the docstring referring to itself. And the cache's only real consumer:

$ grep -rn "EvalCache\|hash_candidate_dir" --include="*.py" core/cap_evolve
core/cap_evolve/gepa.py:55:from .cache import EvalCache, hash_candidate_dir
core/cap_evolve/gepa.py:116:    cache: EvalCache | None,
core/cap_evolve/gepa.py:142:    chash = hash_candidate_dir(candidate_dir) if cache is not None else None
core/cap_evolve/gepa.py:486:    cache = EvalCache(run_dir.root / "eval_cache.json")
core/cap_evolve/gepa.py:726:    cache: EvalCache, mb_size: int, ...
core/cap_evolve/cache.py:34:def hash_candidate_dir(candidate_dir: Path) -> str:
core/cap_evolve/cache.py:64:class EvalCache:
core/cap_evolve/__init__.py:16:from .cache import EvalCache, hash_candidate_dir

GEPA only — confirming the corrected docstring.

6. Post-change: no leftover references

$ grep -rn "note=note\|impact=impact\|_latest_journal_note" core/cap_evolve
core/cap_evolve/target_profile.py:154:  notes=str(spec["notes"]), resolution_note=note)

Unrelated (target_profile's own resolution_note).

$ grep -n "_build_ledger\|_augment_instructions" core/cap_evolve/harness.py core/cap_evolve/gepa.py
core/cap_evolve/harness.py:716:def _build_ledger(workdir: Path, run_dir: RunDir) -> None:
core/cap_evolve/harness.py:890:def _augment_instructions(instructions: str, workdir: Path, run_dir: RunDir) -> str:
core/cap_evolve/harness.py:900:    _build_ledger(workdir, run_dir)
core/cap_evolve/harness.py:1244:    instructions = _augment_instructions(instructions, workdir, run_dir)
core/cap_evolve/gepa.py:57:    _augment_instructions,
core/cap_evolve/gepa.py:569:        instructions = _augment_instructions(instructions, workdir, run_dir)

Both down to 3 params from 5, all call sites updated.

7. Full core suite (baseline 179)

$ PYTHONPATH=/tmp/wt-114/core python -m pytest core/tests -q
........................................................................ [ 40%]
........................................................................ [ 80%]
...................................                                      [100%]
179 passed in 59.77s

179 passed, 0 failed. The flaky test_dashboard_launch.py::test_maybe_launch_spawns_when_available (#200, port 7878) passed in this environment.

8. Dashboard backend suite

$ PYTHONPATH=/tmp/wt-114/core python -m pytest dashboard/backend/tests -q
..........................................                               [100%]
42 passed, 1 warning in 1.88s

(The one warning is a pre-existing StarletteDeprecationWarning from fastapi.testclient.)

9. compileall

$ python -m compileall -q core skills && echo COMPILEALL CLEAN
COMPILEALL CLEAN

10. Diff quantified

$ git diff --stat origin/main
 core/cap_evolve/__init__.py                       |   2 +-
 core/cap_evolve/cache.py                          |  16 ++-
 core/cap_evolve/gepa.py                           |   2 +-
 core/cap_evolve/harness.py                        |  42 ++-----
 core/cap_evolve/memory.py                         | 146 ++++------------------
 core/cap_evolve/rundir.py                         |   4 +-
 core/tests/test_core.py                           |  35 ++++--
 core/tests/test_per_task_impact.py                |   7 +-
 skills/algorithms/skillopt/references/concepts.md |   5 +-
 9 files changed, 82 insertions(+), 177 deletions(-)

$ git diff --numstat origin/main | awk '{a+=$1;d+=$2} END{print "added:",a," removed:",d}'
added: 82  removed: 177

memory.py 145 → 54 lines. Zero files removed (it still holds the two live writers).


11. Merge-order verification — #199 + #204 + this

Built the merged tree locally:

$ git worktree add /tmp/wt-114-merge -b tmp/merge-114 origin/fix/issue-109-optimizer-context
$ git merge origin/feat/issue-117-event-ticker
CONFLICT (content): Merge conflict in core/cap_evolve/gepa.py
CONFLICT (content): Merge conflict in core/cap_evolve/skillopt.py

Pre-existing, between #199 and #204 themselves (both edit the _init_memory_store(...) call line) — nothing to do with this PR. Both sides are additive; resolution keeps both:

rejected, history, store = _init_memory_store(run_dir, store, algorithm="gepa")
ctx = ctx or OptimizerContext()

Then merging this PR gives exactly one conflict:

$ git merge refactor/issue-114-drop-write-only-memory
Auto-merging core/cap_evolve/__init__.py
Auto-merging core/cap_evolve/cache.py
Auto-merging core/cap_evolve/gepa.py
CONFLICT (content): Merge conflict in core/cap_evolve/gepa.py
Auto-merging core/cap_evolve/harness.py
Auto-merging core/cap_evolve/rundir.py

cache.py auto-merges#199's _IGNORE_DIRS / INJECTED_* change is preserved, my docstring edit does not touch it. The single gepa.py hunk resolves mechanically (keep #199's render_instructions, drop the two dead args).

12. _init_memory_store intact on the merged tree — stamp preserved

$ sed -n "$(grep -n 'def _init_memory_store' core/cap_evolve/harness.py | cut -d: -f1),+22p" core/cap_evolve/harness.py
def _init_memory_store(run_dir: RunDir, store, algorithm: str | None = None):
    """Create the optimizer memory (rejected + accepted history) and ensure a
    version store (default git) is initialized + holds an initial 'seed' commit.

    Also stamps the run's ``algorithm`` event. Every deterministic loop (hill-climb,
    gepa, skillopt) routes through here, so logging it once here is what makes the
    dashboard's algorithm label work for all of them — and from iteration 1, not only
    once ``final.json`` exists.

    NOTE for #114 (removing write-only optimizer memory): if this function is renamed,
    split or inlined, the ``algorithm`` stamp must move with it — it is the single
    choke point the dashboard label depends on. ``test_dashboard.py``'s
    ``test_every_deterministic_loop_stamps_the_algorithm_event`` guards the drop.
    """
    from .memory import History, RejectedMemory
    from .store import VersionStore
    if algorithm and last_algorithm_event(run_dir) != algorithm:
        run_dir.log_event("algorithm", name=algorithm)
    rejected = RejectedMemory(run_dir.rejected_path)
    history = History(run_dir.history_path)

#204 left a NOTE for #114 asking that the stamp move if this function is renamed/split/inlined. It is none of those — this PR does not touch _init_memory_store, so the note's condition never triggers and #204's guard test still applies unchanged.

13. Real e2e on the merged tree — all three deterministic algorithms

$ bash /tmp/e2e114m.sh   # toy_calc, mock optimizer, fresh run dir per algorithm

================ ALGORITHM: hill-climb ================
--- sealed test number (final.json) ---
SEALED TEST: {'split': 'test', 'reward': 1.0, 'stderr': 0.0, 'pass_k': {'1': 1.0, '2': 0.0},
  'pass_at_k': {'1': 1.0, '2': 1.0}, 'per_task': [{'task_id': 'a7', 'reward': 1.0, ...},
  {'task_id': 'a8', 'reward': 1.0, ...}], 'cost_usd': 0.0, 'tokens': 0} best_id: cand_0001
--- dashboard summary algorithm field ---
algorithm = 'hill-climb:all'
counts    = {'accepted': 1, 'rejected': 2, 'failed': 0, 'seed': 1, 'total': 4}
test      = 1.0

================ ALGORITHM: gepa ================
SEALED TEST: {'split': 'test', 'reward': 1.0, 'stderr': 0.0, ... 'cost_usd': 0.0} best_id: gepa_0001
algorithm = 'gepa'
counts    = {'accepted': 1, 'rejected': 0, 'failed': 0, 'seed': 1, 'total': 2}
test      = 1.0

================ ALGORITHM: skillopt ================
SEALED TEST: {'split': 'test', 'reward': 1.0, 'stderr': 0.0, ... 'cost_usd': 0.0} best_id: so_e01s01
algorithm = 'skillopt'
counts    = {'accepted': 1, 'rejected': 2, 'failed': 0, 'seed': 1, 'total': 4}
test      = 1.0

All three reach a sealed test number of 1.0 ± 0.0 at cost_usd: 0.0, and the dashboard algorithm label is non-blank for each.

14. The algorithm event on disk, per run dir

$ for RD in .../run_e2e_{hill-climb,gepa,skillopt}; do
    echo "### $(basename $RD)"; grep '"kind": *"algorithm"' "$RD/events.jsonl"; done
### run_e2e_hill-climb
{"t": 1785367294.371619, "kind": "algorithm", "name": "hill-climb:all"}
### run_e2e_gepa
{"t": 1785367297.4603388, "kind": "algorithm", "name": "gepa"}
### run_e2e_skillopt
{"t": 1785367300.6982982, "kind": "algorithm", "name": "skillopt"}

The algorithm-label stamp works for all three loops after this PR.

15. Memory jsonl still round-trips through the REAL dashboard reader

Read back via capevolve_dashboard.memory.read_memory() — the actual production code path, not a stub:

--- hill-climb ---
history  n=1 [{'candidate_id': 'cand_0001', 'summary': 'candidate cand_0001 (val 1.000, Δ +1.000)', 'val': 1.0}]
rejected n=2 [{'candidate_id': 'cand_0002', 'summary': 'candidate cand_0002 (val 1.000, Δ +0.000)', 'reason': 'paired Δ̄=+0.0000 <= 0 (SE=0 → STRICT fallback, warned; n=2)', 'val': 1.0}]

--- gepa ---
history  n=1 [{'candidate_id': 'gepa_0001', 'summary': 'candidate gepa_0001 (val 1.000, Δ +1.000)', 'val': 1.0}]
rejected n=2 [{'candidate_id': 'gepa_0002', 'summary': 'candidate gepa_0002 (mb 1.000 vs parent 1.000)', 'reason': 'local minibatch gate: sum(child) <= sum(parent)', 'val': 1.0}]

--- skillopt ---
history  n=1 [{'candidate_id': 'so_e01s01', 'summary': 'candidate so_e01s01 (val 1.000, Δ +1.000)', 'val': 1.0}]
rejected n=2 [{'candidate_id': 'so_e02s01', 'summary': 'candidate so_e02s01 (val 1.000, Δ +0.000)', 'reason': 'paired Δ̄=+0.0000 <= 0 (SE=0 → STRICT fallback, warned; n=2)', 'val': 1.0}]

Records carry exactly the four live keys — the dead note/broke/fixed are gone from real on-disk output, and the Memory panel + Insights dead-ends still get everything they read.

16. Full suite on the merged tree

$ PYTHONPATH=/tmp/wt-114-merge/core python -m pytest core/tests -q
........................................................................ [ 35%]
........................................................................ [ 70%]
...........................................................              [100%]
203 passed in 73.97s

$ python -m compileall -q core skills && echo MERGED COMPILEALL CLEAN
MERGED COMPILEALL CLEAN

203 passed, 0 failed with #199 + #204 + this PR combined (179 base + 24 from #199/#204's new tests), including #204's test_every_deterministic_loop_stamps_the_algorithm_event.

@OsherElhadad

Copy link
Copy Markdown
Collaborator Author

🔍 Review — PR #212

Verdict: APPROVE WITH NITS

The issue's premise was wrong and the author is right. rejected.jsonl/history.jsonl have a live reader; the writes were correctly kept and only the unread prompt-facing API was deleted. Every deletion has zero readers — verified independently by grep, by a real e2e run of all three algorithms, and by hitting GET /api/runs/{id}/memory. No live code was deleted. All 14 of the PR's verification claims reproduced.

Blocking

None.

Non-blocking

  1. core/cap_evolve/cache.py:3-14 — the corrected docstring collides head-on with fix(algorithm): one shared scratch-file list so GEPA snapshots are clean (#110) #211. fix(algorithm): one shared scratch-file list so GEPA snapshots are clean (#110) #211 rewrites the exact same paragraph to document rollout_file and keeps the false maybe_cached_score line (origin/fix/issue-111-gepa-cache-traces:core/cap_evolve/cache.py:26-27). Trial-merging fix(algorithm): one shared scratch-file list so GEPA snapshots are clean (#110) #211 onto main+199+204+212 conflicts in cache.py and, if resolved by taking fix(algorithm): one shared scratch-file list so GEPA snapshots are clean (#110) #211's side, re-introduces the exact bug this PR fixes. Consequence: the false docstring silently comes back. Fix: whoever merges second must hand-resolve to keep both facts — fix(algorithm): one shared scratch-file list so GEPA snapshots are clean (#110) #211's rollout_file paragraph plus this PR's **Scope: GEPA only.** paragraph — and must not restore the maybe_cached_score sentence. Worth a note in fix(algorithm): one shared scratch-file list so GEPA snapshots are clean (#110) #211.

  2. skills/algorithms/skillopt/SKILL.md:28 and skills/algorithms/skillopt/references/concepts.md:38 say "with RejectedMemory/History" / "writes RejectedMemory/History". Both are literally still true (the writes stayed), but they read as if the memory is part of the optimization loop, which is precisely the confusion this PR set out to kill — and the author did fix the sibling paragraph at concepts.md:62-67. Consequence: a future contributor re-adds a render(). Fix: one-word clarification, e.g. "…and writes the dashboard's rejected.jsonl/history.jsonl audit records".

Nits

  1. core/cap_evolve/__init__.py:19,42-43 still exports History and RejectedMemory from the public __all__, but after this PR the only out-of-package importer is the new test (core/tests/test_core.py:133). Nothing external constructs them — _init_memory_store imports from .memory directly. Not worth churning now (removing a public name is a bigger blast radius than the 4 lines saved); flag only because the PR trimmed the module docstring's "optimizer memory" claim while leaving the exports.

  2. dashboard/frontend/src/components/MemoryPanel.tsx:10 — the doc comment calls the rejected list "do-not-re-propose", the mental model this PR just proved false (nothing re-reads it to avoid re-proposing). Frontend-only, out of this PR's scope, but it's the last surviving instance of the wrong framing.

Dead-vs-live audit

Greps run over **/*.py, **/*.md, **/*.ts, **/*.tsx, **/*.yaml across the whole repo excluding node_modules.

Deleted symbol / field Readers found Safe to delete?
RejectedMemory.render() 0 (only memory.py calling itself pre-PR)
History.render() 0
RejectedMemory.entries() 0 (insights.ts:60 is Map.entries(); dashboard.py:884/888 is JS Object.entries)
History.entries() 0
_render_impact 0
_store_impact 0
harness._latest_journal_note 0 — sole caller was the note= kwarg. _journal_tail (its own dependency) is kept and still live at harness.py:795 in _reconcile_journal. target_profile.py:154's resolution_note is unrelated.
record field note 0 in dashboard/, 0 in skills/, 0 in core/. dashboard.py:283 reads ev.get("note") from events.jsonl, not memory jsonl — different file, unaffected.
record field broke 0 (insights.test.ts:36 is the substring "broke correctness gate" inside a reason string; Trajectories.tsx:115 is Tailwind fixed inset-0)
record field fixed 0
rejected/history params on _augment_instructions, _build_ledger Unused in both bodies pre-PR; all 3 call sites updated (harness.py:874,1252, gepa.py:617)
_candidate_task_impact call in run_step See below — genuinely redundant

Anything KEPT that may be dead:

Kept thing Live? Note
rejected.jsonl / history.jsonl writes LIVE dashboard/backend/capevolve_dashboard/memory.py:31-32app.py:60-62 GET /api/runs/{id}/memoryMemoryPanel.tsx:37,52, Insights.tsx:17insights.ts:31 deadEnds, and export_static.py:83. Deleting these would have been the blocking bug the issue invited.
val on rejected records Written, read by nothing MemoryPanel.tsx:38 renders h.val for history only; the rejected <li> (lines 52-57) renders candidate_id/summary/reason and not r.val. deadEnds uses reason+candidate_id. So RejectedEntry.val (types.ts:192) is a declared-but-unrendered field. Under-deletion, minor — and correctly left alone: the API contract declares it, and dropping it is a dashboard-side decision, not this PR's.
History/RejectedMemory in __all__ Test-only importer Nit 3.

Algorithm-stamp survival

Survives. _init_memory_store is untouched by this PR — git diff origin/main -- core/cap_evolve/harness.py \| grep _init_memory_store returns empty, so #204's NOTE for #114 condition ("renamed, split or inlined") never fires.

Merged tree built for real at /tmp/rv212-merge: main → merge #199 → merge #204 (2 pre-existing conflicts in gepa.py/skillopt.py, both at the _init_memory_store(...) call line, between #199 and #204 themselves, nothing to do with #212; resolved additively keeping both algorithm= and ctx = ctx or OptimizerContext()) → merge #212 (exactly one conflict, gepa.py:613-622, resolved by keeping #199's render_instructions and dropping , rejected, history — exactly as the author reported).

$ grep -n -A22 "def _init_memory_store" core/cap_evolve/harness.py   # merged tree
1387:def _init_memory_store(run_dir: RunDir, store, algorithm: str | None = None):
...
1406:    if algorithm and last_algorithm_event(run_dir) != algorithm:
1407:        run_dir.log_event("algorithm", name=algorithm)

Real zero-API e2e on examples/toy_calc with the mock optimizer, fresh run dir per algorithm, on the merged tree:

### hill-climb
{"t": 1785368785.692548, "kind": "algorithm", "name": "hill-climb:all"}
### gepa
{"t": 1785368788.614474, "kind": "algorithm", "name": "gepa"}
### skillopt
{"t": 1785368791.6526232, "kind": "algorithm", "name": "skillopt"}

### hill-climb: summary.algorithm='hill-climb:all' counts={'accepted': 1, 'rejected': 2, 'failed': 0, 'seed': 1, 'total': 4} test=1.0
### gepa:       summary.algorithm='gepa'           counts={'accepted': 1, 'rejected': 0, 'failed': 0, 'seed': 1, 'total': 2} test=1.0
### skillopt:   summary.algorithm='skillopt'       counts={'accepted': 1, 'rejected': 2, 'failed': 0, 'seed': 1, 'total': 4} test=1.0

All three non-blank, matching the author's claim. Merged-tree suite: 203 passed; merged-tree vitest 52 passed; merged-tree backend 43 passed; compileall clean.

Did anything live get deleted?

NO.

Every deleted symbol has zero readers (table above). The live path is intact end to end after this PR — real e2e on this branch, then GET /api/runs/{id}/memory against the produced run dirs:

### hill-climb GET /api/runs/run_e2e212/memory -> 200
  history : [{"candidate_id":"cand_0001","summary":"candidate cand_0001 (val 1.000, Δ +1.000)","val":1.0}]
  rejected: [{"candidate_id":"cand_0002",...,"reason":"paired Δ̄=+0.0000 <= 0 (SE=0 → STRICT fallback, warned; n=2)","val":1.0}, {…cand_0003…}]
  key sets: hist=[['candidate_id','summary','val']]  rej=[['candidate_id','reason','summary','val']]
### gepa      -> 200  history=1 rejected=2   (gepa_0001 accepted; gepa_0002/0003 local-minibatch-gate rejects)
### skillopt  -> 200  history=1 rejected=2   (so_e01s01 accepted; so_e02s01/so_e02_slow rejects)

Non-empty for all three, exactly the 4 keys MemoryPanel + deadEnds read.

insights.ts::deadEnds — its input is MemoryResult['rejected'], keyed on reason + candidate_id, both still written; its unit test (insights.test.ts:32) passes in the 45-test vitest run, and the real rejected records above carry populated reason strings that normalizeReason collapses.

export_static.py — ran for real against a post-PR run dir:

$ python -m capevolve_dashboard.export_static --base /tmp/e2e212out/hill-climb/.capevolve --run-id run_e2e212 --out /tmp/e2e212static
wrote 92 JSON files to /tmp/e2e212static
$ cat /tmp/e2e212static/*memory*.json
{"history":[{"candidate_id":"cand_0001","summary":"candidate cand_0001 (val 1.000, Δ +1.000)","val":1.0}],"rejected":[{"candidate_id":"cand_0002",…,"reason":"paired Δ̄=+0.0000 <= 0 …","val":1.0},{…}]}

dashboard/ is untouched by this PR (git diff --name-only origin/main | grep -c dashboard0), so no frontend consumer changed.

Replacement test — is it genuinely stronger?

Yes. core/tests/test_core.py:127-150 asserts the on-disk artifact (json.loads of the jsonl the writer produced) and pins the key set with set(recs[0]) == {...} — an equality, not a superset, so both a rename and an extra key fail it. That is the dashboard's actual contract: memory.py:31-32 reads those raw lines and types.ts:183-193 declares exactly those keys. The old test only asserted that render()'s markdown contained some substrings — output of a function nothing consumed.

Mutation-verified rather than assumed. Renamed reasonwhy in memory.py:

$ pytest core/tests/test_core.py -q -k memory_jsonl
E       AssertionError: assert {'candidate_i... 'val', 'why'} == {'candidate_i...mmary', 'val'}
E         Extra items in the left set:   'why'
E         Extra items in the right set: 'reason'
FAILED core/tests/test_core.py::test_memory_jsonl_record_shape_matches_dashboard_contract
1 failed, 16 deselected

(reverted). It fails on the rename that would blank the Memory panel's reject line and break deadEnds. One honest limitation: it asserts the writer's keys against a hardcoded literal, not against types.ts — a dashboard-side rename would still slip past. That's inherent to a Python test and out of scope; dashboard/backend/tests/test_memory_api.py:38 covers the reader half.

_candidate_task_impact — did removal drop data?

No. It was genuinely redundant. Three call sites existed pre-PR; the PR removed only the run_step one:

  • harness.py:745 in _build_ledger — computes it per row for every candidate, still there.
  • harness.py:803 in _reconcile_journal — computes it for the current candidate, still there, and run_step calls _reconcile_journal immediately above the deleted call, on the same cid, same "val" split. So the deleted call was a second identical read of the same rollouts in the same function body.
  • harness.py (test) test_per_task_impact.py:65 — the function itself is untested-by-removal; test still passes.

Per-task impact still lands in the run dir, verified on a post-PR run:

$ grep -n "RESULT (framework" $RD/JOURNAL.md
23:> **RESULT (framework, objective):** ACCEPTED (new champion) · val=1.000 Δ=+1.000 · fixed={a1, a4} · broke={—}.
28:> **RESULT …** REJECTED (champion unchanged) · val=1.000 Δ=+0.000 · fixed={—} · broke={—}. — its WHOLE batch was reverted…

fixed={a1, a4} is real localized per-task data, produced by the kept _reconcile_journal path. LEDGER.md also carries the broke {}/fixed {} columns ($RD/work/cand_000{1,2,3}/LEDGER.md). The dashboard does not render broke/fixed at all (grep "broke\|impact" core/cap_evolve/dashboard.py → empty), so nothing UI-facing lost a field either. The only thing lost is the broke/fixed keys on the memory jsonl records — which no reader ever read.

#128/#129 seam guidance

Correct. memory.py's new header points future work at _augment_instructions, and that is genuinely the choke point: harness.py:1252 (so hill-climb via run_step, and skillopt too — skillopt.py:295,468 both delegate to harness.run_step) plus gepa.py:617 directly. Two entry points, three algorithms, all covered.

Security

Unaffected. dashboard/ and core/cap_evolve/dashboard.py are untouched (0 files in the diff). dashboard.redact (hardened in #190/#193) still wraps the memory payload at dashboard/backend/capevolve_dashboard/memory.py:29, and the payload keys shrank rather than grew, so nothing new bypasses it. _SNAPSHOT_IGNORE (harness.py:1568) and the trial/protected-task logic (harness.py:1398,1513) are unmodified. Nothing deleted was in a redaction or protected set.

Merge-order note

Verified with real trial merges, not merge-tree alone.

Pairwise vs #212 (git merge-tree --write-tree):

212 x #199 (issue-109)  -> CONFLICT core/cap_evolve/gepa.py
212 x #204 (issue-117)  -> clean
212 x #210 (issue-110)  -> clean
212 x #211 (issue-111)  -> CONFLICT core/cap_evolve/cache.py

Recommended order: #199#204#211#210#212.

I differ from the author's #199#204#212 only on where #210/#211 sit, and the reason is finding 1: #212 should land after #211, not before. Both rewrite the same cache.py docstring paragraph. If #212 goes first, #211's merge conflicts and the natural "take the incoming version" resolution restores the false maybe_cached_score line. Landing #211 first means #212's docstring fix is applied last and its conflict resolution is the one that matters — and #212's resolution is trivially the right one because it only has to keep #211's rollout_file prose and swap the honesty-note lie.

Verification I re-ran

Branch tree at /tmp/rv-212 (2d9ff6d), merged tree at /tmp/rv212-merge. Python /tmp/ce-venv/bin/python (3.14). Zero API cost (mock optimizer) throughout.

$ PYTHONPATH=/tmp/rv-212/core python -m pytest core/tests -q
179 passed in 60.83s                                  # matches baseline 179, no net change

$ PYTHONPATH=/tmp/rv-212/core python -m pytest dashboard/backend/tests -q
42 passed, 1 warning in 1.47s                         # warning = pre-existing StarletteDeprecationWarning

$ cd dashboard/frontend && npx vitest run
Test Files  13 passed (13)
     Tests  45 passed (45)

$ python -m compileall -q core skills && echo COMPILEALL_CLEAN
COMPILEALL_CLEAN

#200's flaky test_dashboard_launch.py::test_maybe_launch_spawns_when_available passed here.

Deleted-symbol grep (repo-wide, node_modules excluded), post-PR:

$ for s in _render_impact _store_impact _latest_journal_note "RejectedMemory.render" "History.render"; do
    echo "-- $s"; grep -rn "$s" --include="*.py" --include="*.md" --include="*.ts" --include="*.tsx" --include="*.yaml" . ; done
-- _render_impact
-- _store_impact
-- _latest_journal_note
-- RejectedMemory.render
-- History.render

All five empty.

maybe_cached_score — confirmed nonexistent as a function; the only hit on every sibling branch tip is the docstring citing itself:

$ for b in origin/main origin/fix/issue-109-optimizer-context origin/feat/issue-117-event-ticker \
           origin/fix/issue-110-gepa-snapshot-ignore origin/fix/issue-111-gepa-cache-traces; do
    git grep -c "maybe_cached_score" $b; done
origin/main:core/cap_evolve/cache.py:1
origin/fix/issue-109-optimizer-context:core/cap_evolve/cache.py:1
origin/feat/issue-117-event-ticker:core/cap_evolve/cache.py:1
origin/fix/issue-110-gepa-snapshot-ignore:core/cap_evolve/cache.py:1
origin/fix/issue-111-gepa-cache-traces:core/cap_evolve/cache.py:1

And the new docstring's "single consumer is gepa._eval_minibatch" claim checks out against the code, not the docstring:

$ grep -n "cache\." core/cap_evolve/gepa.py
149:            cached = cache.get(chash, task.id) if cache is not None else None
178:                cache.put(chash, task.id, sc.reward, sc.feedback or "")
486:    cache = EvalCache(run_dir.root / "eval_cache.json")
$ grep -n "cache" core/cap_evolve/harness.py   # -> only shutil.ignore_patterns hits; evaluate_candidate never caches

Merged tree (main + #199 + #204 + #212):

$ PYTHONPATH=/tmp/rv212-merge/core python -m pytest core/tests -q
203 passed in 72.74s
$ PYTHONPATH=/tmp/rv212-merge/core python -m pytest dashboard/backend/tests -q
43 passed, 1 warning in 1.61s
$ cd dashboard/frontend && npx vitest run
Test Files  14 passed (14)
     Tests  52 passed (52)
$ python -m compileall -q core skills && echo COMPILEALL_CLEAN
COMPILEALL_CLEAN
$ grep -rn "_augment_instructions(\|_build_ledger(" core/cap_evolve/*.py
core/cap_evolve/gepa.py:617:        instructions = _augment_instructions(instructions, workdir, run_dir)
core/cap_evolve/harness.py:710:def _build_ledger(workdir: Path, run_dir: RunDir) -> None:
core/cap_evolve/harness.py:864:def _augment_instructions(instructions: str, workdir: Path, run_dir: RunDir) -> str:
core/cap_evolve/harness.py:874:    _build_ledger(workdir, run_dir)
core/cap_evolve/harness.py:1252:    instructions = _augment_instructions(instructions, workdir, run_dir)

No stale 5-arg call sites on the merged tree.

…mory->prompt framing

Review fixes for #212:

1. cache.py — restore main's intro paragraph verbatim (the sentence #211
   rewrites) and confine the correction to a separate "Scope: GEPA only"
   paragraph, so the #211 conflict is a single textual hunk whose wrong
   resolution can no longer restore the false maybe_cached_score line — that
   line's removal now auto-merges outside the conflict region.
2. test_w1_engine.py — new guard pinning that no doc under core/ or skills/
   cites maybe_cached_score, so a revert of the docstring fix fails a test.
3. skillopt SKILL.md / concepts.md / skillopt.py — the rejected/history jsonl
   are dashboard audit records, write-only, never prompt input.
4. MemoryPanel.tsx — drop the "do-not-re-propose" framing this PR disproved.
Copilot AI review requested due to automatic review settings July 30, 2026 00:10

Copilot AI left a comment

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

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

Copilot was unable to review this pull request because the user who requested the review has reached their quota limit.

@OsherElhadad

Copy link
Copy Markdown
Collaborator Author

🔧 Review fixes

All 4 findings addressed. Commit 5e31cf4. Suite 180 (179 + 1 new guard), 0 failed.


⚠️ Merge resolution required — NONE for cache.py (structurally fixed)

Finding 1 is now moot: cache.py merges CLEAN against #211. No hand-resolution, no way to restore the bug.

I restored main's intro paragraph verbatim (the sentence #211 re-wraps) and confined my correction to a separate **Scope: GEPA only** paragraph. The two diffs no longer overlap textually, so git merges both facts additively:

$ git merge-tree --write-tree origin/fix/issue-111-gepa-cache-traces \
                              origin/refactor/issue-114-drop-write-only-memory
CLEAN (exit 0)          # was CONFLICT before this commit

Real trial merge main#211#212:

$ git merge origin/fix/issue-111-gepa-cache-traces      # clean
$ git merge origin/refactor/issue-114-drop-write-only-memory
 13 files changed, 103 insertions(+), 178 deletions(-)   # no conflict in cache.py

Merged cache.py carries both #211's rollout_file prose and #212's scope correction, and the false line is gone:

$ grep -rn maybe_cached_score core skills | grep -v test_w1_engine | wc -l
0

Note the mechanism: the maybe_cached_score sentence lives in the honesty-notes block, outside the paragraph #211 rewrites — so its deletion always auto-merged even before this change. The old conflict was only the intro paragraph; a "take incoming" resolution there could not have restored the flag line by itself. That said, the conflict is now gone entirely, so the trap is closed either way.

Merge order (restating the reviewer's revised order): #199#204#211#210#212. #212 stays last. Its only remaining conflict on the 4-way tree is the known one-liner at gepa.py:613-622 (keep #199's render_instructions, drop , rejected, history).


1. cache.py docstring collision with #211 — FIXED structurally

Above. Plus a guard test so a future revert of the docstring fails CI rather than shipping:

def test_no_docs_cite_a_nonexistent_cache_flag():
    """#114: ``cache.py``'s docstring long claimed the cache was gated behind a flag
    named ``maybe_cached_score`` — a function that exists nowhere. ..."""
    hits = [str(f) for pat in (...) for f in root.glob(pat)
            if f.resolve() != me and "maybe_cached_score" in f.read_text(errors="ignore")]
    assert hits == [], f"docs cite a nonexistent cache flag: {hits}"

Judged worth it, not over-engineering: this epic has shipped fictional-symbol docs three times now (#203's cap-evolve finalize, #217's stdout contract, this one). It's 8 lines, stdlib-only, no git dependency (a git grep would vacuously pass outside a repo).

Mutation-verified, not assumed. Re-added the lie to the docstring:

$ pytest core/tests/test_w1_engine.py -q -k nonexistent
FAILED core/tests/test_w1_engine.py::test_no_docs_cite_a_nonexistent_cache_flag
1 failed, 24 deselected in 0.05s
$ # reverted
1 passed, 24 deselected in 0.03s

2. Surfaces implying the memory feeds the loop — FIXED (3, not 2)

skills/algorithms/skillopt/SKILL.md:28 and references/concepts.md:38 fixed as suggested. The re-grep turned up a third the review missed — core/cap_evolve/skillopt.py:37, the module docstring listing RejectedMemory/History among the "honesty-critical" delegated steps. Same false implication, fixed the same way.

$ grep -rn -iE "RejectedMemory|memory.*(prompt|inject)|(prompt|inject).*memory" \
    --include="*.md" --include="*.py" --include="*.ts" --include="*.tsx" core skills dashboard
core/cap_evolve/harness.py:1358:    """Create the optimizer memory (rejected + accepted history) ...
core/cap_evolve/memory.py:1:"""Optimizer memory: append-only jsonl records of rejected + accepted candidates.
core/cap_evolve/memory.py:6:- ``RejectedMemory`` (``rejected.jsonl``) — every candidate the gate rejected, ...
core/cap_evolve/__init__.py:19,43                      # the exports themselves (nit 3)
core/cap_evolve/rundir.py:380 / dashboard.py:583       # scratch-file ignore lists — correct as-is
core/tests/test_store_memory.py:1 / test_core.py:12,136
dashboard/backend/capevolve_dashboard/memory.py:1      # the reader — correct as-is

Remaining hits are all either the class names themselves, the write-side docstrings (which memory.py's new header already scopes as write-only), scratch-file ignore lists, or the dashboard reader. No surviving surface claims the memory reaches a prompt. _augment_instructions remains the only path that does.


3. Nit — History/RejectedMemory still in __all__ — DECLINED

Agreeing with the reviewer's own read: removing a public name is a bigger blast radius than the 4 lines saved, and _init_memory_store imports from .memory directly either way. Out of scope for a pure-deletion PR.


4. Nit — MemoryPanel.tsx:10 "do-not-re-propose" — FIXED

The reviewer called it out of scope, but it's a doc comment and it was the last surviving instance of the framing this PR disproved. Zero-risk, so fixed:

-/** Optimizer memory: accepted history, rejected ("do-not-re-propose"), and the
+/** Run audit records: accepted history, rejected candidates (an audit trail — nothing
+ * re-reads it to avoid re-proposing; see core/cap_evolve/memory.py), and the

Verification

This branch

$ PYTHONPATH=/tmp/fx-212b/core python -m pytest core/tests -q
180 passed in 57.63s                       # 179 baseline + 1 new guard

$ PYTHONPATH=/tmp/fx-212b/core python -m pytest dashboard/backend/tests -q
42 passed, 1 warning in 2.10s              # warning = pre-existing StarletteDeprecationWarning

$ cd dashboard/frontend && npx vitest run
 Test Files  13 passed (13)
      Tests  45 passed (45)

$ python -m compileall -q core skills && echo COMPILEALL_CLEAN
COMPILEALL_CLEAN

#200's flaky test_dashboard_launch.py::test_maybe_launch_spawns_when_available passed.

#211 collision handled

$ git merge-tree --write-tree origin/fix/issue-111-gepa-cache-traces \
                              origin/refactor/issue-114-drop-write-only-memory
CLEAN (exit 0)

$ # real merge: main -> #211 -> #212
$ grep -rn maybe_cached_score core skills | grep -v test_w1_engine | wc -l
0
$ PYTHONPATH=/tmp/fx212-mt/core python -m pytest core/tests/test_w1_engine.py -q -k nonexistent
1 passed, 24 deselected in 0.05s

4-way merged tree (main + #199 + #204 + #212)

$ PYTHONPATH=/tmp/fx212-4w/core python -m pytest core/tests -q
204 passed in 74.43s                       # reviewer's 203 + my new guard
$ python -m compileall -q core skills && echo COMPILEALL_CLEAN
COMPILEALL_CLEAN
$ grep -rn maybe_cached_score core skills | grep -v test_w1_engine | wc -l
0

Conflicts on the way there were the pre-existing #199-vs-#204 pair (gepa.py/skillopt.py at the _init_memory_store(...) call, resolved additively keeping both algorithm= and ctx = ctx or OptimizerContext()) plus #212's one known gepa.py:613-622 hunk. cache.py did not conflict.

Live path still works after the doc edits

Real zero-API e2e (examples/toy_calc/run.sh, mock optimizer), then the endpoint:

$ python -m cap_evolve.cli run ...
"test_reward": 1.0, "test_delta": 1.0, "iterations": 3

$ GET /api/runs/run_demo/memory -> 200
history : [{"candidate_id": "cand_0001", "summary": "candidate cand_0001 (val 1.000, Δ +1.000)", "val": 1.0}]
rejected: [{"candidate_id": "cand_0002", "summary": "...", "reason": "paired Δ̄=+0.0000 <= 0 (SE=0 → STRICT fallback, warned; n=2)", "val": 1.0},
           {"candidate_id": "cand_0003", ...}]
key sets: hist=[['candidate_id','summary','val']]  rej=[['candidate_id','reason','summary','val']]

200, non-empty, exactly the 4 keys MemoryPanel + deadEnds read. Docs/docstrings only — no behavior change, as expected.

Files touched

File Change
core/cap_evolve/cache.py docstring reshaped so #211 merges clean
core/tests/test_w1_engine.py +8 — maybe_cached_score absence guard
core/cap_evolve/skillopt.py module docstring — write-only framing (3rd surface)
skills/algorithms/skillopt/SKILL.md audit-record framing
skills/algorithms/skillopt/references/concepts.md audit-record framing
dashboard/frontend/src/components/MemoryPanel.tsx doc comment — nit 4

OsherElhadad pushed a commit that referenced this pull request Jul 30, 2026
#212 narrows the signature (drops rejected/history). Passing them positionally made
this a THIRD mechanical merge site — and unlike the two in harness.py it surfaced as a
test failure git does not flag as a conflict. Fill trailing params reflectively so the
test passes on both signatures.
OsherElhadad pushed a commit that referenced this pull request Jul 30, 2026
Fourth mechanical merge site for #212's narrowed signatures (two in harness.py, two
here) — all in tests, so git flags none of them as conflicts. Also refresh the module
docstring for the renamed sections and the new pinned properties.
OsherElhadad pushed a commit that referenced this pull request Jul 30, 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.

Remove write-only optimizer-memory (memory.py) + unused params; fix misleading cache.py docstring

3 participants