Skip to content

feat(runtime): export mutating-subagent changes as staged changesets#320

Merged
OBenner merged 1 commit into
developfrom
feat/subagent-changeset-export
Jun 10, 2026
Merged

feat(runtime): export mutating-subagent changes as staged changesets#320
OBenner merged 1 commit into
developfrom
feat/subagent-changeset-export

Conversation

@OBenner

@OBenner OBenner commented Jun 10, 2026

Copy link
Copy Markdown
Owner

Context — Phase A / PR-2 of "mutating subagents → safe"

Follows #319 (write-scope enforcement). With this PR a transactional_write child never touches the shared workspace: its mutations are auto-staged and exported as a changeset that the parent will apply transactionally in Phase B (PR-3).

What changes

1. Changeset-exporting sessionsGenericEditRuntimeSession(changeset_export=True):

  • Mutations auto-stage into the runtime-owned __changeset__ batch, reusing the proven staged-batch mechanics (materialize-per-action + restore) — the real workspace stays at baseline between actions and after finish.
  • Model-issued begin/commit/abort_batch is rejected (staging is runtime-owned), so the only staging path is the implicit batch.
  • At a clean finish the staged pre/postimages finalize into a changeset payload — first preimage + last postimage per path, exportability decided by the existing materializability predicate (≤20KB UTF-8 / clean deletions) — returned on AgentRunResult.changeset. The open-batch finish veto now applies only to model-managed batches. Error/recovery exits export nothing.

2. Orchestrator hardening (subagents.py):

  • The confinement check additionally refuses a confined-but-directly-writing child (changeset_export required for mutating tasks).
  • Children are validated against execution requirements (generic_edit() for mutating tasks): task.requirements is the parent-side policy gate (incl. the policy-granted subagents capability), not what the child session physically does — without this split, every real mutating child would fail the child-side capability re-check.
  • Per-child artifact namespace — every child runs with spec_dir = artifacts/subagents/<task-id>/, so a runtime child can never clobber the parent's (or a sibling's) generic_edit_trace.json / recovery checkpoint. This closes a latent recovery-correctness hazard: previously a parent crash after a generic_edit child finished could resume from the child's checkpoint.
  • RuntimeSubagentResult.changeset persists in the per-child artifact; the merge plan records changeset_result_ids / missing_changeset_result_ids for the Phase B executor.

3. Production wiring (agents/coder.py): the subagent session factory builds a confined changeset-exporting generic_edit child for transactional_write tasks (write_scope_guard from the task); read-only children stay analysis_only.

What this PR intentionally does NOT do

No merge-plan execution (Phase B / PR-3), no conflict resolution (PR-4), no probes/evidence (Phase C), no safe flip (Phase D). read_only children and non-changeset sessions are unchanged.

Tests (4 new + 2 updated, 382 passed across the affected surface)

  • Changeset export leaves the workspace untouched — edit + create paths, exact pre/postimage contents asserted.
  • Model batch control rejected without poisoning staging (subsequent mutations still export).
  • End-to-end orchestrator run with a REAL confined generic_edit child: changeset in the result and embedded in the per-child artifact, child runtime artifacts under artifacts/subagents/edit-a/, parent artifacts untouched, merge plan updated.
  • Confined-but-directly-writing child refused with a clear reason.

🤖 Generated with Claude Code

