Skip to content

feat(eval): freeze cell identity and resume semantics before stable cut - #436

Merged
solomonneas merged 4 commits into
mainfrom
feat/eval-cell-identity-freeze
Jul 22, 2026
Merged

feat(eval): freeze cell identity and resume semantics before stable cut#436
solomonneas merged 4 commits into
mainfrom
feat/eval-cell-identity-freeze

Conversation

@solomonneas

@solomonneas solomonneas commented Jul 22, 2026

Copy link
Copy Markdown
Collaborator

Closes #434

Landed

  • docs/phase-eval-cell-identity.md: tracked semantics doc stating the exact cell_id identity field set, per-state resume rules (including running), the keep-and-report stale-cell policy, the manifest_digest attribution field, and the rule that any identity payload change requires a CELL_SCHEMA bump plus a migration note.
  • Identity lock test: snapshot-style test asserting the exact identity payload keys and the resulting cell_id hex from a fixed manifest/roster fixture.
  • Line-ending normalization: \r\n and \r normalize to \n in prompt text before it enters the identity payload, so the same logical manifest hashes identically across checkouts. Changes ids only for manifests that contained CRLF/CR; documented as acceptable before stable.
  • manifest_digest is recorded in every cell.json at both write sites (the running marker and the final payload).
  • Attempt numbering is max(existing attempt numbers) + 1 over both the attempt-NNN directories and the attempt value recorded in cell.json, tolerating gaps and non-matching directories; deleting even the highest attempt directory never reuses a number. Tested with a gapped attempts/ directory and with a deleted highest attempt.
  • Resume visibility: execute --resume prints a one-line stale-cell count to stderr when the plan has stale cells.
  • Manifest-edit-under-resume test: unchanged cell is skipped, edited cell re-runs under a new cell_id, old cell appears in summary stale_counts.
  • Kill-mid-run test: a cell left in running re-runs on resume as a new attempt.

Deferred (per issue, not in this PR)

  • The concurrency lockfile / per-cell claim guard for two execute processes on one output dir. The doc states the current answer: concurrent executes are unguarded; running is treated as crash-on-resume.

Verify

Gate: brigade work verify run --target . --command "./scripts/verify" --capture brigade-work, run 20260722-164001-work-verify-292c12 on a clean checkout of this branch (receipt preserved at .brigade/work/verify-runs/20260722-164001-work-verify-292c12/receipt.json):

status: completed
- ./scripts/verify [completed] exit=0
Required test coverage of 78% reached. Total coverage: 82.61%
3708 passed, 3 skipped in 358.74s (0:05:58)

Main-worktree loop gate: brigade work verify run --target . --command ".venv/bin/python -m pytest tests/test_model_trials.py -q" --capture brigade-work, run 20260722-163934-work-verify-a6f513: exit=0, 17 passed.

Note: the full ./scripts/verify against the main working tree fails at ruff format --check with Would reformat: src/brigade/component_install.py — an unrelated uncommitted WIP file already dirty before this branch; this PR does not touch it.

Summary by CodeRabbit

  • Bug Fixes

    • Improved resume behavior so interrupted trials continue with the correct next attempt number.
    • Changed case prompts with different line endings to produce consistent results.
    • Resume now detects edited cases, reruns only affected cells, and preserves stale results.
    • Added clearer reporting when stale cells are retained during resume.
    • Trial metadata now retains manifest information for more reliable tracking.
  • Tests

    • Added coverage for deterministic cell identity, resume scenarios, interrupted runs, and attempt numbering.

- Document identity fields, per-state resume rules, and stale-cell
  keep-and-report policy in docs/phase-eval-cell-identity.md
- Snapshot-lock the identity payload keys and cell_id digest in tests
- Normalize CRLF/CR prompt line endings before identity hashing
- Record manifest_digest in every cell.json (running and final writes)
- Number attempts as max(existing) + 1, tolerating gaps and foreign dirs
- Print stale-cell count to stderr on execute --resume
- Cover manifest-edit-under-resume and kill-mid-run resume in tests
@coderabbitai

