Skip to content

P448 derive goal execution from decisions, ledger and live owners - #2529

Merged
trissim merged 20 commits into
mainfrom
fix/p448-derived-goal-execution
Oct 21, 2026
Merged

trissim merged 20 commits into
mainfrom
fix/p448-derived-goal-execution

Conversation

@trissim

@trissim trissim commented Oct 21, 2026 •

Copy link
Copy Markdown
Contributor

P448: derive goal execution from its owners

Supersedes #2503, P386, P416, P448's earlier patch and P406. Implements the design in #2521, docs/design/goal-execution-derived.md.

Draft checkpoint, not delivered or ready to install. Published head 9da7227a997ec906c79d1677f1641ec268f5b7e9, synced through main 15d015642. Remote SHA checked equal to HEAD. The 35-test code checkpoint is 2ee417223; the subsequent commit only corrects the migration note, with no src/tests changes.

Implemented

  • GoalState → GoalWait → GenerationState → TurnDeclaration delegates execution. GoalExecution members own rendering, controls and commander blocks. One frozen GoalExecutionSource and Goals.snapshot_for serve command, history, roster and ACP readers.
  • Deleted backend registry block writers, FailedTurnProjection, GoalExecutionState, BlockingOnFailure, FailedGoalOrigin and BlockedHolder. Resume refuses without writing a block; Retry asks the ledger. Failure evidence remains fenced in the ledger.
  • Accepted orders and productive verified turns share GenerationState.continue_work. BlockedGeneration owns continuation authorization, with the order ID or verified-turn:<turn> as witness; BlockedState no longer repeats it. A fresh order works even when the registry goal is already Active. Unknown generations are not resolved by this path, and UNKNOWN inputs are never replayed.
  • RunnableExecution and RunningExecution are concrete siblings under abstract ActiveExecution, which owns their pause control. StandbyExecution composes ActiveExecution and WaitingExecution, deleting its copied pause getter. P439 can distinguish idle runnable work from a live running attempt without reading stored thread.executing. The unused RetryableGeneration marker is deleted.
  • The worker's OwnerStop reaches settlement; operator stop beats the dying backend's failure.
  • Edit 9, authorized by follow-ups 15045/15169/15244: OwnerReleaseEvidence owns the recorded PID plus start_time. Its process must be dead and no longer the registry owner. Both dispatched and undispatched owner loss use the shared native-child exclusion. A live successor's turn does not invalidate original-process loss. Missing input/claim stays UNKNOWN and unsafe to replay.
  • Commander GoalCannotRun consumes GoalExecution.unit_blocks(). Its goal and execution come from the same GoalSnapshot; enforcement retains the captured goal precondition, refusing changed observations rather than silently applying them.
  • One installer Carry outside src converts only the retired W1–W8 reasons to ActiveGoal and preserves other decisions. Retire this tool and its one-time test after rollout.
  • Removed tests of deleted block writers/projections and classifier accepts methods; migrated the surviving consumers to nominal executions.

Source evidence

Refactor-audit Package/AST inventories cover Core declarations, inheritance, imports and consumers; the paired Toad consumers were read separately. Initial Core inventory parsed 427 modules with no omissions. Final owner inventory (final-owner-ast.txt) parsed 427 modules with no omissions and includes the sibling declarations, continuation consumers and release/subprocess proof owners. The control checkpoint inventory (controls-core-ast.txt) again parses 427 modules with zero omissions; the paired Toad main inventory (toad-controls-ast.txt) parses 283 modules with zero omissions. Dynamic Python resolution is not claimed by AST evidence.

Owner relationships: GoalState/GoalWait/GenerationState/TurnDeclaration select execution; Reservation.fail and FailedTurnEvidence own failures; OwnerReleaseEvidence owns the original released process identity. Coordinator AttemptRecord stores generation/token, not a second process identity.

Patterns removed: IDEN-5 failure copies, IMPL-12 competing writers, IMPL-2 enum dispatch, IDEN-3 nullable execution classification. Unit-filebus's registry lock scopes in reserve()/tool_ended() are retained.

Compared with synced main: 716 src lines deleted, 544 added, net −172. Required whole-src obsolete-name check: 14 → 0 (f1ebe84c8 baseline; current source).

