Skip to content

Merge concurrent experiment state rebases in push_experiments_state - #49631

Merged
pelikhan merged 11 commits into
mainfrom
copilot/aw-failures-fix-push-experiments-state
Aug 2, 2026
Merged

Merge concurrent experiment state rebases in push_experiments_state#49631
pelikhan merged 11 commits into
mainfrom
copilot/aw-failures-fix-push-experiments-state

Conversation

Copilot AI commented Aug 1, 2026

Copy link
Copy Markdown
Contributor

push_experiments_state was treating state.json rebase conflicts as terminal failures, which caused concurrent runs writing to the same experiments/<workflow> branch to drop state updates. This change makes that shared path merge experiment state during rebase instead of aborting on expected writer races.

  • Rebase conflict recovery in the shared push path

    • Added a conflict-resolution hook to signed commit replay so callers can recover from specific rebase conflicts before falling back to failure.
    • Kept existing behavior for unrelated conflicts and partial-clone recovery paths.
  • Field-level merge for experiment state

    • Taught push_experiment_state to resolve state.json as a 3-way JSON merge using base/remote/local versions from the rebase index.
    • Numeric counters are merged additively, preserving concurrent increments instead of letting one writer win.
    • runs history is unioned/deduplicated so both runs remain represented.
  • Assignments handling

    • When assignments.json is part of the same conflict set, the resolver keeps the local/current-run version while continuing the rebase.
  • Coverage for the race

    • Added focused unit coverage for the JSON merge helper.
    • Added an integration test that simulates two writers rebasing onto the same experiments branch and verifies the merged payload includes both updates.

Example of the merge behavior:

// base
{ "counts": { "prompt_style": { "concise": 1, "detailed": 1 } } }

// remote
{ "counts": { "prompt_style": { "concise": 2, "detailed": 1 } } }

// local
{ "counts": { "prompt_style": { "concise": 2, "detailed": 1 } } }

// merged
{ "counts": { "prompt_style": { "concise": 3, "detailed": 1 } } }

Generated by 👨‍🍳 PR Sous Chef · gpt54 · 10.4 AIC · ⌖ 6.45 AIC · ⊞ 8.2K ·
Comment /souschef to run again


Generated by 👨‍🍳 PR Sous Chef · gpt54 · 14.5 AIC · ⌖ 7.61 AIC · ⊞ 8.2K ·
Comment /souschef to run again

Co-authored-by: pelikhan <4175913+pelikhan@users.noreply.github.com>
Co-authored-by: pelikhan <4175913+pelikhan@users.noreply.github.com>
Copilot AI changed the title [WIP] Fix push_experiments_state to merge state.json field-by-field Merge concurrent experiment state rebases in push_experiments_state Aug 1, 2026
Copilot AI requested a review from pelikhan August 1, 2026 20:31
@pelikhan

pelikhan commented Aug 1, 2026

Copy link
Copy Markdown
Collaborator

@copilot add new file format that uses .jsonl file to avoid merge issues. Support both old and new format:

Co-authored-by: pelikhan <4175913+pelikhan@users.noreply.github.com>

Copilot AI commented Aug 1, 2026

Copy link
Copy Markdown
Contributor Author

@copilot add new file format that uses .jsonl file to avoid merge issues. Support both old and new format:

Addressed in 5b26586. Added state.jsonl support to the experiment load/pick/push flow, kept backward compatibility with state.json, and covered both formats in targeted tests, including concurrent rebase handling.

@pelikhan

pelikhan commented Aug 1, 2026

Copy link
Copy Markdown
Collaborator

@copilot the jsonl file should be a ledger of workflow run id , experiment variant info etc...

Copilot AI and others added 2 commits August 1, 2026 23:04
Co-authored-by: pelikhan <4175913+pelikhan@users.noreply.github.com>
Co-authored-by: pelikhan <4175913+pelikhan@users.noreply.github.com>

Copilot AI commented Aug 1, 2026

