Skip to content

feat(episode): record episodes in the run index - #457

Open
polakamtejas wants to merge 1 commit into
mainfrom
feat/episode-run-index
Open

polakamtejas wants to merge 1 commit into
mainfrom
feat/episode-run-index

Conversation

@polakamtejas

@polakamtejas polakamtejas commented Nov 3, 2026 •

Copy link
Copy Markdown
Collaborator

What

An episode now records itself in the run index, as a runner records the runs it accepts:

  • Before Task.run: a RunRecord with runner episode, status RUNNING, the task, its version and the instance id. It's written before the first await, so start() still returns with the run listed.
  • After teardown: mark_terminal with COMPLETED, FAILED and the error, or CANCELED and the abandon reason.
  • EpisodeSpec.start(run_group_id=...): files the run in that run group (overrides.metadata.run_group_id), the key the explorer groups a task's runs by. Without it, each run is its own group.

Why

The explorer's run lists read only the run index (agent_env.runner.store). An episode called Task.run directly, so its runs never appeared there: training episodes were reachable only by instance id. That holds for every trainer that plays through the Episode API, the OpenEnv-based helpers included. With a run group, a trainer can show a group of rollouts together, with their mean score.

The terminal status is written after teardown, as the cancelled record already is. A scored episode still answers result(teardown=False) at once (#393), and its run turns COMPLETED once its teardown is done.

Known limit

An episode in a trainer process that dies stays RUNNING in the index. LocalRunner marks its own interrupted runs failed on restart, and nothing does that for episodes yet.

Tests

  • New: run_test.py::test_an_episode_is_in_the_run_index_running_then_ended_in_the_group_it_was_started_in. It checks running with its group, then completed, cancelled with the reason, and failed with the error.
  • Affected suites: tst/unit/task, runner, explorer, openenv and training pass, 532 of 532, on five runs. One run right after uv sync had a single failure that didn't recur, likely a timing flake; the log didn't keep which test.
  • End to end: Tinker training on a local bundle wrote its episodes here with run groups, and the explorer's task page listed them grouped by training step. The Tinker PR stacked on this one uses run_group_id.

🤖 Generated with Claude Code

RetriggerView in GreptileConfidence Score: 3/5

Fix the incorrect completed status and unsupported explorer cancellation before merging.

Fix All in CursorFindings

  1. P1 Failed episodes look successful ▶
  2. P1 Cancel cannot stop episodes ▶
  3. P2 Failed starts retain episodes ▶
Fix with agent prompt
### Issue 1
src/agent_env/task/episode/run.py:287-289
`_outcome` records `COMPLETED` when a step fails with `fail_task_on_error=False`. In that case, `Task.run()` returns without raising and leaves the failure in `failed_steps`. The explorer therefore counts a failed episode as completed.

After the cancellation checks, use `run_outcome(self._context, failure)` so episodes follow the same rule as other runners. Add a test for a failed step that allows the task to continue.

### Issue 2
src/agent_env/task/episode/run.py:398-400
The newly listed episodes get a Cancel button, but the explorer sends their `run_id` to `get_runner().cancel()`. With `LocalRunner`, cancellation always returns false because episodes never enter its `_inflight` map. The episode keeps running, and the page eventually asks the user to retry.

Either route cancellation to the episode's owner or hide cancellation for these records.

### Issue 3
src/agent_env/task/episode/run.py:257
`_record_run` runs before the cleanup `try`. If the store raises during insertion, `_drive` exits without removing the episode from `_live`. Since `running` was already set, `start()` still returns an episode even though its task never started. Repeated failed starts retain more unused episodes in memory.

Protect registration with cleanup and pass the insertion error back through `start()`.

---

For each issue above, determine whether it is valid and should be fixed. If so, fix it directly.

Summary

Episodes now appear in the run index and accept an optional run_group_id.

  • Episodes now appear in explorer run lists and can be grouped.

Acknowledged limit: polakamtejas states that episodes left by a dead trainer process remain running because episode recovery is deferred.

Diagram

sequenceDiagram
    participant Caller
    participant Episode
    participant Index as Run index
    participant Task
    participant Explorer
    Caller->>Episode: start(run_group_id)
    Episode->>Index: Insert RUNNING record
    Episode->>Task: run()
    Episode-->>Caller: Episode handle
    Task-->>Episode: Finish or raise
    Episode->>Task: Tear down resources
    Episode->>Index: Write terminal status
    Explorer->>Index: Read runs and groups
Loading

Reviews (1) · Last reviewed commit: "feat(episode): record episodes in the ru..." · Reviewed by Greptile

An episode called Task.run directly, so the run index never saw it and the explorer's run lists, which read only the
index, never showed it: training episodes were reachable only by instance id. An episode now records itself as a
runner records the runs it accepts (runner `episode`): RUNNING before Task.run, then COMPLETED, FAILED with the error,
or CANCELED with the abandon reason once its teardown is done. `EpisodeSpec.start(run_group_id=...)` files it in a
run group, so a trainer can show a group of rollouts together.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
Comment on lines +287 to +289
if failure is not None:
return RunStatus.FAILED, f"{type(failure).__name__}: {failure}"
return RunStatus.COMPLETED, None

Copy link
Copy Markdown

Choose a reason for hiding this comment

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

P1 Failed episodes look successful

_outcome records COMPLETED when a step fails with fail_task_on_error=False. In that case, Task.run() returns without raising and leaves the failure in failed_steps. The explorer therefore counts a failed episode as completed.

After the cancellation checks, use run_outcome(self._context, failure) so episodes follow the same rule as other runners. Add a test for a failed step that allows the task to continue.

Knowledge Base Used: Task orchestration and execution lifecycle

Prompt To Fix With AI
This is a comment left during a code review.
Path: src/agent_env/task/episode/run.py
Line: 287-289

Comment:
**Failed episodes look successful**

`_outcome` records `COMPLETED` when a step fails with `fail_task_on_error=False`. In that case, `Task.run()` returns without raising and leaves the failure in `failed_steps`. The explorer therefore counts a failed episode as completed.

After the cancellation checks, use `run_outcome(self._context, failure)` so episodes follow the same rule as other runners. Add a test for a failed step that allows the task to continue.

**Knowledge Base Used:** [Task orchestration and execution lifecycle](https://app.greptile.com/-/switch-tenant?tenant=scale-ai&next=%2Fcustom-context%2Fknowledge-base%2Fscaleapi%2Fagentenv-framework%2F-%2Fdocs%2Ftask-orchestration.md)

---

For each issue above, determine whether it is valid and should be fixed. If so, fix it directly.

Fix in Cursor Fix in Claude Code Fix in Codex

Comment on lines +398 to +400
run_store.insert_run(RunRecord(
run_id=_run_id(instance_id), runner="episode", task_id=task.id, task_version=task.version,
instance_id=instance_id, status=RunStatus.RUNNING, started_at_utc=now,

Copy link
Copy Markdown

Choose a reason for hiding this comment

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

P1 Cancel cannot stop episodes

The newly listed episodes get a Cancel button, but the explorer sends their run_id to get_runner().cancel(). With LocalRunner, cancellation always returns false because episodes never enter its _inflight map. The episode keeps running, and the page eventually asks the user to retry.

Either route cancellation to the episode's owner or hide cancellation for these records.

Knowledge Base Used: Runtime explorer APIs and user interface

Prompt To Fix With AI
This is a comment left during a code review.
Path: src/agent_env/task/episode/run.py
Line: 398-400

Comment:
**Cancel cannot stop episodes**

The newly listed episodes get a Cancel button, but the explorer sends their `run_id` to `get_runner().cancel()`. With `LocalRunner`, cancellation always returns false because episodes never enter its `_inflight` map. The episode keeps running, and the page eventually asks the user to retry.

Either route cancellation to the episode's owner or hide cancellation for these records.

**Knowledge Base Used:** [Runtime explorer APIs and user interface](https://app.greptile.com/-/switch-tenant?tenant=scale-ai&next=%2Fcustom-context%2Fknowledge-base%2Fscaleapi%2Fagentenv-framework%2F-%2Fdocs%2Fruntime-explorer.md)

---

For each issue above, determine whether it is valid and should be fixed. If so, fix it directly.

Fix in Cursor Fix in Claude Code Fix in Codex

resolves."""
running.set()
with namespace_routing():
_record_run(self._task, self.id, self._context.metadata) # before any await: start() returns it listed

Copy link
Copy Markdown

Choose a reason for hiding this comment

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

P2 Failed starts retain episodes

_record_run runs before the cleanup try. If the store raises during insertion, _drive exits without removing the episode from _live. Since running was already set, start() still returns an episode even though its task never started. Repeated failed starts retain more unused episodes in memory.

Protect registration with cleanup and pass the insertion error back through start().

Prompt To Fix With AI
This is a comment left during a code review.
Path: src/agent_env/task/episode/run.py
Line: 257

Comment:
**Failed starts retain episodes**

`_record_run` runs before the cleanup `try`. If the store raises during insertion, `_drive` exits without removing the episode from `_live`. Since `running` was already set, `start()` still returns an episode even though its task never started. Repeated failed starts retain more unused episodes in memory.

Protect registration with cleanup and pass the insertion error back through `start()`.

---

For each issue above, determine whether it is valid and should be fixed. If so, fix it directly.

Fix in Cursor Fix in Claude Code Fix in Codex

This branch has not been deployed

No deployments
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.

1 participant