Checks and behavior evidence

  • git diff --check: clean.
  • Five execution-family tests initially passed. Expanded serial run: 80 passed in 166.55s, before the latest main sync and the strengthened next-turn assertion. This includes real owner recovery, native worker launch, live-child rejection, original-alive/tampered-release rejection, and expired-proof rejection.
  • Initial parallel run: 65 passed, one stale rename-identity assertion failed, 15 fixture setup errors. Removed the obsolete name-only mismatch case: RecordedThreadIncarnation deliberately compares birth, not renameable names. Serial run passed. No production binding check was weakened.
  • Consumer run exposed my incorrect use of a holder incarnation as a Thread; fixed by consuming one GoalSnapshot. It also exposed an unrelated stale allocation fixture (test_send_and_check_accept_a_deletion_check_on_fresh_main): worker already holds P1/P2 while outsider is idle, so the current allocation policy correctly refuses. That unchanged test is excluded from the final focused run, not represented as passing.
  • The next batch found two real fixture/ownership gaps: synthetic backend blocks were still being created by blocked_holder, and fresh-order retry authority still lived on BlockedState. The fixture now uses a real failed OwnedTurn; GenerationState.continue_work owns both order and verified-progress continuation. Running/runnable are siblings, and the two agreed account registry locks plus GoalScheduler.prepare_goal are restored from unit-filebus's165f3818c (claim scope captured inside).
  • Broad revised batch: 97 passed, 1 deselected in 71.11s, before the final main sync and deletion of the unused marker.
  • Earlier checkpoint 4bb907101 confirmation: 19 passed in 66.87s, exit 0, after its main sync. Includes all five requested family cases, both previously failing commander recovery cases, running/blocked readers, stale-action safeguards, edit-9 successor/child checks and native worker launch/stop. Exact command/results: published-head-tests.log and .exit.
  • Control checkpoint 2ee417223: 35 collected and passed in 31.98s, exit 0 across test_goal_execution.py, test_goals.py, test_goal_standby.py and test_goal_nominal.py. Deleted the stale state-toggle/caption table rather than porting its wording assertions. Real turn tests now verify Retry after a failed ledger with an unchanged Active goal, versus Pause during a live productive turn. Standby control reuse is implemented through the shared parent, not a second getter. Logs: controls-tests.log/.exit.
  • All runs used timeout 300, -n 0 for the final serial batches, -o faulthandler_timeout=60, PYTHONPATH=src, the shared installed Python and owned persistent scratch. No local ratchets or full suite.

Requested family tests:

  1. test_failed_turn_writes_ledger_once_and_reads_failed_execution
  2. test_provider_retry_during_live_turn_writes_no_goal_failure
  3. test_owner_stop_beats_backend_failure_and_next_turn_runs_without_retry
  4. test_verified_productive_turn_recovers_blocked_generation
  5. test_cutover_retires_backend_text_and_preserves_owner_block

Edit-9 regressions include test_undispatched_original_recovers_beside_a_running_successor, test_undispatched_owner_loss_refuses_a_live_native_child (--session and --session-dir), and test_dead_original_settles_unknown_beside_a_live_successor (both send histories), through the real recovery entrypoints. Original bus/input bytes remain unchanged; no UNKNOWN input is replayed.

Copied live-store carry

Read the live private registry under its registry lock; copied its declared registry/guard files plus bus protocol metadata into an owner-private scratch directory. The carry ran only on that copy:

  • Dry run: 24 migrated, 1 preserved.
  • Apply: 24 migrated, 1 preserved.
  • Second apply: 0 migrated, 1 preserved.
    The copy was retired; the live registry was read only. Snapshot SHA and exact results are in live-migration.log.

Evidence directory: /home/ts/.cache/agent-scratch/unit-goalblock/ (focused-serial.log, continuation-tests.log, published-head-tests.log, final-owner-ast.txt, live-migration.log, continuation-push-verified.log, controls-tests.log, controls-core-ast.txt, toad-controls-ast.txt, toad-notes-final-publish.log). Owned checkout plus generated native test artifact: about 227 MiB; scratch below 1 MiB at publication. Pytest built the missing checkout-pinned native artifact once; subsequent runs reuse it. /home/ts/bin/agent-resource-check is absent on this runtime; measured 1.8 GiB available RAM and 34 GiB free disk, so the final run is serial.

Still required before delivery

  • Paired Toad import/control migration. Ready-to-apply edits, all consumer/fixture migrations and installed acceptance steps are published in notes/P448-TOAD-MIGRATION.md, pinned to Toad main 43b0467826. GoalSession owns both control captions and actions; no new wrapper is required. Existing Toad remains incompatible. The prescribed checkout /home/ts/wt/unit-goalblock--P448--toad was denied by Core's allocation owner; commander owns PR Judge a new second-repository checkout by the repository it is added from #2535, expected in install 42. No alternate checkout/path or allocation bypass.
  • P439 derive one sibling UnitState per unit #2488 rebases onto this PR and deletes its UnitBlock family. Handoff was sent to unit-unitstate in 15044 and confirmed in 15046.
  • Installed paired native/Toad journey: productive successor visibly active, stopped workers continuing after actual relaunch without Retry, fresh DM answered with its Toad tab open and surviving tab return. Model/turn tests are not physical UI acceptance.
  • Run the carry in installation and retire it after rollout. Do not install Core alone while paired Toad imports remain incompatible.

Assumed:

  • Explicit model/owner decisions remain stored and precede derived execution; recorded waits precede the ledger.
  • OwnerReleaseEvidence is the trusted original release/process record; a successor's PID or moving claim counter is not original-process evidence.
  • UNKNOWN remains UNKNOWN after settlement; successful fresh progress never grants replay permission.
  • Neither source tests nor merge status establishes installed Toad behavior.

@trissim trissim changed the title Derive goal execution from the ledger; remove stored backend blocks P448 derive goal execution from decisions, ledger and live owners Oct 21, 2026