Copy link
Copy Markdown
Contributor Author

@copilot the jsonl file should be a ledger of workflow run id , experiment variant info etc...

Addressed in 4b9b3c2. state.jsonl is now written as a run ledger of per-workflow-run records (run_id, timestamp, assignments, with optional baseline_counts only for legacy JSON migration), while readers still accept the old snapshot-style state.json and legacy hybrid state.jsonl format.

@pelikhan
pelikhan marked this pull request as ready for review August 2, 2026 01:16
Copilot AI review requested due to automatic review settings August 2, 2026 01:16

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.

Pull request overview

Adds conflict recovery for concurrent experiment-state pushes and migrates experiment history to an append-only JSONL ledger.

Changes:

  • Adds rebase conflict hooks and experiment-state merge logic.
  • Adds JSON/JSONL migration, loading, reporting, and tests.
  • Recompiles experiment-enabled workflow locks for state.jsonl.
Show a summary per file
File Description
pkg/workflow/compiler_experiments.go Configures JSONL experiment state.
pkg/cli/experiments_command.go Parses and reports JSONL ledgers.
pkg/cli/experiments_command_test.go Tests JSONL CLI parsing.
pkg/cli/audit_report_experiments.go Reads JSONL audit artifacts.
pkg/cli/audit_report_experiments_test.go Tests JSONL audit extraction.
actions/setup/js/push_signed_commits.cjs Adds rebase conflict resolution hook.
actions/setup/js/push_signed_commits.test.cjs Tests concurrent rebase recovery.
actions/setup/js/push_experiment_state.cjs Merges JSON and JSONL conflicts.
actions/setup/js/push_experiment_state.test.cjs Tests field-level state merging.
actions/setup/js/pick_experiment.cjs Implements JSONL ledger persistence.
actions/setup/js/pick_experiment.test.cjs Tests JSONL loading and migration.
actions/setup/js/load_experiment_state_from_repo.cjs Loads JSONL with JSON fallback.
actions/setup/js/load_experiment_state_from_repo.test.cjs Tests repository-state compatibility.
.github/workflows/weekly-blog-post-writer.lock.yml Uses JSONL experiment state.
.github/workflows/typist.lock.yml Uses JSONL experiment state.
.github/workflows/test-quality-sentinel.lock.yml Uses JSONL experiment state.
.github/workflows/smoke-temporary-id.lock.yml Uses JSONL experiment state.
.github/workflows/smoke-project.lock.yml Uses JSONL experiment state.
.github/workflows/smoke-gemini.lock.yml Uses JSONL experiment state.
.github/workflows/smoke-copilot.lock.yml Uses JSONL experiment state.
.github/workflows/smoke-copilot-sub-agents.lock.yml Uses JSONL experiment state.
.github/workflows/smoke-copilot-aoai-entra.lock.yml Uses JSONL experiment state.
.github/workflows/smoke-copilot-aoai-apikey.lock.yml Uses JSONL experiment state.
.github/workflows/smoke-checkout-pr-dispatch.lock.yml Regenerates runtime settings.
.github/workflows/smoke-antigravity.lock.yml Uses JSONL experiment state.
.github/workflows/plan.lock.yml Uses JSONL experiment state.
.github/workflows/issue-arborist.lock.yml Uses JSONL experiment state.
.github/workflows/gpclean.lock.yml Uses JSONL experiment state.
.github/workflows/dependabot-go-checker.lock.yml Uses JSONL experiment state.
.github/workflows/deep-report.lock.yml Uses JSONL experiment state.
.github/workflows/dataflow-pr-discussion-dataset.lock.yml Uses JSONL experiment state.
.github/workflows/daily-semgrep-scan.lock.yml Uses JSONL experiment state.
.github/workflows/daily-security-red-team.lock.yml Uses JSONL experiment state.
.github/workflows/daily-safe-output-optimizer.lock.yml Uses JSONL experiment state.
.github/workflows/daily-news.lock.yml Uses JSONL experiment state.
.github/workflows/daily-issues-report.lock.yml Uses JSONL experiment state.
.github/workflows/daily-fact.lock.yml Uses JSONL experiment state.
.github/workflows/daily-doc-updater.lock.yml Uses JSONL experiment state.
.github/workflows/daily-doc-healer.lock.yml Uses JSONL experiment state.
.github/workflows/daily-compiler-quality.lock.yml Uses JSONL experiment state.
.github/workflows/daily-community-attribution.lock.yml Uses JSONL experiment state.
.github/workflows/daily-code-metrics.lock.yml Uses JSONL experiment state.
.github/workflows/daily-caveman-optimizer.lock.yml Uses JSONL experiment state.
.github/workflows/daily-cache-strategy-analyzer.lock.yml Uses JSONL experiment state.
.github/workflows/daily-astrostylelite-markdown-spellcheck.lock.yml Uses JSONL experiment state.
.github/workflows/daily-arxiv-researcher.lock.yml Regenerates model-routing settings.
.github/workflows/daily-architecture-diagram.lock.yml Uses JSONL experiment state.
.github/workflows/daily-agentrx-trace-optimizer.lock.yml Uses JSONL experiment state.
.github/workflows/copilot-agent-analysis.lock.yml Uses JSONL experiment state.
.github/workflows/ci-coach.lock.yml Uses JSONL experiment state.
.github/workflows/breaking-change-checker.lock.yml Uses JSONL experiment state.
.github/workflows/blog-auditor.lock.yml Uses JSONL experiment state.
.github/workflows/aw-failure-investigator.lock.yml Uses JSONL experiment state.
.github/workflows/audit-workflows.lock.yml Uses JSONL experiment state.
.github/workflows/architecture-guardian.lock.yml Uses JSONL experiment state.
.github/workflows/agent-persona-explorer.lock.yml Uses JSONL experiment state.
.github/workflows/agent-performance-analyzer.lock.yml Uses JSONL experiment state.

