Skip to content

Commit 4f92eff

Browse files
fluffy314cursoragent
authored andcommitted
fix(autoresearch): resume bound decomposition directly
Bypass legacy candidate novelty only for fully target-bound Research Contract decomposition resumes, with a typed evidence backjump for the prior misclassification. Co-authored-by: Cursor <cursoragent@cursor.com>
1 parent 5d4f59c commit 4f92eff

2 files changed

Lines changed: 148 additions & 3 deletions

File tree

autoresearch/prefill/supervisor.py

Lines changed: 96 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -39,6 +39,7 @@
3939
from autoresearch.prefill.cursor_strategy import CursorStrategyAdapter
4040
from autoresearch.prefill.strategy_tournament import StrategyEvent
4141
from autoresearch.prefill.orchestration_state import (
42+
BlockedEventType,
4243
BlockedExitEvent,
4344
OrchestrationCheckpoint,
4445
ProofState,
@@ -1696,6 +1697,73 @@ def route_contract_to_subgoal_generation(
16961697
return True
16971698

16981699

1700+
def is_contract_bound_subgoal_resume(
1701+
checkpoint: OrchestrationCheckpoint | None,
1702+
) -> bool:
1703+
"""Recognize a typed decomposition resume independent of wrapper candidate."""
1704+
return bool(
1705+
checkpoint is not None
1706+
and checkpoint.proof_state == ProofState.DECOMPOSER
1707+
and checkpoint.target_obligation_id
1708+
and checkpoint.proposition_hash
1709+
and checkpoint.target_context_hash
1710+
and checkpoint.selected_strategy_plan_id
1711+
and checkpoint.research_contract_id
1712+
and not checkpoint.adapter_status
1713+
)
1714+
1715+
1716+
def recover_contract_subgoal_duplicate_block(
1717+
checkpoint: OrchestrationCheckpoint | None,
1718+
) -> BlockedExitEvent | None:
1719+
"""Repair only the legacy novelty block on a bound decomposition resume."""
1720+
duplicate_reason = (
1721+
"Strategy proposals were duplicates; reuse the current candidate "
1722+
"and unresolved role."
1723+
)
1724+
if (
1725+
checkpoint is None
1726+
or checkpoint.proof_state != ProofState.BLOCKED
1727+
or checkpoint.blocked_reason != duplicate_reason
1728+
or checkpoint.proof_plan_id
1729+
or checkpoint.executable_plan_node_id
1730+
or not checkpoint.research_contract_id
1731+
or not checkpoint.selected_strategy_plan_id
1732+
or not checkpoint.target_context_hash
1733+
):
1734+
return None
1735+
evidence = next(
1736+
(
1737+
str(item.get("event_id", ""))
1738+
for item in reversed(checkpoint.recovery_events)
1739+
if item.get("event_type") == "RESEARCH_CONTRACT_SUBGOAL_REQUIRED"
1740+
and item.get("target_obligation_id")
1741+
== checkpoint.target_obligation_id
1742+
and item.get("research_contract_id")
1743+
== checkpoint.research_contract_id
1744+
),
1745+
"",
1746+
)
1747+
if not evidence:
1748+
return None
1749+
event = BlockedExitEvent(
1750+
event_id=hashlib.sha256(
1751+
(
1752+
"contract-subgoal-duplicate-recovery:"
1753+
+ evidence
1754+
+ checkpoint.target_context_hash
1755+
).encode()
1756+
).hexdigest(),
1757+
event_type=BlockedEventType.VALIDATED_EVIDENCE_BACKJUMP.value,
1758+
reason="target-bound decomposition bypasses candidate novelty",
1759+
target_state=ProofState.DECOMPOSER.value,
1760+
reset_role=ProofState.DECOMPOSER.value,
1761+
metadata={"evidence_sha256": evidence},
1762+
)
1763+
apply_blocked_exit_event(checkpoint, event)
1764+
return event
1765+
1766+
16991767
def should_resume_downstream(
17001768
checkpoint: OrchestrationCheckpoint | None,
17011769
*,
@@ -1705,9 +1773,12 @@ def should_resume_downstream(
17051773
) -> bool:
17061774
"""Keep a persisted role unless an explicit Strategy policy overrides it."""
17071775
return bool(
1708-
is_resumable_checkpoint(
1709-
checkpoint,
1710-
candidate_sha256=candidate_sha256,
1776+
(
1777+
is_resumable_checkpoint(
1778+
checkpoint,
1779+
candidate_sha256=candidate_sha256,
1780+
)
1781+
or is_contract_bound_subgoal_resume(checkpoint)
17111782
)
17121783
and not (
17131784
checkpoint is not None
@@ -3065,6 +3136,28 @@ def run_supervisor_iterations(args) -> int:
30653136
cause=event.event_type,
30663137
event_id=event.event_id,
30673138
)
3139+
recovered_event = recover_contract_subgoal_duplicate_block(checkpoint)
3140+
if recovered_event is not None and orchestration_path is not None:
3141+
save_orchestration_checkpoint(orchestration_path, checkpoint)
3142+
append_blocked_event_journal(
3143+
orchestration_path.with_name(
3144+
"proof_orchestration.journal.jsonl",
3145+
),
3146+
recovered_event,
3147+
before_state=ProofState.BLOCKED.value,
3148+
after_state=checkpoint.state,
3149+
)
3150+
blocked_logger.transition(
3151+
next_state=checkpoint.state,
3152+
cause=recovered_event.event_type,
3153+
event_id=recovered_event.event_id,
3154+
)
3155+
print(
3156+
"[proof-live] stage=backjump "
3157+
"from=BLOCKED to=DECOMPOSER "
3158+
"reason=target-bound-candidate-novelty-bypass",
3159+
flush=True,
3160+
)
30683161
if (
30693162
checkpoint is not None
30703163
and route_contract_to_subgoal_generation(checkpoint)

tests/inference_engine/bench/test_autoresearch_supervisor.py

Lines changed: 52 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -40,12 +40,14 @@
4040
extract_gan_failure_reason,
4141
failure_class_for_exception,
4242
infrastructure_failure_fingerprint,
43+
is_contract_bound_subgoal_resume,
4344
is_resumable_checkpoint,
4445
is_nonfatal_semantic_continuation,
4546
parse_strategy_candidate_transport,
4647
parse_research_verdict,
4748
read_results,
4849
repair_candidate_schema,
50+
recover_contract_subgoal_duplicate_block,
4951
render_candidate,
5052
route_contract_to_subgoal_generation,
5153
run_supervisor_iterations,
@@ -100,6 +102,56 @@ def test_contract_with_executable_subgoal_does_not_backjump():
100102
assert checkpoint.proof_state == ProofState.PROOF_SEARCH
101103

102104

105+
def test_target_bound_decomposer_resume_ignores_wrapper_candidate_hash():
106+
checkpoint = OrchestrationCheckpoint(
107+
state=ProofState.DECOMPOSER.value,
108+
current_role="decomposer",
109+
target_obligation_id="RH-C0-root",
110+
proposition_hash="p" * 64,
111+
target_context_hash="c" * 64,
112+
selected_strategy_plan_id="SP-root",
113+
research_contract_id="RC-root",
114+
candidate_sha256="",
115+
)
116+
assert is_contract_bound_subgoal_resume(checkpoint)
117+
assert should_resume_downstream(
118+
checkpoint,
119+
candidate_sha256="different-wrapper-hash",
120+
force_strategy=False,
121+
strategy_trigger_exists=False,
122+
)
123+
124+
125+
def test_duplicate_wrapper_block_recovers_bound_decomposer():
126+
checkpoint = OrchestrationCheckpoint(
127+
state=ProofState.BLOCKED.value,
128+
current_role="blocked",
129+
target_obligation_id="RH-C0-root",
130+
proposition_hash="p" * 64,
131+
target_context_hash="c" * 64,
132+
selected_strategy_plan_id="SP-root",
133+
research_contract_id="RC-root",
134+
blocked_reason=(
135+
"Strategy proposals were duplicates; reuse the current candidate "
136+
"and unresolved role."
137+
),
138+
recovery_events=[{
139+
"event_type": "RESEARCH_CONTRACT_SUBGOAL_REQUIRED",
140+
"event_id": "e" * 64,
141+
"target_obligation_id": "RH-C0-root",
142+
"research_contract_id": "RC-root",
143+
}],
144+
)
145+
event = recover_contract_subgoal_duplicate_block(checkpoint)
146+
assert event is not None
147+
assert event.event_type == (
148+
BlockedEventType.VALIDATED_EVIDENCE_BACKJUMP.value
149+
)
150+
assert checkpoint.proof_state == ProofState.DECOMPOSER
151+
assert checkpoint.blocked_reason == ""
152+
assert recover_contract_subgoal_duplicate_block(checkpoint) is None
153+
154+
103155
def test_live_status_atomic_transitions_and_permissions(tmp_path):
104156
path = tmp_path / "proof_live_status.json"
105157
status = AtomicLiveStatus(

0 commit comments

Comments
 (0)