@trissim trissim left a comment

Copy link
Copy Markdown
Contributor Author

Choose a reason for hiding this comment

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

agent-comms/review: failure at 9da7227.

The Core refactor is good work. GoalState → GoalWait → GenerationState → TurnDeclaration now selects execution through the family, with no isinstance chain. The block writers and FailedTurnProjection are deleted, and net src is −172. I ran the touched test files at this head: 139 passed, 2 failed, and both failures (test_send_and_check_accept_a_deletion_check_on_fresh_main, test_watcher_git_and_github_reads_leave_wire_free) fail on main too. It can't land, though:

  1. The PR deletes Core names that Toad imports, and no Toad PR exists. Your own notes/P448-TOAD-MIGRATION.md:39-40 says src/toad/goal_interaction.py imports the deleted ThreadGoalExecution and reads goal.state for its controls, and the same note says "Core must not install alone". gh pr list -R OpenHCSDev/toad --search P448 returns nothing. With auto-merge armed, the queue would land a Core that breaks the owner's Toad at the next install. Fix: open the paired Toad PR from the note's edits, and land the two as one batch. #2535 is the allocation fix that unblocks your Toad checkout. Until both heads exist, this stays draft.
  2. src/agent_comms/goal_presentation.py:226: a Core execution family imports agent_comms.commander.standing.UnownedWheelSpin to build its reissue action. Core now depends on the commander layer for a constructor of a Core type, ReissuedOrderGoalAction. That's BOUND-8: the action's construction is flattened into a commander module and pulled back. Fix: move UnownedWheelSpin.order_for (commander/standing.py:309) to ReissuedOrderGoalAction.for_todo(todo, commander, assigner) in goal_actions.py. Have both standing.py and goal_presentation.py call it, and delete the commander import.
  3. src/agent_comms/goal_presentation.py:225 and :272: enforce_unit_block(self, comms, ...) leaves comms untyped. That's an implicit Any on a family's base method. Fix: annotate it comms: Comms (under TYPE_CHECKING) on both the base and the override.

…side its execution

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01AeUaZjvChAcb6qAd6zPYmw
…yped

UnownedWheelSpin.order_for moves to ReissuedOrderGoalAction.for_todo, so Core's goal
execution family no longer imports the commander layer to build a Core action; both
callers use it. enforce_unit_block types comms on the base and the override.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01AeUaZjvChAcb6qAd6zPYmw

@trissim trissim left a comment

Copy link
Copy Markdown
Contributor Author

Choose a reason for hiding this comment

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

agent-comms/review: failure at 636c531. Items 2 and 3 are fixed exactly as ordered, both within four minutes:

  • ReissuedOrderGoalAction.for_todo now owns the reissue, so Core no longer imports anything from the commander.
  • comms: Comms is typed on the base and on the override.

Item 1 still stands. No Toad PR exists: gh pr list -R OpenHCSDev/toad --state all --search P448 is empty. Yet notes/P448-TOAD-MIGRATION.md:39-40 still says src/toad/goal_interaction.py imports the deleted ThreadGoalExecution. #2535 has merged (cf80294), so the allocation that blocked your Toad checkout is no longer in the way. Fix: open the paired Toad PR from the note's edits, then land both as one batch. Until the Toad head exists, this Core head breaks Toad at the next install.

claude and others added 8 commits October 21, 2026 17:12
GoalExecution no longer answers toggle with None: ControlledExecution is the
nominal family of executions the owner can move with one control (active,
paused, blocked), and Toad asks the family. TurnRunner's owner_stop default
was dead (acp.py always passes it), so it is required. Long lines wrapped,
imports sorted and the new carry tool and goal_generation formatted.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01AeUaZjvChAcb6qAd6zPYmw
…-execution

# Conflicts:
#	src/agent_comms/goal_actions.py
… and turn booleans

Active, Complete, Terminal, Failed and Executing replace the active, terminal,
failed and executing class flags across P448's derivation. Resume refusal moves
onto GenerationState.resume_status and refuses without writing a BlockedGoal.
FailedTurnProjection and RetryableGeneration stay deleted with the reissue rule.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01AeUaZjvChAcb6qAd6zPYmw
Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01AeUaZjvChAcb6qAd6zPYmw
goal_generation and turn_lease reach goal_presentation and
goal_failure_observation at import time; neither reaches back. Only the toggle
actions stay function-local: goal_actions imports goal_presentation through
goal_waits.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01AeUaZjvChAcb6qAd6zPYmw
@trissim
trissim marked this pull request as ready for review October 21, 2026 21:51
Copilot AI balanced review requested due to automatic review settings October 21, 2026 21:51
@trissim
trissim merged commit 4310740 into main Oct 21, 2026
4 of 6 checks passed

Copilot AI left a comment

Copy link
Copy Markdown

Choose a reason for hiding this comment

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

Copilot was unable to review this pull request because the user who requested the review has reached their quota limit.

An organization owner can choose the repository organization as the billing source for reviews requested by members with a paid Copilot license. Learn about Copilot code review billing.

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