coderabbitai Bot commented Jul 22, 2026

Copy link
Copy Markdown

Review Change Stack

Note

Reviews paused

It looks like this branch is under active development. To avoid overwhelming you with review comments due to an influx of new commits, CodeRabbit has automatically paused this review. You can configure this behavior by changing the reviews.auto_review.auto_pause_after_reviewed_commits setting.

Use the following commands to manage reviews:

  • @coderabbitai resume to resume automatic reviews.
  • @coderabbitai review to trigger a single review.

Use the checkboxes below for quick actions:

  • ▶️ Resume reviews
  • 🔍 Trigger review

No actionable comments were generated in the recent review. 🎉

ℹ️ Recent review info
⚙️ Run configuration

Configuration used: Repository: escoffier-labs/coderabbit/.coderabbit.yaml

Review profile: CHILL

Plan: Pro Plus

Run ID: c4742b2e-d522-4dc3-a2c3-c63ea9b64dab

📥 Commits

Reviewing files that changed from the base of the PR and between c0e69da and 0246cae.

⛔ Files ignored due to path filters (1)
  • docs/phase-eval-cell-identity.md is excluded by !docs/**, !**/docs/**
📒 Files selected for processing (2)
  • src/brigade/model_trials.py
  • tests/test_model_trials.py

📝 Walkthrough

Walkthrough

Model trials now normalize prompt line endings for stable cell identity, persist manifest digests in cell metadata, report stale cells during resume, and derive attempt numbers from the highest recorded attempt rather than directory counts.

Changes

Model trial identity and resume

Layer / File(s) Summary
Prompt identity normalization
src/brigade/model_trials.py, tests/test_model_trials.py
Inline and file-based prompts normalize CRLF/CR endings to LF before identity generation; tests lock the canonical payload and resulting cell_id.
Manifest-aware resume tracking
src/brigade/model_trials.py, tests/test_model_trials.py
Cell records persist manifest_digest; resume reports stale cells and reruns only cells affected by manifest edits.
Attempt allocation for resumed cells
src/brigade/model_trials.py, tests/test_model_trials.py
Attempt numbering uses the highest matching directory or recorded attempt plus one, including gaps, deleted attempts, and running-cell markers.

Estimated code review effort: 3 (Moderate) | ~25 minutes

Sequence Diagram(s)

sequenceDiagram
  participant User
  participant execute
  participant build_plan
  participant CellStorage
  User->>execute: resume trial
  execute->>build_plan: rebuild manifest plan
  build_plan->>execute: return cells and stale_cells
  execute->>CellStorage: read prior cell records
  execute->>CellStorage: persist digest and new attempt
  execute-->>User: report stale cell count
Loading

Possibly related issues

  • #434: The changes implement the linked identity and resume semantics, including prompt normalization, stale-cell reporting, durable manifest attribution, and max-plus-one attempt numbering.
🚥 Pre-merge checks | ✅ 4 | ❌ 1

❌ Failed checks (1 inconclusive)

Check name Status Explanation Resolution
Linked Issues check ❓ Inconclusive Most requirements are covered, but the tracked semantics doc at docs/phase-eval-cell-identity.md was excluded by the !docs/** path filter, so full compliance cannot be verified. Restore the doc to the reviewable set or provide its contents so the identity and resume contract can be verified.
✅ Passed checks (4 passed)
Check name Status Explanation
Description Check ✅ Passed Check skipped - CodeRabbit’s high-level summary is enabled.
Title check ✅ Passed The title clearly matches the PR’s main change: freezing eval cell identity and resume semantics.
Out of Scope Changes check ✅ Passed The summarized code and tests all align with the linked issue scope, with no unrelated changes evident.
Docstring Coverage ✅ Passed No functions found in the changed files to evaluate docstring coverage. Skipping docstring coverage check.
✨ Finishing Touches
📝 Generate docstrings
  • Create stacked PR
  • Commit on current branch
🧪 Generate unit tests (beta)
  • Create PR with unit tests
  • Commit unit tests in branch feat/eval-cell-identity-freeze

Comment @coderabbitai help to get the list of available commands.

@greptile-apps

greptile-apps Bot commented Jul 22, 2026

Copy link
Copy Markdown

Greptile Summary

This PR defines stable evaluation-cell identity and resume behavior. The main changes are:

  • Canonical cell identity rules and line-ending normalization.
  • Manifest attribution for running and final cell receipts.
  • Monotonic attempt numbering based on directories and the recorded attempt.
  • Stale-cell reporting and resume tests for edited or interrupted runs.

Confidence Score: 5/5

This looks safe to merge.

  • The recorded attempt preserves the high-water mark when the highest attempt directory is deleted.
  • Existing attempt directories remain part of the next-number calculation.
  • No blocking issue remains in the updated attempt-allocation path.

Important Files Changed

Filename Overview
src/brigade/model_trials.py Normalizes prompts, records manifest digests, reports stale cells, and preserves attempt-number progression after deletion.
tests/test_model_trials.py Adds tests for identity stability, attempt numbering, stale cells, manifest edits, and interrupted-run resume behavior.
docs/phase-eval-cell-identity.md Defines the cell identity, resume, attempt numbering, stale-cell, and concurrency contracts.

Reviews (3): Last reviewed commit: "fix(eval): ignore non-positive recorded ..." | Re-trigger Greptile

Comment thread src/brigade/model_trials.py
@solomonneas solomonneas self-assigned this Jul 22, 2026
@solomonneas solomonneas added the coderabbit-review Request one CodeRabbit review after CI is green label Jul 22, 2026
Review feedback on #436: max(attempt dirs) + 1 reuses a number when the
highest attempt directory is deleted. Take the next attempt as max over
both the attempt-NNN directories and the attempt value recorded in
cell.json, and document the mechanism.
coderabbitai[bot]
coderabbitai Bot previously requested changes Jul 22, 2026

@coderabbitai coderabbitai Bot left a comment

Copy link
Copy Markdown

Choose a reason for hiding this comment

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

Actionable comments posted: 1

🤖 Prompt for all review comments with AI agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.

Inline comments:
In `@src/brigade/model_trials.py`:
- Around line 329-341: Update _attempt_number to include the valid attempt value
from the persisted nonterminal cell.json marker when calculating the next
attempt, even if no attempt directory exists; preserve directory-based numbering
and default behavior otherwise. Add a regression test covering a running marker
with no attempt directory and verify that the next attempt number is allocated
without reuse.
🪄 Autofix (Beta)

Fix all unresolved CodeRabbit comments on this PR:

  • Push a commit to this branch (recommended)
  • Create a new PR with the fixes

ℹ️ Review info
⚙️ Run configuration

Configuration used: Repository: escoffier-labs/coderabbit/.coderabbit.yaml

Review profile: CHILL

Plan: Pro Plus

Run ID: 34c935ee-2d27-4c35-87b2-e00694612c4d

📥 Commits

Reviewing files that changed from the base of the PR and between c0e69da and aaba5d1.

⛔ Files ignored due to path filters (1)
  • docs/phase-eval-cell-identity.md is excluded by !docs/**, !**/docs/**
📒 Files selected for processing (2)
  • src/brigade/model_trials.py
  • tests/test_model_trials.py

Comment on lines +329 to +341
_ATTEMPT_DIR = re.compile(r"attempt-(\d+)")


def _attempt_number(cell_dir: Path) -> int:
attempts = cell_dir / "attempts"
existing = [p for p in attempts.iterdir() if p.is_dir()] if attempts.is_dir() else []
return len(existing) + 1
if not attempts.is_dir():
return 1
numbers = [
int(match.group(1))
for entry in attempts.iterdir()
if entry.is_dir() and (match := _ATTEMPT_DIR.fullmatch(entry.name)) is not None
]
return max(numbers, default=0) + 1

Copy link
Copy Markdown

Choose a reason for hiding this comment

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

🎯 Functional Correctness | 🟠 Major | ⚡ Quick win

Account for a durable running marker with no attempt directory.

cell.json is persisted as running before aboyeur.run can create attempt-<n>/run. A crash in that window leaves no matching directory, so resume returns 1 and reuses attempt 1 instead of allocating a new attempt. Include the previous nonterminal marker’s valid attempt in the maximum, and add a regression test for this crash window.

Proposed fix
-def _attempt_number(cell_dir: Path) -> int:
+def _attempt_number(cell_dir: Path, *, prior_attempt: int | None = None) -> int:
     attempts = cell_dir / "attempts"
-    if not attempts.is_dir():
-        return 1
     numbers = [
         int(match.group(1))
         for entry in attempts.iterdir()
-        if entry.is_dir() and (match := _ATTEMPT_DIR.fullmatch(entry.name)) is not None
-    ]
+        if attempts.is_dir()
+        and entry.is_dir()
+        and (match := _ATTEMPT_DIR.fullmatch(entry.name)) is not None
+    ] if attempts.is_dir() else []
+    if isinstance(prior_attempt, int) and not isinstance(prior_attempt, bool) and prior_attempt > 0:
+        numbers.append(prior_attempt)
     return max(numbers, default=0) + 1
📝 Committable suggestion

‼️ IMPORTANT
Carefully review the code before committing. Ensure that it accurately replaces the highlighted code, contains no missing lines, and has no issues with indentation. Thoroughly test & benchmark the code to ensure it meets the requirements.

Suggested change
_ATTEMPT_DIR = re.compile(r"attempt-(\d+)")
def _attempt_number(cell_dir: Path) -> int:
attempts = cell_dir / "attempts"
existing = [p for p in attempts.iterdir() if p.is_dir()] if attempts.is_dir() else []
return len(existing) + 1
if not attempts.is_dir():
return 1
numbers = [
int(match.group(1))
for entry in attempts.iterdir()
if entry.is_dir() and (match := _ATTEMPT_DIR.fullmatch(entry.name)) is not None
]
return max(numbers, default=0) + 1
_ATTEMPT_DIR = re.compile(r"attempt-(\d+)")
def _attempt_number(cell_dir: Path, *, prior_attempt: int | None = None) -> int:
attempts = cell_dir / "attempts"
numbers = [
int(match.group(1))
for entry in attempts.iterdir()
if attempts.is_dir()
and entry.is_dir()
and (match := _ATTEMPT_DIR.fullmatch(entry.name)) is not None
] if attempts.is_dir() else []
if isinstance(prior_attempt, int) and not isinstance(prior_attempt, bool) and prior_attempt > 0:
numbers.append(prior_attempt)
return max(numbers, default=0) + 1
🤖 Prompt for AI Agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.

In `@src/brigade/model_trials.py` around lines 329 - 341, Update _attempt_number
to include the valid attempt value from the persisted nonterminal cell.json
marker when calculating the next attempt, even if no attempt directory exists;
preserve directory-based numbering and default behavior otherwise. Add a
regression test covering a running marker with no attempt directory and verify
that the next attempt number is allocated without reuse.

Copy link
Copy Markdown
Collaborator Author

Choose a reason for hiding this comment

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

Already addressed, plus the requested regression test. The fix landed in 683d6cd: _attempt_number reads the persisted attempt from cell.json (including a nonterminal running marker) and takes the max with the attempt-NNN directories, so the crash window you describe (running marker, no attempt dir) allocates a new number instead of reusing attempt 1. a7f4b84 adds test_attempt_number_counts_running_marker_without_attempt_dir pinning exactly that window: cell.json with state: running, attempt: 1, no attempts/ directory, next attempt is 2. (Used the existing read-cell.json-inside-_attempt_number shape rather than the proposed prior_attempt parameter; same coverage, one less caller-side contract.)

@solomonneas

Copy link
Copy Markdown
Collaborator Author

Review-response pass complete (683d6cd).

Addressed

  • Greptile P1 (attempt-number reuse after deleting the highest attempt dir): valid. _attempt_number now computes max over both the attempt-NNN directories and the attempt value persisted in cell.json, closing the reuse gap with already-persisted state. Covered by test_attempt_number_does_not_reuse_deleted_highest_attempt; the no-reuse mechanism is now documented in docs/phase-eval-cell-identity.md. Replied on the thread.

Rejected: none — the Greptile finding was the only one received.

CodeRabbit: no CodeRabbit review or comments had posted after re-checking every 2 minutes for 20 minutes, so this pass proceeded with the Greptile feedback only.

PR body correction: the Verify section previously cited a gate run whose receipt lived only in a throwaway checkout. Re-ran the full gate on a clean checkout of 683d6cd and preserved the receipt in the repo: 20260722-164001-work-verify-292c12./scripts/verify [completed] exit=0, 3708 passed, 3 skipped, coverage 82.61%. Main-worktree loop gate 20260722-163934-work-verify-a6f513 (pytest tests/test_model_trials.py): exit=0, 17 passed. The PR body now cites both.

CodeRabbit review on #436: pin the crash window where cell.json records
a running attempt but no attempt-NNN directory exists yet, so resume
allocates the next number instead of reusing it.
@solomonneas

Copy link
Copy Markdown
Collaborator Author

CodeRabbit review arrived at 16:40 UTC (after the earlier 20-minute poll window closed) — one Major finding on _attempt_number: a crash between the running cell.json write and attempt-dir creation would reuse attempt 1 on resume.

Status: already fixed by 683d6cd, which reads the persisted attempt from cell.json (including nonterminal running markers) when computing the next number. Replied on the thread. a7f4b84 adds the requested regression test for that exact crash window (test_attempt_number_counts_running_marker_without_attempt_dir).

Loop gate after the test addition: 20260722-170324-work-verify-e0f0fd — pytest tests/test_model_trials.py, exit=0, 18 passed.

A corrupt-but-valid-JSON cell.json marker (attempt -1 or 0) would lower
the high-water mark and produce attempt-000, violating the 1-based
numbering contract. Only positive integers count.
@solomonneas

Copy link
Copy Markdown
Collaborator Author

Additional local review pass with codex review --base main:

  • First run against the main worktree also picked up the unrelated parked WIP (agent-notify component work); its findings concern those files, not this PR — flagging them here only so the WIP owner is aware (rollback across the 4→5 component upgrade, beta manifest compat, Rosetta smoke gap, plus WIP test failures).
  • Re-ran against a clean checkout of this branch: one valid P2. A corrupt-but-valid-JSON cell.json marker (attempt: -1 or 0) would lower the high-water mark and yield attempt-000, breaking the 1-based numbering contract. Fixed in 0246cae: only positive integers count as the persisted high-water mark, with test_attempt_number_ignores_nonpositive_recorded_attempt pinning both cases.

No other findings on the PR diff. Loop gate after the fix: 20260722-192818-work-verify-4242e4 — pytest tests/test_model_trials.py, exit=0, 19 passed; ruff and mypy clean.

@solomonneas

Copy link
Copy Markdown
Collaborator Author

@coderabbitai review

@coderabbitai

coderabbitai Bot commented Jul 22, 2026

Copy link
Copy Markdown
✅ Action performed

Review finished.

Note: CodeRabbit is an incremental review system and does not re-review already reviewed commits. This command is applicable only when automatic reviews are paused.

@solomonneas
solomonneas dismissed coderabbitai[bot]’s stale review July 22, 2026 20:19

Findings addressed: _attempt_number reads the persisted cell.json attempt high-water mark (683d6cd), regression tests added (a7f4b84, 0246cae), incremental CodeRabbit re-review on 2026-07-22 reported no new comments.

@solomonneas
solomonneas merged commit 645ff6a into main Jul 22, 2026
29 checks passed
@solomonneas
solomonneas deleted the feat/eval-cell-identity-freeze branch July 26, 2026 20:37
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

coderabbit-review Request one CodeRabbit review after CI is green

Projects

None yet

Development

Successfully merging this pull request may close these issues.

Freeze eval cell identity and resume semantics before the stable cut

1 participant