Skip to content

feat(runner): persist and resume partial sample outcomes across aborted runs - #52

Closed
jamespsterling wants to merge 2 commits into
mainfrom
devin/1787595061-partial-outcome-flush
Closed

feat(runner): persist and resume partial sample outcomes across aborted runs#52
jamespsterling wants to merge 2 commits into
mainfrom
devin/1787595061-partial-outcome-flush

Conversation

@jamespsterling

Copy link
Copy Markdown

TL;DR

Adds an optional PartialOutcomeStoreService so an aborted run (e.g. Cloud Run SIGTERM) flushes completed sample outcomes durably, and the retry skips those sample/epochs and aggregates the combined result.

What changed?

  • src/results/partial-outcome-store.ts (new): PartialOutcomeStoreService contract (read/write/remove) plus Zod schemas (SampleOutcomeSchema, PartialOutcomesSchema) to validate persisted JSON at runtime. Exported as ./partial-outcome-store.
  • src/harness/run.ts:
    • RunConfig gains skipSampleEpochs?: ReadonlySet<string> and onOutcome?: (outcome: SampleOutcome) => void.
    • New helpers sampleEpochKey(sampleId, epoch) and aggregateOutcomes(outcomes) (loop-based fold into the existing FoldAccumulator/finalizeRun).
    • The sample-epoch stream filters out already-completed keys; each completed outcome invokes onOutcome before progress reporting.
  • src/runner/run-by-id.ts: partialOutcomeStore? on the run input. Flow:
    1. read() prior outcomes (best-effort) and build the skip set.
    2. Run with skipSampleEpochs + collect new outcomes via onOutcome.
    3. On abort (abortSignal.aborted and new outcomes exist): write() prior + new outcomes, then rethrow/return the abort as before.
    4. On success: aggregate prior + new outcomes into the final RunResult and remove() the partial object.
    • Failures other than abort leave existing partial data untouched.

Why?

Cloud Run gives ~10s between SIGTERM and SIGKILL. Without a durable flush, every completed-but-unpersisted sample in an in-flight chunk is lost and re-run on retry. With this, the retry only reruns missing sample/epochs. Cancellation semantics are unchanged — an aborted run still surfaces as aborted; the flush is a side effect.

How to test

  • bun test src/harness/run.test.ts src/results/partial-outcome-store.test.ts — covers skip-set filtering, onOutcome invocation, aggregation, and schema accept/reject.
  • Simulate resume: run with a store whose read() returns prior outcomes; observe skipped sample/epochs in the run and a final result aggregating both sets, followed by remove().

Benchmark impact

No score changes for uninterrupted runs (store is optional and unused by default). Resumed runs aggregate persisted + new outcomes identically to a single full run over the same sample/epochs.

Reviewer focus

  • Abort-flush condition in run-by-id.ts (only when abort signal fired and new outcomes exist; write errors are logged, never masked into a different failure).
  • sampleEpochKey identity — stable across attempts so skip sets are safe.

Checklist

  • Tests cover changed behavior
  • Public API or configuration changes are backward compatible, or the break is documented
  • Benchmark changes document dataset provenance and licensing
  • No credentials, private results, or restricted dataset contents are included
  • Documentation is updated where needed

Link to Devin session: https://openrouter.devinenterprise.com/sessions/4899435489c14bba8242921472c31807
Requested by: @jamespsterling

…ed runs

Co-Authored-By: Devin AI <158243242+devin-ai-integration[bot]@users.noreply.github.com>
@devin-ai-integration

Copy link
Copy Markdown
Contributor
Original prompt from James

I want to explore using SIGTERM/SIGKILL to alleviate the heartbeat timeouts on the temporal benchmarking workflows. What can we do to quickly stop & resume the benchmark within ~10s using SIGTERM/SIGKILL hooks? I want the most minimally invasive path if possible to test this. do we need a parallel activity with a short polling interval that can just listen for this event given it's scoped to like 10s? then this activity can signal to the long-running process to stop or even just collect what finished benchmarking chunks we have and terminate it if not stopped quickly enough?

Cloud Run sends a SIGTERM signal to your container to start a 10-second graceful shutdown window before forcing termination with SIGKILL. [1]
How Shutdown Signals Work

  • SIGTERM (Signal 15): Sent to notify your container instance that it will shut down. Your application has 10 seconds to finish active requests, close database connections, and flush logs. [1, 2]
  • SIGKILL (Signal 9): Sent immediately if your container is still running after the 10-second grace period ends. This signal cannot be caught or blocked, resulting in an abrupt stop. [1, 2]
  • Environment behavior: If you use the first-generation environment and do not trap SIGTERM, the instance shuts down instantly. Second-generation environments strongly recommend installing a signal handler. [1]
    Best Practices for Handling Signals
  • Catch SIGTERM: Write a signal handler in your application ... (734 chars truncated...)

@devin-ai-integration

Copy link
Copy Markdown
Contributor

🤖 Devin AI Engineer

I'll be helping with this pull request! Here's what you should know:

✅ I will automatically:

  • Address comments on this PR that start with 'DevinAI' or '@devin'.
  • Look at CI failures and help fix them

Note: I can only respond to comments from users who have write access to this repository.

⚙️ Control Options:

  • Disable automatic comment, CI, and merge conflict monitoring

@devin-ai-integration devin-ai-integration Bot 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.

✅ Devin Review: No Issues Found

Devin Review analyzed this PR and found no potential bugs to report.

View in Devin Review to see 1 additional finding.

Open in Devin Review

@jamespsterling
jamespsterling marked this pull request as ready for review August 24, 2026 21:50
@jamespsterling
jamespsterling requested a review from a team as a code owner August 24, 2026 21:50
Comment thread src/harness/run.ts Outdated
Comment thread src/runner/run-by-id.ts
Comment thread src/runner/run-by-id.ts Outdated
Comment thread src/runner/run-by-id.ts Outdated
Comment thread src/results/partial-outcome-store.ts
- report absolute progress counts when resuming from skipped sample-epochs
- persist and validate the run scope (epochs/range) with partial outcomes,
  discarding persisted outcomes from a mismatched scope
- remove the partial store only after final result persistence succeeds
- log partial-store read failures instead of silently starting fresh
- link the persisted schemas to the source types via ZodShape and cover
  optional fields (trajectory, responseItems, requestBody, metadata,
  serverToolUse, reasoningTokens) in round-trip tests

Co-Authored-By: Devin AI <158243242+devin-ai-integration[bot]@users.noreply.github.com>
@devin-ai-integration

Copy link
Copy Markdown
Contributor

Closing in favor of #48 (per-sample write-through persistence), based on an empirical head-to-head of both resume strategies on the same deterministic workload:

Findings

Worth porting into #48 (its write failures are currently silently swallowed):

  • explicit wLog/eLog on partial-store write failures (this PR logs Failed to flush partial benchmark outcomes after abort);
  • the scope guard (epochs/range) so stale persisted outcomes from a different run scope are never reused.

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