Review details

Tip

Add Copilot custom instructions for smarter, more guided reviews. Learn how to get started.

Suppressed comments (3)

actions/setup/js/push_experiment_state.cjs:125

  • Full-record deduplication corrupts counts during the supported legacy migration race. If the base state.json has counts but no runs, each concurrent writer migrates it by attaching the same baseline_counts to its own distinct run record; this union retains both records, and every reader adds the baseline twice. The JSONL conflict merge needs base-aware baseline reconciliation, or migration must emit a common deduplicable baseline record.
    actions/setup/js/push_experiment_state.cjs:110
  • This always appends the complete local ledger after the remote ledger, so concurrent runs can leave state.jsonl out of timestamp order. Consumers use the final record as the latest run; when the remote run is newer than the local run (as in the integration test), they report the older local assignment. Preserve chronological run order during the union.
    actions/setup/js/push_experiment_state.cjs:70
  • Concurrent increments are still lost when the counter did not exist in the base state. For a newly added experiment/variant, both writers can produce 1 from an absent base key; because baseValue is not finite this falls through to the equal-values case and returns 1 instead of 2. Treat an absent base numeric counter as zero for the additive merge.
  • Files reviewed: 57/57 changed files
  • Comments generated: 4
  • Review effort level: Balanced

// experimentStateFile is the path to the experiment state JSON written by pick_experiment.cjs.
const experimentStateFile = experimentsCacheDir + "/state.json"
// experimentStateFile is the path to the experiment run-ledger JSONL file written by pick_experiment.cjs.
const experimentStateFile = experimentsCacheDir + "/state.jsonl"
Comment thread actions/setup/js/pick_experiment.cjs Outdated
Comment on lines +240 to +242
const latestRun = runs[runs.length - 1];
fs.appendFileSync(stateFile, `${JSON.stringify(latestRun)}\n`, "utf8");
return;
Comment thread pkg/cli/experiments_command.go Outdated
Comment on lines 806 to 811
if err := json.Unmarshal(data, &state); err == nil {
if state.Counts == nil {
state.Counts = map[string]map[string]int{}
}
return &state
}
Comment on lines +54 to +58
for (const run of [...remoteRuns, ...localRuns]) {
const key =
isPlainObject(run) && typeof run.run_id === "string" && typeof run.timestamp === "string" && isPlainObject(run.assignments)
? `${run.run_id}\u0000${run.timestamp}\u0000${stableJSONStringify(run.assignments)}`
: stableJSONStringify(run);
@gh-aw-bot

Copy link
Copy Markdown
Collaborator

@copilot quick triage for this PR:

Run details: https://github.com/github/gh-aw/actions/runs/30727625239

Generated by 👨‍🍳 PR Sous Chef · gpt54 · 10.4 AIC · ⌖ 6.45 AIC · ⊞ 8.2K ·
Comment /souschef to run again

- Fix TypeScript error: add @type annotation to state.runs in loadState so
  tsc no longer infers never[] for the empty array literal (pick_experiment.cjs)
- Fix cache-mode upgrade path: when state.jsonl is absent but state.json exists
  loadState now falls back to the legacy file (ENOENT-only, no swallowing of
  parse errors)
- Fix Go parseExperimentState: only treat unmarshalled data as a snapshot when
  state.Counts != nil, so single-record JSONL files are correctly routed to the
  JSONL parser instead of being returned as empty snapshots
- Sort merged runs by timestamp (then run_id as tie-breaker) in both
  mergeExperimentRuns and mergeExperimentStateJSONL so ledger order is always
  chronological regardless of which writer submitted first
- Compact ledger in mergeExperimentStateJSONL to MAX_LEDGER_RECORDS (512)
  entries; counts from pruned records are folded into the first remaining
  entry's baseline_counts so cumulative totals are preserved
- Export mergeExperimentStateJSONL and mergeExperimentRuns for test coverage
- Add tests: Go single-record JSONL, JS timestamp ordering, JS compaction,
  JS cache-mode loadState fallback

Co-authored-by: gh-aw-bot <259018956+gh-aw-bot@users.noreply.github.com>
@github-actions

github-actions Bot commented Aug 2, 2026

Copy link
Copy Markdown
Contributor

🧠 Matt Pocock Skills Reviewer has completed the skills-based review. ✅

@pelikhan

pelikhan commented Aug 2, 2026

Copy link
Copy Markdown
Collaborator

@copilot

  • update Go parsers to handy the new file format.
  • update conclusion job to include the experiments data in the usage artifact and summaries json payload

@github-actions github-actions 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.

Skills-Based Review 🧠

Applied /diagnosing-bugs and /tdd — requesting changes on merge-correctness edge cases and missing observability.

📋 Key Themes & Highlights

Key Issues

  • Silent last-writer-wins fallback (push_experiment_state.cjs:105): non-numeric/non-object/non-array conflicts are swallowed with no log — add a core.warning and a regression test.
  • Additive counter can underflow (push_experiment_state.cjs:84): remote + local - base can produce a lower value than either writer if either side decremented or reset a counter — consider flooring at Math.max(additive, remote, local) and documenting the invariant.
  • New-key conflict is under-specified (push_experiment_state.cjs:98): when both writers add the same key with different values (base = undefined), the merge silently picks local — no test documents the expected result.
  • Negative counts not validated (push_experiment_state.cjs:113): mergeExperimentStateJSON could pass counts that went negative through the additive merge; add a post-merge guard.
  • Silent resolver decline (push_experiment_state.cjs:220): resolveExperimentStateRebaseConflict returns false without logging when an unexpected file is in the conflict set — callers see a cryptic generic rebase error.

Positive Highlights

  • ✅ Integration tests for the two-writer race in push_signed_commits.test.cjs are thorough and well-structured.
  • ✅ The mergeExperimentStateJSONL compaction logic (baseline_counts rollup) is well thought out and tested.
  • ✅ Good use of stableJSONStringify for deterministic deduplication across the merge helpers.
  • ✅ The resolveRebaseConflict hook is a clean extension point in pushSignedCommits — keeps the core path agnostic of state format.

🧠 Reviewed using Matt Pocock's skills by Matt Pocock Skills Reviewer · sonnet46 · 79.6 AIC · ⌖ 11.4 AIC · ⊞ 7.1K
Comment /matt to run again

@gh-aw-bot

Copy link
Copy Markdown
Collaborator

@copilot quick triage for this PR:

  • Review feedback still worth closing out before merge:
    1. copilot-pull-request-reviewer — reconcile the suppressed comments around baseline double-counting during migration, preserving chronological ledger order, and additive merges for newly introduced counters. Merge concurrent experiment state rebases in push_experiments_state #49631
  • Recent maintainer follow-up to address on the current head:
    1. update Go parsers to handle the new file format
    2. update the conclusion job to include experiments data in the usage artifact and summaries payload
  • Please run the pr-finisher skill before handoff.

Generated by 👨🍳 PR Sous Chef

Generated by 👨‍🍳 PR Sous Chef · gpt54 · 14.5 AIC · ⌖ 7.61 AIC · ⊞ 8.2K ·
Comment /souschef to run again

Co-authored-by: pelikhan <4175913+pelikhan@users.noreply.github.com>

Copilot AI commented Aug 2, 2026

Copy link
Copy Markdown
Contributor Author

@copilot

  • update Go parsers to handy the new file format.
  • update conclusion job to include the experiments data in the usage artifact and summaries json payload

Done in the latest commit.

Go parser updates:

  • parseExperimentStateJSONL now skips unrecognized/invalid lines instead of returning empty state, making the parser resilient to partial writes or corruption. Dead code in the snapshot branch was also removed.
  • extractExperimentData falls back to the usage activity summary when no separate experiment state file is available, so experiment assignments can be recovered from the lighter usage artifact.
  • usageActivitySummary struct now includes an Experiments field (experiments.assignments) that maps experiment names to selected variants.

Conclusion job update:

  • generate_usage_activity_summary.cjs now reads assignments.json written by pick_experiment.cjs and includes the current run's experiment assignments in the usage artifact summary.json under experiments.assignments.

Tests added for all new behavior (JSONL skip-invalid-line resilience, usage summary fallback, and parseExperimentsData).

@pelikhan

pelikhan commented Aug 2, 2026

Copy link
Copy Markdown
Collaborator

@copilot

  • review JavaScript that pushes jsonl update and ensure it is resilient to merge conflicts
  • update Go Experiments command to handle both file formats

…in saveState

Co-authored-by: gh-aw-bot <259018956+gh-aw-bot@users.noreply.github.com>

Copilot AI commented Aug 2, 2026

Copy link
Copy Markdown
Contributor Author

@copilot quick triage for this PR:

  • Review feedback still worth closing out before merge:
    1. copilot-pull-request-reviewer — reconcile the suppressed comments around baseline double-counting duri...

Addressed in the latest commit.

Ledger unbounded growth (reviewer feedback): saveState in pick_experiment.cjs now uses writeFileSync with all bounded runs (state.runs, already limited to MAX_RUN_HISTORY=512) instead of appendFileSync. This keeps the on-disk ledger at most MAX_RUN_HISTORY entries on every write. Any cumulative counts from records trimmed by the MAX_RUN_HISTORY slice are folded into runs[0].baseline_counts so historical totals are always preserved. Two new tests cover this: one verifies the compaction/baseline folding when trimming occurs, and one verifies the file is overwritten (not appended) on normal JSONL writes.

Chronological order: Already fixed in c5d6f3a via sortRunsByTimestamp in both mergeExperimentRuns and mergeExperimentStateJSONL.

JSON/JSONL disambiguation: Fixed in c5d6f3aparseExperimentState only treats unmarshalled data as a snapshot when state.Counts != nil, so single-record JSONL files route correctly to the JSONL parser.

Go parsers + conclusion job: Done in b57f585 — Go now reads both state.jsonl (with JSONL ledger support) and state.json; the conclusion job includes experiment assignments in the usage artifact summary.json under experiments.assignments.

Co-authored-by: pelikhan <4175913+pelikhan@users.noreply.github.com>

Copilot AI commented Aug 2, 2026

Copy link
Copy Markdown
Contributor Author

@copilot

  • review JavaScript that pushes jsonl update and ensure it is resilient to merge conflicts
  • update Go Experiments command to handle both file formats

Done in the latest commit.

JS resilience for JSONL merge conflicts:

  • mergeExperimentStateJSONL in push_experiment_state.cjs now skips unparseable lines with a core.warning instead of throwing. This mirrors the Go parser's behavior and makes conflict resolution resilient to partially-written or corrupt lines that can appear when two writers race on the same experiments branch.
  • Added a test that verifies the resilient behavior (corrupt line is skipped, warning is emitted, valid lines are still merged).

Go experiments command — both file formats:

  • The Go experiments command already handles both state.jsonl (new primary) and state.json (legacy) via experimentStateFilenames(), which returns both in priority order, and parseExperimentState, which handles both the JSONL run-ledger and the legacy JSON snapshot format.
  • Updated stale comments in logs_artifact_set.go and experiments_analyze_statistics.go that still referenced only state.json.

@github-actions

github-actions Bot commented Aug 2, 2026

Copy link
Copy Markdown
Contributor

PR Triage

  • Category:
  • Risk:
  • Priority: (score: 64/100)
    • Impact: 32/50, Urgency: 16/30, Quality: 16/20
  • Recommended action:

Automated triage — see full report issue for details.

Structured data:

{
  "action": "defer",
  "category": "chore",
  "pr_number": 49631,
  "risk": "high"
}

Generated by 🔧 PR Triage Agent · auto · 73.4 AIC · ⌖ 3.47 AIC · ⊞ 8K ·

@gh-aw-bot

Copy link
Copy Markdown
Collaborator

@copilot This PR has merge conflicts. Please run make merge-main, resolve any resulting conflicts cleanly, and then run the pr-finisher skill.

Failed check still listed in compact triage:

Generated by 👨🍳 PR Sous Chef

Generated by 👨‍🍳 PR Sous Chef · gpt54 · 10.1 AIC · ⌖ 6.66 AIC · ⊞ 5.9K ·
Comment /souschef to run again

…ix-push-experiments-state

# Conflicts:
#	actions/setup/js/generate_usage_activity_summary.test.cjs

Co-authored-by: gh-aw-bot <259018956+gh-aw-bot@users.noreply.github.com>

Copilot AI commented Aug 2, 2026

Copy link
Copy Markdown
Contributor Author

@copilot This PR has merge conflicts. Please run make merge-main, resolve any resulting conflicts cleanly, and then run the pr-finisher skill.

Failed check still listed in compact triage:...

Done in aa3befa. Merged origin/main and resolved the conflict in generate_usage_activity_summary.test.cjs — our parseExperimentsData import was merged with main's MANIFEST_FILE_PATH addition on the require line. All tests, lint, and workflow compilation pass.

@pelikhan
pelikhan merged commit de72b54 into main Aug 2, 2026
37 checks passed
@pelikhan
pelikhan deleted the copilot/aw-failures-fix-push-experiments-state branch August 2, 2026 14:42
@github-actions

github-actions Bot commented Aug 3, 2026

Copy link
Copy Markdown
Contributor

🎉 This pull request is included in a new release.

Release: v0.84.3

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Projects

None yet

Development

Successfully merging this pull request may close these issues.

[aw-failures] push_experiments_state fails on concurrent state.json rebase conflicts (shared action, 20+ workflows)

4 participants