Skip to content

refactor(execution): decompose execute_task into named phases (#2314) - #2489

Merged
vybe merged 2 commits into
devfrom
refactor/2314-decompose-execution
Sep 3, 2026
Merged

vybe merged 2 commits into
devfrom
refactor/2314-decompose-execution

Conversation

@dolho

@dolho dolho commented Sep 2, 2026

Copy link
Copy Markdown
Contributor

Summary

Two commits decomposing task_execution_service.py (#2314), both in-place — the module keeps its name and every module global stays put, because ~140 monkeypatch sites across the unit suites patch services.task_execution_service.*, and moving the class to a sibling module is precisely the silent-test-rot hazard (patches apply cleanly and test nothing) the issue's own Technical Notes warn against mid-#1081.

  1. execution_envelope.pyTaskExecutionErrorCode / TaskExecutionResult / TerminalEnvelope carved into a leaf module, re-exported from the service so every existing import keeps resolving.
  2. execute_task decomposed: the 907-line monolith becomes a ~350-line orchestrator (mostly the unchanged preamble + kwarg plumbing) over nine named phase methods:
Phase Method Lines
2 — admission + fast-fail terminals _admission_gate 115
3 — dispatch activity _start_dispatch_activity 40
3b — breaker fast-fail _breaker_fast_fail 58
4a — prompt compose + fallback _compose_effective_system_prompt 58
4 — call + #678/#792 retries _call_agent_with_retries 223
5–7 — cancel/#1410/SUCCESS terminals _finalize_sync_response 122
timeout / budget / HTTP-error handlers _handle_timeout / _handle_budget_exhausted / _handle_http_error 35/51/89

_AttemptState replaces the old hoisted retry locals — the exception handlers read what the retries wrote (retry count, rolled-up failed-attempt cost, the #792 one-shot switch flag, the retry-reset start_time the timeout handler's elapsed derives from), and creating it before the try removes the historical NameError hazard the old hoisting comment guarded against. Phase 3c, the payload dict, the #1083 202-ACK handoff (which flips async_handoff for the finally) and the finally stay inline: each couples to orchestrator-local control flow.

_admission_gate is added to the #1804 parity allowlist with the same justification its sibling admission-path entries carry (no dispatch activity exists before step 3 — before this PR those writes sat inline in execute_task, whose own close calls satisfied the function-level scan).

What this deliberately does NOT do

The issue's headline "no module >800 lines" is not met for this file (2,398 lines with the added signatures/docstrings): meeting it requires physically relocating execute_task/apply_result, which detaches those ~140 patch sites mid-#1081 — the "big-bang" the issue's Technical Notes rule out. The four other oversized modules from #2314's list ship in #2487 as real package splits. The remaining step for this file — moving the phase methods out behind the now-explicit seams once #1081 settles — is now mechanical.

Verification

  • Every moved block is byte-preserved modulo the state. renames, checked with an undefined-name AST walk over the module (it caught the one real transcription slip — a log line reading two now-out-of-scope names, repointed at the payload).
  • All 89 test files that touch task_execution_service / execute_task: 1,844 passed, 8 skipped. The single failure is test_2467_turn_integrity's fleet-list arm — the dev-wide hardcoded-timestamp time bomb (fires after 2026-09-02T10:00Z on every branch), fixed separately in fix(tests): defuse the hardcoded-timestamp time bomb in the #2467 fleet-list test #2488.

Closes #2314

🤖 Generated with Claude Code

https://claude.ai/code/session_01NLfHNPtB5UCMk4LonZiJux

dolho and others added 2 commits September 2, 2026 13:12
…2314)

Step 1 of the decomposition. `TaskExecutionErrorCode`, `TaskExecutionResult`
and `TerminalEnvelope` are the one part of task_execution_service with no
collaborators at all — pure dataclasses and an enum, no db, no HTTP, no
capacity manager — so they can be imported from anywhere without a cycle.
That is what lets the phase modules beside them name the same vocabulary
without importing the service they belong to.

Re-exported from task_execution_service: ~80 call sites and 83 test files
import these names from there, and a decomposition that renames the import
surface is not a pure refactor.

Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01NLfHNPtB5UCMk4LonZiJux
…phases (#2314)

The 907-line monolith is now a ~350-line orchestrator plus nine phase
methods, decomposed IN-PLACE — same module, so the ~140 monkeypatch
sites across the unit suites keep patching the module globals the code
actually reads (moving the class out is exactly the silent-test-rot
hazard the issue's own Technical Notes warn about mid-#1081).

- _admission_gate: step 2 + the three fast-fail terminals (CapacityFull
  / CircuitOpen / EphemeralBudgetExhausted); returns (slot_acquired,
  denial). Added to the #1804 parity allowlist with the same
  justification the sibling admission-path entries carry (no dispatch
  activity exists before step 3).
- _start_dispatch_activity, _breaker_fast_fail (hands the transport
  CircuitState back for the #678 mid-turn re-check),
  _compose_effective_system_prompt (fallback path preserved verbatim).
- _call_agent_with_retries: the #678 reader-race retry and the #792
  SUB-003 switch+retry, mutating a shared _AttemptState.
- _finalize_sync_response: raise_for_status + #679 cancel
  cross-validation + #1410 guard + the SUCCESS terminal.
- _handle_timeout / _handle_budget_exhausted / _handle_http_error: the
  three big except bodies, verbatim.

_AttemptState replaces the old hoisted retry locals: the exception
handlers read what the retries wrote (retry_count, rolled-up
failed-attempt cost, the one-shot switch flag, the retry-reset
start_time), and creating it BEFORE the try removes the historical
NameError hazard the hoisting comment guarded against. The generic
Exception / CancelledError handlers, phase 3c, the payload dict, the
#1083 202-ACK handoff (which flips async_handoff for the finally) and
the finally itself stay inline — each is small and couples to
orchestrator-local control flow.

Behavior-neutral by construction: every moved block is byte-preserved
modulo the state-object renames, checked with an undefined-name AST
walk over the module (which caught the one real transcription slip, a
log line reading two now-out-of-scope names).

Related to #2314

Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01NLfHNPtB5UCMk4LonZiJux

@obasilakis obasilakis 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.

/validate-pr: APPROVE

The terminal/CAS contract is preserved by construction, which is the thing that had to be proven for a refactor of this file. Verification run against PR head:

  • AST function-level diff, base ↔ head: every pre-existing function is byte-identical. Only execute_task changed (907 → 350 lines) plus 9 private phase methods added. apply_result and _write_terminal_and_gate — the two owners of the CAS won gate, spawn_task_terminal_event (#1578) and close_execution_activity (#1804) — are untouched.
  • Flattened, order-sensitive call sequence (orchestrator with phase calls inlined) is identical to base. No reordering, no dropped or added side effect. Counts match exactly: spawn_task_terminal_event 3/3, close_execution_activity 3/3, record_outcome 3/3, update_execution_status 9/9, _write_terminal_and_gate 6/6.
  • activity_id is threaded into all 5 exception paths; _breaker_fast_fail (step 3b, post-activity) correctly routes through _write_terminal_and_gate with activity_id and agent_name (task_execution_service.py:1257-1263).
  • The CancelledError handler keeps its if won: gate on the close (:1190); the finally slot release is still gated on slot_acquired and not async_handoff (:1206), both flags initialised before the try.
  • _AttemptState mutation sites map 1:1 to base's retry locals — 10 sites, same order and operators.
  • tests/unit/test_1804_terminal_activity_parity.py runs green against PR head (6 passed). The new _admission_gate allowlist entry is justified correctly: its three terminals all fire from capacity.acquire at step 2, before step 3 opens the dispatch activity.

All 24 checks pass.

Warnings

  • #2314 AC#1 ("no resulting file over 800 lines") is explicitly unmet — the file grew 2,202 → 2,397 lines — and the PR uses "Related to #2314" rather than a closing keyword, so the issue stays status-in-progress. Both are honestly disclosed in the body, and the remaining move is described as mechanical post-#1081. Worth a note on #2314 that #2489 and #2487 are partial delivery, so the P-16 debt slot doesn't read as stalled.

Suggestions

  • _AttemptState is constructed before the try (:984), whereas base assigned the retry locals just above the agent call. That's a small positive behaviour delta in a "pure refactor" — it removes a latent NameError if steps 2–4a ever raised into except httpx.HTTPError. Disclosed in the body and no reachable path changes, so no objection.
  • execution_envelope.py:29 carries a vestigial @dataclass on class TaskExecutionErrorCode(str, Enum), moved verbatim from base. A leaf-module carve-out is the cheapest moment to drop it.

Findings produced by /validate-pr (Claude Code).

@vybe
vybe merged commit c3ddd96 into dev Sep 3, 2026
25 checks passed
vybe pushed a commit that referenced this pull request Sep 3, 2026
Resolves the conflict in services/task_execution_service.py between this
branch and #2314 (PR #2489), which decomposed execute_task into named phases.

The two changes are semantically disjoint. Every ent#279 scrub site lives in
_write_terminal_and_gate and apply_result, both of which #2314 left
byte-identical; every other difference this branch had in that file was black
reformatting with no semantic content. Resolution therefore takes dev's file
whole -- keeping the decomposed execute_task, the nine extracted helpers and
the services/execution_envelope re-export -- and re-applies only the scrub
import and the four scrub blocks. No reformatting is carried over.

One real cross-PR gap surfaced, caught by test_every_free_text_writer_scrubs:
#2314 moved three static admission-refusal writes (capacity-full,
dispatch-breaker, ephemeral-exhausted) out of execute_task and into the new
_admission_gate, which the ent#279 allowlist did not yet name. Those writes
are unchanged and still run before any agent call, so no agent text or staged
secret can exist on that path; _admission_gate is allowlisted on the same
grounds the execute_task entry already recorded, and that entry is narrowed to
the single backend-shutdown write it still holds.

Verified: 1864 passed, 9 skipped across all 90 unit-test files touching
task_execution_service / execute_task / apply_result.

Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01YTHX5aMsCJf71QhtysewL4
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.

3 participants