Phase A / PR-2 of moving mutating parallel subagents toward `safe`
(follows #319's write-scope enforcement). A transactional_write child no
longer needs to touch the shared workspace at all: its mutations are
auto-staged and exported as a changeset the parent will apply
transactionally (Phase B).

- GenericEditRuntimeSession(changeset_export=True): mutations auto-stage
  into the runtime-owned __changeset__ batch (existing staged-batch
  mechanics: materialize-per-action + restore, so the real workspace
  stays at baseline between actions and after finish). Model-issued
  begin/commit/abort batch is rejected — staging is runtime-owned. At a
  clean finish the staged pre/postimages are finalized into a changeset
  payload (first preimage + last postimage per path, exportability via
  the existing materializability predicate) returned on
  AgentRunResult.changeset; the open-batch finish veto now applies only
  to model-managed batches. Error/recovery finishes export nothing.
- Orchestrator: mutating children must be changeset-exporting (the
  confinement check refuses directly-writing sessions); children run
  against execution requirements (generic_edit for mutating tasks) since
  task.requirements is the parent-side policy gate, not what the child
  session physically does; every child gets a per-child artifact
  namespace (artifacts/subagents/<task-id>/) so a runtime child never
  clobbers the parent's — or a sibling's — generic_edit trace/checkpoint
  artifacts. RuntimeSubagentResult.changeset is persisted in the
  per-child artifact, and the merge plan now records
  changeset_result_ids / missing_changeset_result_ids for the Phase B
  executor.
- Production wiring (agents/coder.py): the subagent session factory
  builds a confined changeset-exporting generic_edit child for
  transactional_write tasks (write_scope_guard from the task), keeping
  analysis_only for read-only children.

Tests: changeset export leaves the workspace untouched (edit + create
paths, pre/postimages asserted); model batch control rejected without
poisoning staging; an end-to-end orchestrator run with a REAL confined
generic_edit child (changeset in result + per-child artifact, child
runtime artifacts namespaced, merge plan updated); confined-but-
directly-writing child refused. 382 passed across the affected surface.

Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
Signed-off-by: Oleg Miagkov <mrobenner@gmail.com>
@coderabbitai

coderabbitai Bot commented Jun 10, 2026

Copy link
Copy Markdown
Contributor

Warning

Review limit reached

@OBenner, we couldn't start this review because you've reached your PR review rate limit.

More reviews will be available in 3 minutes and 51 seconds. Learn how PR review limits work.

Your organization has run out of usage credits. Purchase more in the billing tab.

⌛ How to resolve this issue?

After more reviews become available, a review can be triggered using the @coderabbitai review command as a PR comment. Alternatively, push new commits to this PR.

We recommend that you space out your commits to avoid hitting the rate limit.

🚦 How do rate limits work?

CodeRabbit enforces hourly rate limits for each developer per organization.

Our paid plans include higher PR review limits than trial, open-source, and free plans. In all cases, reviews become available again over time. During sustained high-volume PR review activity, CodeRabbit may temporarily slow when the next review becomes available.

Please see our Fair Usage Limits Policy for further information.

ℹ️ Review info
⚙️ Run configuration

Configuration used: Path: .coderabbit.yaml

Review profile: ASSERTIVE

Plan: Pro

Run ID: a097a695-b126-427f-b3e2-f420240baba3

📥 Commits

Reviewing files that changed from the base of the PR and between e1184c1 and 33d630b.

📒 Files selected for processing (6)
  • apps/backend/agents/coder.py
  • apps/backend/agents/runtime/adapters/__init__.py
  • apps/backend/agents/runtime/adapters/generic_edit.py
  • apps/backend/agents/runtime/result.py
  • apps/backend/agents/runtime/subagents.py
  • tests/test_agent_runtime.py
✨ Finishing Touches
🧪 Generate unit tests (beta)
  • Create PR with unit tests
  • Commit unit tests in branch feat/subagent-changeset-export

Thanks for using CodeRabbit! It's free for OSS, and your support helps us grow. If you like it, consider giving us a shout-out.

❤️ Share

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

@sonarqubecloud

Copy link
Copy Markdown

@OBenner OBenner merged commit 7aaf9fd into develop Jun 10, 2026
20 checks passed
OBenner added a commit that referenced this pull request Jun 10, 2026
…sets (#321)

Phase B / PR-3 of moving mutating parallel subagents toward `safe`
(stacked on #320's changeset export). The merge plan is no longer just
descriptive: after a run_subagents run whose plan requires a parent
merge, the parent now APPLIES each child's exported changeset
transactionally.

- execute_subagent_changeset_merge: each mutating child is one
  transaction boundary. Its changeset is scope-revalidated
  (defense-in-depth against a tampered payload), baseline-verified
  (every entry's preimage must still match the live workspace via the
  existing drift comparator — an unverifiable baseline refuses the merge
  just like real drift), then applied as a unit. A mid-apply failure
  restores that child's captured baseline in reverse order; siblings are
  unaffected. Conflicted children are NEVER auto-applied — they surface
  as conflicted_unresolved for explicit resolution (deterministic
  AutoMerger routing lands in PR-4).
- Each applied child is recorded as a parent mutation snapshot with
  transaction id subagent_merge:<child-id>, so the parent's existing
  rollback_transaction tool can undo exactly one child's merge.
- run_subagents now reports merge_plan + merge_execution in its action
  data; the action only stays ok when every appliable changeset applied
  (applied/noop). Mutating delegation inside an open model-managed batch
  is refused up front (mutating_subagents_in_open_batch) before any
  child spawns.

Tests: end-to-end parent run (real confined changeset child; file lands
on the shared workspace via the parent; merge_execution recorded with
the per-child transaction id), baseline drift refused with workspace
intact, mid-apply failure rolled back (first entry restored), applied
child undone via rollback_transaction, conflicted children kept
unapplied, and delegation refused inside an open parent batch.

Signed-off-by: Oleg Miagkov <mrobenner@gmail.com>
Co-authored-by: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
OBenner added a commit that referenced this pull request Jun 10, 2026
…sets (#322)

* feat(runtime): execute the transactional parent merge of child changesets

Phase B / PR-3 of moving mutating parallel subagents toward `safe`
(stacked on #320's changeset export). The merge plan is no longer just
descriptive: after a run_subagents run whose plan requires a parent
merge, the parent now APPLIES each child's exported changeset
transactionally.

- execute_subagent_changeset_merge: each mutating child is one
  transaction boundary. Its changeset is scope-revalidated
  (defense-in-depth against a tampered payload), baseline-verified
  (every entry's preimage must still match the live workspace via the
  existing drift comparator — an unverifiable baseline refuses the merge
  just like real drift), then applied as a unit. A mid-apply failure
  restores that child's captured baseline in reverse order; siblings are
  unaffected. Conflicted children are NEVER auto-applied — they surface
  as conflicted_unresolved for explicit resolution (deterministic
  AutoMerger routing lands in PR-4).
- Each applied child is recorded as a parent mutation snapshot with
  transaction id subagent_merge:<child-id>, so the parent's existing
  rollback_transaction tool can undo exactly one child's merge.
- run_subagents now reports merge_plan + merge_execution in its action
  data; the action only stays ok when every appliable changeset applied
  (applied/noop). Mutating delegation inside an open model-managed batch
  is refused up front (mutating_subagents_in_open_batch) before any
  child spawns.

Tests: end-to-end parent run (real confined changeset child; file lands
on the shared workspace via the parent; merge_execution recorded with
the per-child transaction id), baseline drift refused with workspace
intact, mid-apply failure rolled back (first entry restored), applied
child undone via rollback_transaction, conflicted children kept
unapplied, and delegation refused inside an open parent batch.

Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
Signed-off-by: Oleg Miagkov <mrobenner@gmail.com>

* fix(runtime): harden subagent merge per PR-322 review + Windows CI

Addresses all 5 CodeRabbit findings and the Windows test failure:

- Changesets are untrusted input at the parent boundary: the declared
  write_scope must be non-empty and normalizable (else scope_violation
  with reason missing_or_invalid_write_scope), and every entry's declared
  path must equal its preimage AND postimage paths (else
  invalid_changeset_path) — the postimage path is what actually gets
  written, so path mismatches could otherwise escape revalidation.
- subagent_merge snapshots now carry workspace_guard: checkpoint
  integrity validation treats it as mandatory, so a recoverable stop
  after a subagent_merge rollback no longer fails resume validation.
- run_subagents result data now surfaces mutation_snapshot_ids,
  mutated_paths, and affected_paths from applied outcomes (each applied
  outcome carries its mutation_snapshot_id), so transaction summaries
  record parent-side merge mutations and a later failing action in the
  same turn yields partial_failure with a recovery plan instead of plain
  failed.
- Windows CI: the rollback test hand-crafted a preimage with byte counts
  that broke under CRLF newline translation; it now captures the real
  preimage via build_generic_edit_file_preimage.
- New tests per review: baseline_unverifiable refusal; partial merge
  keeps the applied sibling while the failing child rolls back; malformed
  changesets (path mismatch, scope-less) rejected with nothing applied.

311 passed across agent_runtime / runtime_capabilities / runtime_modes.

Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
Signed-off-by: Oleg Miagkov <mrobenner@gmail.com>

* fix(tests): guard collection access before indexing merge snapshots

SonarCloud reliability gate flagged the unguarded snapshots[0] access;
assert the expected lengths before indexing.

Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
Signed-off-by: Oleg Miagkov <mrobenner@gmail.com>

---------

Signed-off-by: Oleg Miagkov <mrobenner@gmail.com>
Co-authored-by: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
OBenner added a commit that referenced this pull request Jun 10, 2026
Phase D / PR-7 — the final step of "mutating subagents -> safe" (stacked
on the PR-6 gate requirement). The protocol is no longer scaffolded:

- children are write-scope confined with runtime enforcement (#319),
- they stage mutations and export changesets without touching the shared
  workspace (#320),
- the parent applies each child transactionally with per-child rollback
  and baseline verification (#322),
- real conflicts are never auto-applied — they block finish until the
  parent resolves explicitly (#323),
- the whole mechanism is covered by the deterministic
  subagent_merge_probe (PR-5) which the promotion gate now requires
  (PR-6).

With that evidence chain in place, AutonomyLevel.SAFE now defaults
mutating_subagents_enabled=True. bold's remaining distinction is skipping
the evidence gate. The explicit AUTO_CODE_MUTATING_SUBAGENTS=false
override still opts out (covered by a new test), per ADR-006 precedence.

ADR-006's level table updated. The .env.example and UI level descriptions
live on the still-open #309/#318 branches and are updated there.

Tests: safe-level expectations flipped (autonomy level + direct-API
runtime policy grant); explicit env opt-out covered. 694 passed across
the affected surface.

Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
Signed-off-by: Oleg Miagkov <mrobenner@gmail.com>
OBenner added a commit that referenced this pull request Jun 10, 2026
Phase D / PR-7 — the final step of "mutating subagents -> safe" (stacked
on the PR-6 gate requirement). The protocol is no longer scaffolded:

- children are write-scope confined with runtime enforcement (#319),
- they stage mutations and export changesets without touching the shared
  workspace (#320),
- the parent applies each child transactionally with per-child rollback
  and baseline verification (#322),
- real conflicts are never auto-applied — they block finish until the
  parent resolves explicitly (#323),
- the whole mechanism is covered by the deterministic
  subagent_merge_probe (PR-5) which the promotion gate now requires
  (PR-6).

With that evidence chain in place, AutonomyLevel.SAFE now defaults
mutating_subagents_enabled=True. bold's remaining distinction is skipping
the evidence gate. The explicit AUTO_CODE_MUTATING_SUBAGENTS=false
override still opts out (covered by a new test), per ADR-006 precedence.

ADR-006's level table updated. The .env.example and UI level descriptions
live on the still-open #309/#318 branches and are updated there.

Tests: safe-level expectations flipped (autonomy level + direct-API
runtime policy grant); explicit env opt-out covered. 694 passed across
the affected surface.

Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
Signed-off-by: Oleg Miagkov <mrobenner@gmail.com>
OBenner added a commit that referenced this pull request Jun 10, 2026
…326)

* chore: retrigger CI after base retarget to develop

Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
Signed-off-by: Oleg Miagkov <mrobenner@gmail.com>

* feat(autonomy): enable mutating parallel subagents at the safe level

Phase D / PR-7 — the final step of "mutating subagents -> safe" (stacked
on the PR-6 gate requirement). The protocol is no longer scaffolded:

- children are write-scope confined with runtime enforcement (#319),
- they stage mutations and export changesets without touching the shared
  workspace (#320),
- the parent applies each child transactionally with per-child rollback
  and baseline verification (#322),
- real conflicts are never auto-applied — they block finish until the
  parent resolves explicitly (#323),
- the whole mechanism is covered by the deterministic
  subagent_merge_probe (PR-5) which the promotion gate now requires
  (PR-6).

With that evidence chain in place, AutonomyLevel.SAFE now defaults
mutating_subagents_enabled=True. bold's remaining distinction is skipping
the evidence gate. The explicit AUTO_CODE_MUTATING_SUBAGENTS=false
override still opts out (covered by a new test), per ADR-006 precedence.

ADR-006's level table updated. The .env.example and UI level descriptions
live on the still-open #309/#318 branches and are updated there.

Tests: safe-level expectations flipped (autonomy level + direct-API
runtime policy grant); explicit env opt-out covered. 694 passed across
the affected surface.

Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
Signed-off-by: Oleg Miagkov <mrobenner@gmail.com>

* chore: retrigger CI after base retarget to develop

Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
Signed-off-by: Oleg Miagkov <mrobenner@gmail.com>

* chore: retrigger CI on develop base

Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
Signed-off-by: Oleg Miagkov <mrobenner@gmail.com>

---------

Signed-off-by: Oleg Miagkov <mrobenner@gmail.com>
Co-authored-by: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
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.

1 participant