Files
workinf_Blender_Wasm/my-tools/wbv2_implementation_loop.py
mes123456 f3e21c6b98
Some checks failed
M6 deployable RC / quick (push) Has been cancelled
M6 deployable RC / chromium (push) Has been cancelled
M6 deployable RC / release (push) Has been cancelled
chore: checkpoint web Blender migration source, tools and docs
Accumulated uncommitted work: GHOST Web backend, WebGL/GPU web layer,
web workflow engine and protocol, tooling, docs and task records.
Large generated evidence, user-testing archives, build snapshots and
imported test assets remain untracked.

Co-Authored-By: Claude Code <noreply@anthropic.com>
2026-11-10 11:18:01 +08:00

1764 lines
122 KiB
Python

#!/usr/bin/env python3
"""Serial implementation-only continuation for the current authorized WBV2 task."""
from __future__ import annotations
import argparse
from datetime import datetime, timezone
import hashlib
import json
import os
from pathlib import Path
import re
import signal
import subprocess
import sys
import time
from wbv2_autopilot import Lease, ProjectFence, alive, identity, now, queue_context, read_json, write_json
from wbv2_gap_progress import resolve_gap
from wbv2_payload_budget import prepare_submission, update_budget
ROOT = Path(__file__).resolve().parents[1]
TASK = 'WBV2-P1424-001'
DIRECTORY = ROOT / 'tests/golden' / TASK / 'unattended-implementation'
DELIVERY_PLAN = ROOT / 'tests/golden' / TASK / 'desktop-delivery-manifest.json'
FOCUSES = [
('bpy', 'Close one concrete menu/F3/property workflow gap using the existing native bpy/RNA and embedded CPython. Reuse registration and lifetime work; change only the missing call or dependency needed by this user entry.'),
('gpu', 'Continue the current native browser GPU implementation from this focus handoff and its exact nextSourcePaths. Reuse the existing gpu/webgl backend, GHOST Web context, wmWindow, WM event delivery and editor drawing. Implement the next missing backend interface and its actual native UI/editor caller together. Existing source and generated artifacts are implementation inputs, not proof of full native UI completion. Native streaming, custom rectangle drawing and React panels do not count as native WASM GPU UI.'),
('editors', 'Implement missing actual editor operations and contexts in the pure WASM path, reusing registered native workspace data and existing production routes. Prioritize actual operations over more read-only panels.'),
]
# Complete adjacent operations as a family before rotating productive work.
# This bounds continuation; it is neither a six-turn target nor proof that
# repeated packaging explains all observed elapsed time.
COHERENT_FOCUS_TURNS = 6
CANDIDATE_REFRESH_GAP = 6
# Bound delivery delay even when subsequent attempts produce no source changes.
CANDIDATE_MAX_DEFERRED_TURNS = 6
INCREMENTAL_BUILD_ONLY = True
ANCHORS = [
'blender-5.2.0/source/blender/web_engine/web_engine_python.cpp',
'blender-5.2.0/source/blender/web_engine/web_engine_python_bootstrap.py',
'blender-5.2.0/source/blender/web_engine/web_engine_api.cpp',
'blender-5.2.0/source/blender/web_engine/web_engine_main_state.cpp',
'blender-5.2.0/source/blender/web_engine/web_rna_fallback.cpp',
'blender-5.2.0/source/blender/web_engine/CMakeLists.txt',
'blender-5.2.0/source/blender/python/intern/bpy.cc',
'blender-5.2.0/source/blender/python/intern/bpy_rna.cc',
'blender-5.2.0/source/blender/python/intern/CMakeLists.txt',
'blender-5.2.0/source/blender/gpu/CMakeLists.txt',
'blender-5.2.0/intern/ghost/CMakeLists.txt',
'web/app/src/workers/web-engine.worker.ts',
'web/app/src/app/App.tsx',
]
def canonical_continuation():
context = read_json(ROOT / 'tests/golden' / TASK / 'task-context.json', {})
continuation = context.get('continuation', {})
return continuation if continuation.get('status') == 'stream_recovery' else {}
def is_source_read_command(command):
if any(marker in command for marker in ('apply_patch', '.write_text(', '.write_bytes(', 'sed -i')):
return False
return ('search-wbv2-batch-source.mjs' in command and ' --pattern ' in command or
any(prefix in command for prefix in ('blender-5.2.0/source/', 'web/app/src/', 'web/protocol/')) and bool(re.search(r'\b(?:cat|head|tail|sed|rg)\b|\.read_(?:text|bytes)\(', command)))
def schema():
strings = {'type': 'array', 'items': {'type': 'string'}}
fields = {
'status': {'type': 'string', 'enum': ['implemented_unverified', 'blocked']},
'summary': {'type': 'string'}, 'workflow': {'type': 'string'},
'changedFiles': strings, 'sourceReferences': strings,
'remaining': strings, 'nextFocus': {'type': 'string'},
'continuationNotes': {'type': 'string'},
'allRequestedImplementationComplete': {'type': 'boolean'},
'focusImplementationComplete': {'type': 'boolean'},
'batchReadyForIntegration': {'type': 'boolean'},
'build': {
'type': 'object', 'additionalProperties': False,
'required': ['status', 'logPath', 'blockingSymbols', 'artifactPaths'],
'properties': {
'status': {'type': 'string', 'enum': ['not_run', 'compile_blocked', 'link_blocked', 'artifact_generated']},
'logPath': {'type': 'string'}, 'blockingSymbols': strings, 'artifactPaths': strings,
},
},
'businessEntry': {'type': 'string'},
'nextSourcePaths': strings,
'efficiencyReview': {
'type': 'object', 'additionalProperties': False,
'required': ['inspected', 'inspectedRoutes', 'findings'],
'properties': {
'inspected': {'type': 'boolean'}, 'inspectedRoutes': strings,
'findings': {'type': 'array', 'maxItems': 6, 'items': {
'type': 'object', 'additionalProperties': False,
'required': ['id', 'kind', 'route', 'sourcePaths', 'reason', 'action', 'status'],
'properties': {
'id': {'type': 'string'},
'kind': {'type': 'string', 'enum': ['placeholder', 'business_blocker', 'idle_repetition']},
'route': {'type': 'string'}, 'sourcePaths': strings,
'reason': {'type': 'string'}, 'action': {'type': 'string'},
'status': {'type': 'string', 'enum': ['open', 'implemented_unverified']},
},
}},
},
},
'delivery': {
'type': 'object', 'additionalProperties': False,
'required': ['stageId', 'status', 'completedRoutes', 'remainingRoutes', 'blockingRoute', 'nextAction', 'sourcePaths'],
'properties': {
'stageId': {'type': 'string'},
'status': {'type': 'string', 'enum': ['in_progress', 'blocked', 'implemented_unverified']},
'completedRoutes': strings, 'remainingRoutes': strings,
'blockingRoute': {'type': 'string'}, 'nextAction': {'type': 'string'},
'sourcePaths': strings,
},
},
}
return {'type': 'object', 'additionalProperties': False, 'required': list(fields), 'properties': fields}
def workspace_relative(value):
if not isinstance(value, str):
return None
relative = Path(value)
if relative.is_absolute():
try:
relative = relative.relative_to(ROOT)
except ValueError:
return None
if '..' in relative.parts or not (ROOT / relative).resolve().is_relative_to(ROOT):
return None
return relative.as_posix()
def production_path(value):
value = workspace_relative(value)
if value is None:
return None
if value.startswith('web/app/src/app/engine-client/'):
value = 'web/app/src/engine-client/' + value[len('web/app/src/app/engine-client/'):]
relative = Path(value)
if not value.startswith(('blender-5.2.0/source/', 'blender-5.2.0/intern/', 'blender-5.2.0/build_files/', 'blender-5.2.0/scripts/', 'web/app/src/', 'web/protocol/', 'tools/web/')):
return None
if relative.suffix not in {'.cc', '.cpp', '.hh', '.h', '.c', '.ts', '.tsx', '.js', '.mjs', '.py', '.css', '.cmake', '.txt', '.in', '.glsl', '.wgsl'}:
return None
path = ROOT / relative
if not path.resolve().is_relative_to(ROOT):
return None
return path
def combined_native_build(build):
"""Interpret an existing artifact handoff, without checking/running binaries."""
if build.get('status') != 'artifact_generated' or build.get('blockingSymbols'):
return False
paths = {path for value in build.get('artifactPaths', [])
if (path := workspace_relative(value))}
return {'build_web_blender6/bin/web_engine.js',
'build_web_blender6/bin/web_engine.wasm'}.issubset(paths)
def candidate_handoff_paths(build, stage_id, source_paths=()):
"""Consume explicit package outputs, including directory-shaped handoffs.
This reads package metadata only, not runtime evidence. A successful package
followed by a source edit cannot clear that edit from the pending queue.
"""
if build.get('status') != 'artifact_generated' or build.get('blockingSymbols'):
return []
newest_source = 0
newest_native_source = 0
for value in source_paths:
path = production_path(value)
if path is not None:
try:
newest_source = max(newest_source, path.stat().st_mtime_ns)
if path.suffix in {'.cc', '.cpp', '.c', '.hh', '.h', '.glsl', '.py'}:
newest_native_source = max(newest_native_source, path.stat().st_mtime_ns)
except OSError as error:
print(f'Candidate handoff deferred: source input {value}: {error}', file=sys.stderr, flush=True)
return []
candidates = []
for original in build.get('artifactPaths', []):
value = workspace_relative(original)
if not value or not value.startswith('build_web_main_desktop/'):
continue
path = ROOT / value
if path.is_dir():
path = path / 'candidate.json'
if path.name != 'candidate.json' or not path.resolve().is_relative_to(ROOT):
continue
try:
record = read_json(path, {})
packaged_wasm = path.parent / 'public/vendor/blender/web_engine.wasm'
if newest_native_source and packaged_wasm.stat().st_mtime_ns < newest_native_source:
print(f'Candidate handoff deferred: {value}; packaged WASM predates native source; incremental link required', file=sys.stderr, flush=True)
continue
desktop_application = (record.get('stageId') == 'main-desktop' and
record.get('entry') == 'app/index.html' and (path.parent / 'app/index.html').is_file())
if ((record.get('stageId') != stage_id and not desktop_application) or
record.get('status') != 'implemented_unverified' or
path.stat().st_mtime_ns < newest_source):
print(f'Candidate handoff deferred: {value}; stage={record.get("stageId")}/{stage_id}; status={record.get("status")}; source_ns={newest_source}; candidate_ns={path.stat().st_mtime_ns}', file=sys.stderr, flush=True)
continue
except (OSError, ValueError, AttributeError):
continue
relative = path.relative_to(ROOT).as_posix()
if relative not in candidates:
candidates.append(relative)
return candidates
def stamp(path):
try:
stat = path.stat()
with path.open('rb') as stream:
digest = hashlib.file_digest(stream, 'sha256').hexdigest()
return (stat.st_size, stat.st_mtime_ns, digest)
except FileNotFoundError:
return None
def read_handoff(directory):
try:
result = read_json(directory / 'result.json', {})
except ValueError:
# Some completed provider turns prepend prose to the structured result.
# Recover the actual final object; never fabricate a successful handoff.
try:
with (directory / 'result.json').open(encoding='utf-8') as stream:
text = stream.read(128 * 1024 + 1)
except (OSError, UnicodeError):
return None
if len(text) > 128 * 1024:
return None
result = None
decoder = json.JSONDecoder()
for offset, character in enumerate(text):
if character != '{' or (offset and text[offset - 1] != '\n'):
continue
try:
candidate, consumed = decoder.raw_decode(text, offset)
except ValueError:
continue
if isinstance(candidate, dict) and not text[consumed:].strip().strip('`'):
result = candidate
break
except OSError:
return None
if not isinstance(result, dict) or result.get('status') not in {'implemented_unverified', 'blocked'}:
return None
for field in ('changedFiles', 'sourceReferences', 'remaining'):
values = result.get(field)
if not isinstance(values, list) or len(values) > 128 or not all(isinstance(value, str) for value in values):
return None
if not all(isinstance(result.get(field), str) for field in ('summary', 'workflow', 'nextFocus')):
return None
# Legacy handoffs predate this required field in the new worker schema.
# Preserve their source/candidate records; do not fabricate a breakpoint.
if 'continuationNotes' in result and not isinstance(result['continuationNotes'], str):
return None
if ('batchReadyForIntegration' in result and
not isinstance(result['batchReadyForIntegration'], bool)):
return None
build = result.get('build')
if build is not None:
if not isinstance(build, dict) or build.get('status') not in {'not_run', 'compile_blocked', 'link_blocked', 'artifact_generated'}:
return None
if not isinstance(build.get('logPath'), str):
return None
for field in ('blockingSymbols', 'artifactPaths'):
if not isinstance(build.get(field), list) or not all(isinstance(value, str) for value in build[field]):
return None
if len(build['blockingSymbols']) > 24 or len(build['artifactPaths']) > 128:
return None
return result
def compact_handoff(value):
return {
'summary': value.get('summary', '')[:700],
'nextFocus': value.get('nextFocus', '')[:1200],
'remaining': [item[:400] for item in value.get('remaining', [])[:6]],
'changedFiles': value.get('changedFiles', [])[:12],
'nextSourcePaths': value.get('nextSourcePaths', [])[:8],
'businessEntry': value.get('businessEntry', '')[:400],
'continuationNotes': value.get('continuationNotes', '')[:1200],
}
def provider_error_kind(event):
# Persist a category, never the raw provider body, headers or credentials.
message = json.dumps(event.get('error') or event.get('message', '')).lower()
if any(word in message for word in ('context_length_exceeded', 'maximum context length', 'request too large', 'payload too large', '413')):
return 'payload_limit'
if any(word in message for word in ('unauthorized', 'authentication', 'access forbidden',
'access denied', 'permission denied', '401', '403')):
return 'authentication'
if any(word in message for word in ('429', 'rate_limit', 'rate limit', 'quota')):
return 'rate_limit'
if any(word in message for word in ('overloaded', 'server_overloaded', 'servers are busy', 'server is busy', 'at capacity')):
return 'overloaded'
if any(word in message for word in ('timeout', 'timed out', 'connection', 'disconnected', 'stream closed', 'network', '502', '503', '504', '524')):
return 'transport'
return 'unclassified'
class ImplementationLoop:
def __init__(self, args):
self.args = args
self.child = None
self.stop_requested = False
self.state = {}
self.log_offset = 0
self.owns_project = False
self.focuses = {}
self.observed_files = set()
self.last_source_reads = []
self.program_stamp = Path(__file__).stat().st_mtime_ns
self.payload_program_stamp = Path(__file__).with_name('wbv2_payload_budget.py').stat().st_mtime_ns
self.delivery_stage = None
self.efficiency_review = {}
def efficiency_due(self):
monitor = self.state.get('efficiency', {})
return (time.time() >= monitor.get('nextReviewAt', 0)
or monitor.get('handoffsSinceReview', 0) >= 3)
def prepare_efficiency_review(self, focus):
monitor = self.state.setdefault('efficiency', {})
branch = self.focuses.get(focus, {})
reason = branch.get('efficiencyUrgent')
# Rotate bounded issue context. Taking the first three dict entries forever
# starves later gaps, even when every new handoff repeats them as "open".
pending = [value for value in monitor.get('issues', {}).values()
if value['stageId'] == self.delivery_stage['id'] and value['focus'] == focus
and value['status'] == 'open']
pending.sort(key=lambda value: (value.get('lastOfferedCycle', 0),
value.get('firstSeenCycle', value.get('cycle', 0)),
value['id']))
selected = pending[:3]
stalled = any(value.get('reportsWithoutSourceProgress', 0) >= 3 for value in selected)
reason = reason or ('open_gap_carried_without_source_progress' if stalled else None)
due = bool(reason) or self.efficiency_due()
issues = [dict(id=value['id'], kind=value['kind'], route=value['route'],
sourcePaths=value['sourcePaths'][:4], action=value['action'],
reportsWithoutSourceProgress=value.get('reportsWithoutSourceProgress', 0))
for value in selected]
for value in selected:
value['lastOfferedCycle'] = self.state['cycle']
self.efficiency_review = dict(due=due, reason=reason or ('interval_or_handoff_count' if due else 'routine_implementation'),
openIssues=issues, remainingOpenIssueCount=len(pending))
monitor['scheduled'] = dict(cycle=self.state['cycle'], focus=focus, **self.efficiency_review)
def record_efficiency(self, focus, result, cycle, changed):
monitor = self.state.setdefault('efficiency', {})
monitor['handoffsSinceReview'] = monitor.get('handoffsSinceReview', 0) + 1
report = result.get('efficiencyReview', {})
if not isinstance(report, dict):
return
findings = report.get('findings', [])
if not isinstance(findings, list):
return
for finding in findings[:6]:
if not isinstance(finding, dict) or not all(isinstance(finding.get(key), str)
for key in ('id', 'kind', 'route', 'reason', 'action', 'status')):
continue
if not finding['id'].strip() or not isinstance(finding.get('sourcePaths'), list):
continue
paths = [path for value in finding['sourcePaths'][:8]
if (path := workspace_relative(value)) and production_path(path)]
if not paths or finding['kind'] not in {'placeholder', 'business_blocker', 'idle_repetition'}:
continue
# Several gap families share these files. File overlap cannot
# assign a finding to a fixed ID or prove its full completion.
normalized_id = finding['id'][:120]
key = self.delivery_stage['id'] + ':' + focus + ':' + normalized_id
previous = monitor.get('issues', {}).get(key, {})
source_progress = any(path in changed for path in paths)
fixed = finding['status'] == 'implemented_unverified' and source_progress
monitor.setdefault('issues', {})[key] = dict(
id=normalized_id, stageId=self.delivery_stage['id'], focus=focus,
kind=finding['kind'], route=finding['route'][:600], sourcePaths=paths,
reason=finding['reason'][:1000], action=finding['action'][:1000], cycle=cycle,
firstSeenCycle=previous.get('firstSeenCycle', previous.get('cycle', cycle)),
lastOfferedCycle=previous.get('lastOfferedCycle', 0),
lastSourceProgressCycle=cycle if source_progress else previous.get('lastSourceProgressCycle', 0),
reportsWithoutSourceProgress=0 if source_progress else previous.get('reportsWithoutSourceProgress', 0) + 1,
status='implemented_unverified' if fixed else 'open', acceptance='NOT_CLAIMED')
routes = report.get('inspectedRoutes')
if report.get('inspected') is True and isinstance(routes, list) and routes and all(isinstance(route, str) for route in routes):
monitor.update(lastReviewAt=now(), lastReviewCycle=cycle, nextReviewAt=time.time() + 1800,
handoffsSinceReview=0, lastInspectedRoutes=routes[:8])
monitor.setdefault('focusReviewedAt', {})[focus] = cycle
self.focuses[focus].pop('efficiencyUrgent', None)
def next_delivery_stage(self):
# These are implementation handoffs, not verification or acceptance.
plan = read_json(DELIVERY_PLAN, {})
if plan.get('task') != TASK or not plan.get('stages'):
raise RuntimeError('Missing current desktop delivery plan for ' + TASK)
progress = self.state.get('deliveryStages', {})
canonical = self.state.get('canonicalContinuation', {})
if canonical and canonical.get('focus', '').startswith(('glb-', 'zero-')):
return next((stage for stage in plan['stages'] if stage['id'] == 'modeling-assets'), plan['stages'][0])
pending = []
for stage in plan['stages']:
recorded = progress.get(stage['id'], {}).get('focuses', {})
if not all(recorded.get(name, {}).get('reportedImplemented') is True
for name in stage['focusOrder']):
pending.append(stage)
if not pending:
return None
policy = plan.get('gapScheduling', {})
# An explicit user priority changes only work selection, not completion.
# Priority chooses the first visit; bounded rotation still limits
# subsequent visits so independent unfinished groups cannot starve.
priority = None if int(self.state.get('noProgressCycles', 0)) >= 8 else next(
(stage for stage in pending if stage['id'] == policy.get('priorityStage')), None)
if priority and (policy.get('mode') != 'bounded_stage_rotation' or
not self.state.get('gapStageRotation', {}).get('stageId')):
return priority
if policy.get('mode') != 'bounded_stage_rotation':
return pending[0]
# A stage is a feature owner, not an all-or-nothing dependency barrier.
# Keep actual completion rules unchanged; only distribute unfinished work.
rotation = self.state.get('gapStageRotation', {})
current_id = rotation.get('stageId', self.state.get('deliveryStage', {}).get('id'))
current = next((stage for stage in pending if stage['id'] == current_id), None)
limit = max(1, min(12, int(policy.get('maxTurnsPerVisit', 6))))
if current and rotation.get('turns', 0) < limit:
return current
alternatives = [stage for stage in pending if stage['id'] != current_id]
if not alternatives:
return pending[0]
visits = rotation.get('lastVisited', {})
# Stable list order selects unvisited groups in plan order; after that,
# the least recently visited unfinished group gets another repair window.
return min(alternatives, key=lambda stage: visits.get(stage['id'], 0))
def concrete_gap_items(self, focus):
stage = self.delivery_stage
findings = self.state.get('efficiency', {}).get('issues', {})
delivery = self.state.get('deliveryStages', {}).get(stage['id'], {}).get('focuses', {}).get(focus, {})
items = []
for seed in stage.get('gapItems', {}).get(focus, []):
finding = findings.get(stage['id'] + ':' + focus + ':' + seed['id'])
item = resolve_gap(seed, finding, delivery)
items.append(item)
return items
def record_delivery(self, focus, result, cycle, changed):
stage = self.delivery_stage
report = result.get('delivery')
if not isinstance(report, dict) or report.get('stageId') != stage['id']:
return
for key in ('completedRoutes', 'remainingRoutes', 'sourcePaths'):
if not isinstance(report.get(key), list) or not all(isinstance(item, str) for item in report[key]):
return
paths = [value for value in report['sourcePaths'][:16] if production_path(value)]
complete = (report.get('status') == 'implemented_unverified'
and result.get('status') == 'implemented_unverified'
and bool(report['completedRoutes']) and bool(paths)
and report['remainingRoutes'] == [] and report.get('blockingRoute') == '')
complete = complete and not any(issue['stageId'] == stage['id'] and issue['focus'] == focus
and issue['status'] == 'open'
for issue in self.state.get('efficiency', {}).get('issues', {}).values())
complete = complete and all(item.get('status') == 'implemented_unverified'
for item in self.concrete_gap_items(focus))
if stage['id'] == 'integration-remainder':
complete = (complete and result.get('focusImplementationComplete') is True
and result.get('remaining') == [])
entry = self.state.setdefault('deliveryStages', {}).setdefault(stage['id'], {'focuses': {}})
previous = entry['focuses'].get(focus, {})
blocker = str(report.get('blockingRoute', '')).strip().casefold()
previous_blocker = previous.get('blockingRoute', '').strip().casefold()
# Rephrasing an unchanged blocker must not reset recovery indefinitely.
# Actual source progress on the same route is not classified as idleness.
route_identity = hashlib.sha256(json.dumps(
[str(result.get('businessEntry', '')).strip().casefold(), sorted(set(paths))],
ensure_ascii=False).encode('utf-8')).hexdigest()
repeated = bool(blocker) and (blocker == previous_blocker or
(bool(previous_blocker) and not changed and
route_identity == previous.get('blockingRouteIdentity')))
repeats = previous.get('sameBusinessBlockerRounds', 0) + 1 if repeated else int(bool(blocker))
if repeats >= 2:
self.focuses[focus]['efficiencyUrgent'] = 'same_business_blocker_in_two_handoffs'
entry['focuses'][focus] = dict(
cycle=cycle, status=report.get('status'), reportedImplemented=complete,
sameBusinessBlockerRounds=repeats,
completedRoutes=report['completedRoutes'][:64], remainingRoutes=report['remainingRoutes'][:64],
blockingRoute=str(report.get('blockingRoute', ''))[:1000],
blockingRouteIdentity=route_identity,
nextAction=str(report.get('nextAction', ''))[:1200], sourcePaths=paths,
scopeRemaining=result.get('remaining', [])[:128],
reportPath=str((DIRECTORY / f'cycle-{cycle:06d}' / 'result.json').relative_to(ROOT)),
validation='NOT_RUN', acceptance='NOT_CLAIMED')
def checkpoint(self, state, **values):
self.state.update(values)
focus = self.state.get('focus')
rounds = self.focuses.get(focus, {}).get('noProgressRounds', 0)
stalled = self.state.get('noProgressCycles', 0)
self.state['noProgressWarningScope'] = 'global_completed_batches'
self.state['focusNoProgressWarning'] = f'{focus} 最近 {rounds} 个批次无源码或有效交付进展' if rounds >= 2 else None
self.state['noProgressWarning'] = f'连续 {stalled} 个批次无源码或有效交付进展;需改变修复策略' if stalled >= 2 else None
self.state.update(schemaVersion=1, task=TASK, pid=os.getpid(), identity=identity(os.getpid()),
state=state, updatedAt=now(), validation='NOT_RUN', acceptance='NOT_CLAIMED',
focusHandoffs=self.focuses)
write_json(DIRECTORY / 'state.json', self.state)
def consume_foreground_candidate(self):
request_path = DIRECTORY / 'foreground-candidate.json'
self.foreground_request_stamp = request_path.stat().st_mtime_ns if request_path.exists() else 0
request = read_json(DIRECTORY / 'foreground-candidate.json', {})
if not request or request.get('task') != TASK:
return
manifest_path = workspace_relative(request.get('manifestPath', ''))
if not manifest_path or not manifest_path.startswith(f'tests/golden/{TASK}/'):
return
manifest = read_json(ROOT / manifest_path, {})
cycles = request.get('sourceCycles', [])
if (manifest.get('task') != TASK or manifest.get('status') != 'implemented_unverified' or
not cycles or any(type(value) is not int or value <= 0 or value > self.state.get('cycle', 0) for value in cycles)):
return
candidate = manifest.get('candidate', {})
if (self.state.get('foregroundCandidateManifest') == manifest_path and
self.state.get('applicationCandidate', {}).get('path') == candidate.get('path') and
not any(value in self.state.get('pendingSourceCycles', []) for value in cycles)):
return
paths = set()
for source in manifest.get('sources', []):
path = production_path(source.get('path', ''))
actual = stamp(path) if path else None
if not actual or actual[2] != source.get('sha256'):
return
paths.add(source['path'])
if not paths:
return
for cycle in cycles:
directory = DIRECTORY / f'cycle-{cycle:06d}'
process = read_json(directory / 'process.json', {})
handoff = read_handoff(directory)
if not process and not handoff:
return
paths.update(value for value in process.get('changedProductionFiles', []) if production_path(value))
paths.update(value for value in (handoff or {}).get('changedFiles', []) if production_path(value))
accepted = candidate_handoff_paths(dict(status='artifact_generated', artifactPaths=[candidate.get('path', '')]), 'main-desktop', paths)
if not accepted:
return
candidate_path = ROOT / accepted[0]
previous_candidate_path = self.state.get('applicationCandidate', {}).get('path')
candidate_stamp = stamp(candidate_path)
if not candidate_stamp or candidate_stamp[2] != candidate.get('sha256'):
return
for name in ('web_engine.js', 'web_engine.wasm'):
actual = stamp(candidate_path.parent / 'public/vendor/blender' / name)
if not actual or actual[2] != candidate.get('nativeSha256', {}).get(name):
return
if max(cycles) < self.state.get('applicationCandidate', {}).get('cycle', 0):
return
self.state['applicationCandidate'] = dict(cycle=max(cycles), path=accepted[0], basis='foreground artifact handoff; unverified')
self.state['foregroundCandidateManifest'] = manifest_path
self.state['pendingSourceCycles'] = [cycle for cycle in self.state.get('pendingSourceCycles', []) if cycle not in cycles]
self.state['noProgressCycles'] = 0
self.state.pop('batchIntegrationRequested', None)
self.state['rootRepair'] = dict(reportPath=request.get('reportPath', manifest_path), resultPath=manifest_path)
if (self.state.get('integrationRetry', {}).get('cycle') == request.get('supersededWaitCycle') or
(previous_candidate_path and accepted[0] != previous_candidate_path)):
self.state.pop('integrationRetry', None)
for branch in self.focuses.values():
if (branch.get('interrupted') or {}).get('cycle') in cycles:
branch.pop('interrupted', None)
self.checkpoint('handed_off', reason='foreground_candidate_consumed', childPid=None)
def integration_reason(self):
"""Delivery scheduling only; never implies functional acceptance."""
pending = self.state.get('pendingSourceCycles', [])
if self.state.get('integrationRetry', {}).get('retryAt', 0) > time.time():
return None
if len(pending) >= CANDIDATE_REFRESH_GAP:
return 'source_batch_limit'
if self.state.get('pendingSourceHistoryIncomplete', False):
return 'source_history_needs_reconciliation'
if not pending:
return None
if self.state.get('batchIntegrationRequested'):
return 'coherent_batch_ready'
if int(self.state.get('cycle', 0)) - min(pending) >= CANDIDATE_MAX_DEFERRED_TURNS:
return 'pending_delivery_deferred'
return None
def stop(self, *_):
self.stop_requested = True
self.signal_child(signal.SIGTERM)
def signal_child(self, sig):
if self.child and self.child.poll() is None:
try:
os.killpg(self.child.pid, sig)
except ProcessLookupError:
pass
def prompt(self, focus, previous, cycle):
branch = self.focuses.get(focus[0], {})
stage = self.delivery_stage
stage_handoff = self.state.get('deliveryStages', {}).get(stage['id'], {}).get('focuses', {}).get(focus[0], {})
# Includes explicit cross-focus residual integration reconciliations;
# an old focus handoff alone must not reopen its packaged source work.
gap_items = self.concrete_gap_items(focus[0])
selected_gap = next((item for item in gap_items
if item.get('status') != 'implemented_unverified'), {})
selected_gap_phase = selected_gap.get('phase', 'implement')
stage_context = dict(id=stage['id'], title=stage['title'], assignment=stage['focuses'][focus[0]],
sourcePaths=stage.get('sourcePaths', {}).get(focus[0], []),
reuseReference=stage.get('reuseReferences', {}).get(focus[0]),
closureContract=stage.get('closureContracts', {}).get(focus[0]),
gapItems=gap_items, selectedGapId=selected_gap.get('id'),
selectedGapPhase=selected_gap_phase,
handoff=stage_handoff)
canonical = self.state.get('canonicalContinuation', {})
if canonical:
stage_context['canonicalContinuation'] = canonical
focus_handoff = compact_handoff(branch.get('handoff', {}))
same_stage = branch.get('handoff', {}).get('delivery', {}).get('stageId') == stage['id']
interrupted = branch.get('interrupted') or {}
path_candidates = (canonical.get('nextSourcePaths', []) +
((interrupted.get('changedFiles', []) + interrupted.get('recentSourceReads', [])) if interrupted.get('stageId') == stage['id'] else []) +
(focus_handoff.get('nextSourcePaths', []) if same_stage else [])
+ selected_gap.get('sourcePaths', [])
+ stage_handoff.get('sourcePaths', [])
+ stage.get('sourcePaths', {}).get(focus[0], []))
if not path_candidates:
card = re.search(r'(docs/tasks/[^`\s]+\.md)', stage.get('focuses', {}).get(focus[0], ''))
if card:
card_path = ROOT / card.group(1)
try:
card_text = card_path.read_text(encoding='utf-8')[:65536]
path_candidates = re.findall(
r'((?:blender-5\.2\.0/(?:source|intern|scripts)/|web/(?:app/src|protocol)/)[A-Za-z0-9_./-]+)',
card_text)
for linked in re.findall(r'`(docs/tasks/[^`\s]+\.md)`', card_text)[:2]:
linked_path = ROOT / linked
if linked_path.exists():
linked_text = linked_path.read_text(encoding='utf-8')[:65536]
path_candidates.extend(re.findall(
r'((?:blender-5\.2\.0/(?:source|intern|scripts)/|web/(?:app/src|protocol)/)[A-Za-z0-9_./-]+)',
linked_text))
except OSError:
path_candidates = []
if not path_candidates:
path_candidates = list(ANCHORS)
focused_paths = list(dict.fromkeys(path for value in path_candidates
if (path := workspace_relative(value))))[:8]
# Other focuses can already have closed this focus's old nextAction.
# Share only bounded same-stage facts from memory, never whole logs.
peer_updates = []
own_cycle = stage_handoff.get('cycle', 0)
path_set = set(focused_paths)
records = self.state.get('deliveryStages', {}).get(stage['id'], {}).get('focuses', {})
for name, record in records.items():
if name == focus[0]:
continue
peer_paths = [path for value in record.get('sourcePaths', [])
if (path := workspace_relative(value))]
if record.get('cycle', 0) <= own_cycle and not path_set.intersection(peer_paths):
continue
peer_updates.append({
'focus': name, 'cycle': record.get('cycle'),
'completedRoutes': record.get('completedRoutes', [])[:6],
'sourcePaths': peer_paths[:8], 'reportPath': record.get('reportPath'),
'validation': 'NOT_RUN',
})
peer_updates.sort(key=lambda value: value.get('cycle') or 0, reverse=True)
shared = self.state.get('sharedBuild', {})
shared_build = {key: shared.get(key) for key in ('cycle', 'focus', 'status', 'logPath', 'blockingSymbols', 'artifactPaths')}
candidate = self.state.get('applicationCandidate', {})
last_implementation = self.state.get('latestImplementation', {}).get('cycle', 0)
cycle_gap = max(0, last_implementation - candidate.get('cycle', 0))
source_gap = len(self.state.get('pendingSourceCycles', []))
candidate_due = bool(self.integration_reason()) and not self.state.get('canonicalContinuation')
integration_first = (
'Candidate refresh is due. First integrate the already implemented pending routes, '
'before opening another unrelated source investigation. Read any interrupted edits '
'affecting these inputs, finish their concrete missing dependency if necessary, then '
'use the existing incremental build tree and candidate packager once. Reuse a matching '
'already linked native pair; newer native inputs require incremental linking. If '
'integration is blocked, retain the old candidate and report the exact failing '
'dependency. A successful pending-source integration may end this turn without '
'inventing another source change, and never counts as new business functionality.'
if candidate_due else
'Candidate refresh is not due. Close the next concrete source/caller group; '
'reuse current artifacts and only compile a demonstrated necessary dependency.')
root_repair = self.state.get('rootRepair', {})
root_reference = {key: root_repair.get(key) for key in ('reportPath', 'resultPath')
if root_repair.get(key)}
combined_native_generated = combined_native_build(shared)
linked = self.state.get('lastCombinedNativeBuild', {})
linked_reference = {key: linked.get(key) for key in (
'cycle', 'focus', 'logPath', 'artifactPaths', 'reportPath') if linked.get(key) is not None}
build_direction = (
'The cited handoff generated the combined JS/WASM artifacts (unverified). Reuse them only for the source increment they contain; later native edits still require incremental web_engine generation before packaging. Implement the remaining real business endpoint without repeating solved dependencies or acceptance.'
if combined_native_generated else
'The latest handoff is not a complete JS/WASM link. Consult the retained combined-link reference before deciding another native build is needed. A newer static library does not erase that reference and cannot establish native source parity. Incorporate newer native inputs incrementally before packaging; with only frontend changes after a matching link, reuse that native pair. Retain concrete unresolved blockers.'
)
# Inject one current handoff and its residuals; full records remain in
# the manifest/state instead of being copied into every worker prompt.
gap_keys = ('id', 'entry', 'status', 'phase', 'classification', 'remainingSubroutes',
'sourcePaths', 'sourceSymbols', 'reuse', 'nextAction', 'completionCondition')
compact_gaps = [{key: item[key] for key in gap_keys if key in item}
for item in gap_items if item.get('status') != 'implemented_unverified'
or item.get('remainingSubroutes')]
current_handoff = {key: stage_handoff.get(key) for key in
('cycle', 'completedRoutes', 'remainingRoutes', 'nextAction',
'sourcePaths', 'reportPath', 'scopeRemaining')}
priority = ('INTEGRATE_PENDING' if candidate_due else
'LOCATE_SOURCE' if selected_gap_phase == 'locate' else 'IMPLEMENT_FAMILY')
recovery_assignment = (
'Integrate the pending source batches once using the matching native pair; report the candidate and end this delivery turn. Do not start another source investigation.'
if candidate_due else branch.get('recoveryDirection')
if branch.get('noProgressRounds', 0) else None)
interrupted = branch.get('interrupted')
if interrupted and interrupted.get('recentSourceReads') and not candidate_due:
recovery_assignment = ('Continue the same-stage continuationNotes exact breakpoint; consult the latest candidate before reopening old missing claims. '
'Trace only its immediate dependency in <=30-line windows. Edit only a confirmed missing edge; otherwise preserve the exact next read in the final handoff. No edit merely to satisfy a timer.')
prefer_recovery = bool(recovery_assignment and (branch.get('noProgressRounds', 0) >= 2 or
branch.get('efficiencyUrgent') or
branch.get('stalledScopeAttempts', 0) >= 2))
canonical_assignment = (canonical.get('nextAction') or '') + (' ' + canonical.get('notes', '') if canonical.get('notes') else '') if canonical else None
context = dict(task=TASK, parent='WBV2-P1424-000', cycle=cycle,
stageId=stage['id'], focus=focus[0], phase='implement' if candidate_due else selected_gap_phase,
priority=priority,
investigationBudget=dict(readCommands=self.args.source_read_command_budget, activeNonBuildSeconds=self.args.implementation_progress_timeout),
integrationRetry=(dict(self.state['integrationRetry'], active=self.state['integrationRetry'].get('retryAt', 0) > time.time()) if self.state.get('integrationRetry') else None),
assignment=(canonical_assignment if canonical else recovery_assignment if candidate_due or prefer_recovery else
((stage_handoff.get('nextAction') or stage['focuses'][focus[0]]) +
(f' {recovery_assignment}' if recovery_assignment else ''))),
focusEntry=canonical.get('focus', stage['focuses'][focus[0]]) if canonical else stage['focuses'][focus[0]],
scopeReference=str(DELIVERY_PLAN.relative_to(ROOT)),
gaps=compact_gaps, handoff=current_handoff,
continuationNotes=(canonical.get('notes', '') if canonical else focus_handoff.get('continuationNotes', '')
if same_stage else ''),
nextSourcePaths=focused_paths, peerUpdates=peer_updates[:2],
interrupted=interrupted, rootRepair=root_reference,
reuseReference=stage.get('reuseReferences', {}).get(focus[0]),
review=self.efficiency_review,
recoveryDirection=None if candidate_due else branch.get('recoveryDirection'),
noProgressRounds=branch.get('noProgressRounds', 0),
sharedBuild=shared_build, lastCombinedNativeBuild=linked_reference,
candidate=candidate, integrationReason=self.integration_reason(),
pendingSourceCycles=self.state.get('pendingSourceCycles', []),
pendingSourceHistoryIncomplete=self.state.get('pendingSourceHistoryIncomplete', False))
self.state['executionMode'] = priority
phase_instruction = {
'LOCATE_SOURCE': 'LOCATE_SOURCE: trace one exact caller/owner boundary using short source windows; do not edit, build, package, or describe locating as implementation.',
'INTEGRATE_PENDING': 'INTEGRATE_PENDING: reuse the matching source pair and package once; do not start a new feature investigation after packaging.',
'IMPLEMENT_FAMILY': 'IMPLEMENT_FAMILY: choose one real user operation and finish its adjacent UI, native call, state feedback, recovery, and persistence as one family.',
}[priority]
no_progress_warning = ('NO_PROGRESS_WARNING: this focus has repeated no-source-progress rounds; change the source-backed strategy or yield to an independent ready focus.'
if not candidate_due and branch.get('noProgressRounds', 0) >= 2 else '')
return f'''Continue authorized desktop Blender UI -> Web Blender UI gap implementation in {ROOT}.
Task/parent/nextTask stay unchanged. Reuse existing Blender 5.2, embedded bpy, WM/GHOST/GPU, OCCT and WASM artifacts.
CURRENT CONTEXT: {json.dumps(context, ensure_ascii=False, separators=(',', ':'))}
Runner already supplies the queue, task context and parent records. Use the current handoff report and at most eight exact production entrypoints. Search Blender with node tools/web/search-wbv2-batch-source.mjs --source <exact-Blender-file> --pattern <text>; use --runtime for Web files. Do not load whole inventories, old worker logs, roadmaps or status history. Read only the relevant function ranges from interrupted work. Hard batch limits: each source window <=30 lines; each edit <=2 files and <=10 changed lines including handoff; use the configured investigation budget ({self.args.source_read_command_budget} reads / {self.args.implementation_progress_timeout} active non-build seconds) to resolve one missing edge; edit only when source evidence is sufficient. LOCATE_SOURCE remains read-only. If the edge already exists or remains uncertain, hand off the exact next dependency; never manufacture an edit to satisfy a read count or timer. Output only a short summary plus key fragments. End every batch with continuationNotes containing the exact breakpoint path/line, completed fact, remaining edge and next direct action; the next batch starts there and must not repeat a whole-repository search.
The source search command requires an exact allowlisted file with --source or --runtime; never invoke it with --runtime alone, a directory, pattern-only query, or invented unit id. If the exact file is not in CURRENT CONTEXT.nextSourcePaths or a named source reference, use the supplied path first and keep the search bounded.
Priority {priority}: {phase_instruction} {no_progress_warning}
If integrationRetry.retryAt is still in the future, retain its blocker, pending edits, file ownership and old candidate; work on an independent unfinished route instead of repeating the blocked integration. Do not treat an unreleased foreground file as available or rebuild its partial inputs.
Efficiency order: pending source batches are integrated before unrelated work; a
delivery-only turn packages once and then ends. When pendingSourceCycles is empty,
do not rebuild or repackage an unchanged candidate; select a new source-backed
operation. Candidate generation, build time and service uptime never count as
business progress.
{integration_first if priority != 'LOCATE_SOURCE' else 'Locate only; artifact generation is not permitted in this phase.'}
INTEGRATE_PENDING is a delivery-only turn: reuse a matching already linked pair or incrementally generate web_engine, package once with tools/web/build-main-desktop-candidate.py --output <new-directory>, write the exact candidate.json path in the final handoff, then END THE TURN. Do not start a new feature investigation after successful packaging. On failure repair the named dependency from source or hand off its exact error; do not rerun unchanged inputs. A partial archive is not a combined pair; later native edits remain pending. Packaging is not feature completion. In locate phase perform only bounded source tracing, no edits/build/package; report the exact missing edge or existing implementation with concrete next action.
Return batchReadyForIntegration=true when this coherent user-operation family has complete source wiring and its remaining work is delivery to an application candidate; false for partial source work or unresolved source dependencies. This flag does not close a gap/focus or establish acceptance. Do not wait for six unrelated edits after a complete family. The parent schedules integration on this explicit request, six pending source batches, or pending work deferred across {CANDIDATE_MAX_DEFERRED_TURNS} turns. Preserve every unrelated residual and source question. If an unchanged build is blocked, repair its named source dependency before any retry.
{phase_instruction} Recent same-stage source and handoffs supersede old missing/nextAction claims. An unverified existing route is not a reason to reimplement it. Reuse completed siblings. UI parity includes labels, grouping, enabled/visible/hover states, shortcuts and feedback; APIs alone do not finish a panel. Operators must be usable from normal UI without JSON/Python input. Keep all real residuals with their original owner. Same file != same completed workflow. Avoid unrelated refactors, generic GPU hardening and per-field microtasks. After two unproductive attempts, change the concrete source-backed repair strategy or implement another ready subroute, retaining the original unknown. Never edit merely to reset a timer.
Native functionality must use original Blender/WASM; OCCT is the geometry kernel and Three.js only displays its output. Preserve native errors and resource ownership. No invented success, JS geometry simulation, stub, second implementation or streaming substitute. Reuse local artifacts before downloading/building dependencies. Node bake storage/find_bake ownership already lives in MOD_nodes_storage.cc; if relevant use tests/golden/{TASK}/nodes-bake-storage-implementation.md.
Batch discipline: name the finite user-operation family, included adjacent actions, reused implementation and missing native edge before editing. Sharing a file or focus is insufficient to group unrelated work. Keep separate short UI-interaction and native-function residuals; a callable button does not establish complete desktop menu/region parity. Prioritize known data/recovery failures and blocked normal workflows. This worker is the assigned production writer; if overlapping external edits are discovered, stop your conflicting writes and hand off exact paths without overwriting, restarting services or changing control state. Distinguish healthy bounded build/resource waits, source blockers and provider backoff from repeated investigation without progress. Report concrete routes changed, source wiring, candidate inclusion and user acceptance separately; unknowns remain explicit, and cycles/builds/repackaging never count as new functionality. Use the current handoff over stale missing claims. Existing timeout, fairness, retry and no-validation rules still apply.
Small-context rule: read only short source windows needed for the current call chain and cap command output and handoff summaries. Do not concatenate full files, long logs, or repeated history. When the read/output budget is reached, stop expanding context, record exact paths and the next action, and hand off to the next safe turn; avoid long-context compaction.
Build only to generate required artifacts: incremental existing tree, fixed -j8, affected target first. Full link/package after a coherent family or {CANDIDATE_REFRESH_GAP} pending source batches. No clean/cache deletion, timestamp forcing, repeated unchanged builds or deployment. Redirect build output to this cycle log, return exit code and at most 24 distinct diagnostics. Never dump whole linker output. No tests, browser/smoke/regression, type/syntax/lint/context checks, screenshots, inventory/evidence audits or pass receipts during implementation. Preserve dirty changes; no reset/checkout/clean, commit/push, other agents/schedulers or persistent servers. Do not edit unattended control files or queue pointers.
Investigation budget: after {self.args.implementation_progress_timeout} active non-build seconds and {self.args.source_read_command_budget} read commands without production changes, the supervisor may end the turn at a command boundary. Source-idle limit is {self.args.source_progress_timeout}s. Active artifact generation/provider reconnect have separate limits; shorter timers are not the objective. Keep reports concise and finish a coherent handoff before more exploration. A source change count is not feature coverage. Report provider limits as external errors, never bypass them.
Write tests/golden/{TASK}/unattended-reports/cycle-{cycle:06d}.md with 处理模块 | 对标项 | 源码落点 | 路由入口 | WASM边界 | 状态 | 结论, exact changed production paths, grouped completed routes, concrete residuals and next direct call, plus a short continuationNotes paragraph. Keep validation=NOT_RUN, acceptance=NOT_CLAIMED. Do not update aggregate manifests. Return the provided JSON schema: build.status reflects commands actually run; artifactPaths includes exact candidate.json; not_run has empty log/artifacts/blockers. delivery.stageId={stage['id']}. Use exact supplied gap IDs in efficiencyReview.findings; inspected=false if no review was performed. nextSourcePaths contains up to eight exact paths. Remaining entries name '[ID] user operation -> function/resource -> missing behavior'; distinguish not implemented, unresolved source question, implemented-unverified and external blocker. Preserve broader ownership in remaining, never erase scope to claim completion. Whole-focus completion requires every assignment implemented with no actual residual; allRequestedImplementationComplete remains false while any requested UI/function remains. Compilation and heartbeats never prove completion. Return immediately after the final structured handoff.
'''
def run_worker(self, directory, prompt, source_baseline=None):
self.observed_files, self.last_source_reads = set(), []
write_json(directory / 'result.schema.json', schema())
prompt = prepare_submission(prompt, (directory / 'result.schema.json').stat().st_size, directory, self.state)
if prompt is None:
self.state['providerErrorKind'] = 'payload_limit'
return None, 'provider_payload_budget'
(directory / 'prompt.txt').write_text(prompt, encoding='utf-8')
command = ['codex', 'exec', '--sandbox', 'danger-full-access', '-c', 'approval_policy="never"',
'-c', f'tool_output_token_limit={self.state["modelPayload"]["limitBytes"] // 48}',
'--ephemeral', '--cd', str(ROOT), '--json', '--color', 'never',
'--output-schema', str(directory / 'result.schema.json'),
'--output-last-message', str(directory / 'result.json'), '-']
started = time.monotonic()
activity, commands, last_activity = 'provider_start', 0, started
active_commands = set()
artifact_commands = set()
tool_output_bytes, completed_items, output_budget_deadline = 0, set(), None
source_idle_seconds = 0.0
progress_tick = started
last_source_change = None
first_action_seconds = None
source_read_commands = 0
implementation_progress_seconds = min(
self.args.implementation_progress_timeout,
self.args.source_progress_timeout,
)
error_kind = None
turn_completed = False
provider = self.state.setdefault('provider', {})
provider['reconnecting'] = False
provider['turnReconnectEvents'] = 0
provider_reconnect_started = None
provider_reconnect_seconds = 0.0
provider_reconnect_timeout = 60
samples = provider.get('startupSeconds', [])[-8:]
startup_timeout = min(self.args.worker_timeout, max(self.args.startup_timeout,
min(180, int(max(samples, default=0) * 2))))
log_path = directory / 'worker.jsonl'
self.log_offset = 0
pending_line = b''
self.observed_files = set()
self.last_source_reads = []
# Shell edits need not emit file_change; reuse the bounded route baseline.
observed_stamps = dict(source_baseline or {})
with log_path.open('wb') as log:
self.child = subprocess.Popen(command, cwd=ROOT, stdin=subprocess.PIPE, stdout=log,
stderr=subprocess.STDOUT, start_new_session=True)
try:
self.child.stdin.write(prompt.encode('utf-8'))
except BrokenPipeError:
pass
finally:
self.child.stdin.close()
self.checkpoint('running', childPid=self.child.pid, childIdentity=identity(self.child.pid),
cycleDirectory=str(directory.relative_to(ROOT)), activity=activity, commands=0,
workState='waiting_executor', startupDeadlineSeconds=startup_timeout,
firstActionSeconds=None, providerErrorKind=None)
stop_reason = None
while True:
tick = time.monotonic()
elapsed = tick - started
# Artifact generation has its own overall deadline; it does not
# consume the source-reading budget needed after compilation.
if (first_action_seconds is not None and not artifact_commands and
not provider.get('reconnecting')):
source_idle_seconds += tick - progress_tick
if provider.get('reconnecting'):
provider_reconnect_seconds += tick - progress_tick
progress_tick = tick
with log_path.open('rb') as stream:
stream.seek(self.log_offset)
content = stream.read(1024 * 1024)
self.log_offset = stream.tell()
if content:
parts = (pending_line + content).split(b'\n')
pending_line = parts.pop()
for line in parts:
try:
event = json.loads(line)
except (ValueError, UnicodeDecodeError):
continue
if event.get('type') in {'turn.completed', 'turn.failed'}:
turn_completed = event['type'] == 'turn.completed'
if event.get('type') in {'error', 'turn.failed'}:
provider['turnReconnectEvents'] += 1
error_kind = provider_error_kind(event)
if not provider.get('reconnecting'):
provider_reconnect_started = time.monotonic()
provider.update(reconnecting=True, lastTransientErrorKind=error_kind,
lastTransientErrorAt=now(),
reconnectEvents=provider.get('reconnectEvents', 0) + 1)
if event['type'] == 'turn.failed':
stop_reason = 'provider_turn_failed'
item = event.get('item', {})
if event.get('type') == 'item.completed' and item.get('type') in {'command_execution', 'mcp_tool_call', 'file_change'}:
item_key = (item.get('type'), item.get('id'))
if item.get('id') is None or item_key not in completed_items:
# JSONL retains raw output beyond the CLI's model-facing truncation budget.
tool_output_bytes += min(len(json.dumps(item, ensure_ascii=False).encode('utf-8')), self.state['modelPayload']['toolOutputTokenLimit'] * 6)
completed_items.add(item_key)
if item.get('type') == 'command_execution':
if event.get('type') == 'item.started':
active_commands.add(item.get('id'))
command_text = item.get('command', '')
if (source_read_commands >= self.args.source_read_command_budget and
is_source_read_command(command_text)):
output_budget_deadline = output_budget_deadline or time.monotonic()
if (self.state.get('executionMode') == 'INTEGRATE_PENDING' and
source_read_commands >= 1 and is_source_read_command(command_text)):
output_budget_deadline = output_budget_deadline or time.monotonic() + 30
if any(marker in command_text for marker in (
'git status', 'git diff', '--full-inventory',
'find ', 'find .', 'find ..', 'rg ')):
output_budget_deadline = output_budget_deadline or time.monotonic()
self.state['executionDeviation'] = {
'cycle': self.state.get('cycle'),
'commandClass': 'broad_or_non_product_inspection',
'instruction': 'Do not repeat; use current handoff exact paths and implement a concrete route.'
}
if any(marker in command_text for marker in (
'cmake --build ', 'ninja ', 'emcc ', 'em++ ',
'build-main-desktop-candidate.py', 'vite/bin/vite.js build')) and not re.search(
r'(?<!\S)-(?:t|n)(?:\s|$)|--dry-run', command_text):
artifact_commands.add(item.get('id'))
elif event.get('type') == 'item.completed':
active_commands.discard(item.get('id'))
finished_artifact = item.get('id') in artifact_commands
artifact_commands.discard(item.get('id'))
if finished_artifact and item.get('exit_code') == 0:
# Allow the completed artifact's explicit final handoff.
source_idle_seconds = 0.0
# Retain only bounded production path references, never command
# bodies or output. Reading is not source progress.
command_text = item.get('command', '')
if item.get('exit_code') == 0 and is_source_read_command(command_text):
source_read_commands += 1
for value in re.findall(
r'(?:blender-5\.2\.0/(?:source|intern|scripts)/|web/(?:app/src|protocol)/)'
r'[A-Za-z0-9_./-]+', command_text):
path = production_path(value)
if path and path.is_file():
if value in self.last_source_reads:
self.last_source_reads.remove(value)
self.last_source_reads.append(value)
self.last_source_reads = self.last_source_reads[-8:]
if event.get('type') == 'item.completed' and item.get('type') == 'file_change':
for change in item.get('changes', []):
value = change.get('path', '')
path = Path(value)
if path.is_absolute():
try:
value = str(path.relative_to(ROOT))
except ValueError:
continue
if production_path(value):
self.observed_files.add(value)
last_source_change = time.monotonic()
source_idle_seconds = 0.0
if event.get('type') in {'item.started', 'item.completed'} and item.get('type') in {'command_execution', 'file_change', 'mcp_tool_call'}:
if provider.get('reconnecting'):
provider_reconnect_started = None
provider_reconnect_seconds = 0.0
provider.update(reconnecting=False, resumedAt=now())
if first_action_seconds is None:
first_action_seconds = round(time.monotonic() - started, 1)
provider['startupSeconds'] = (samples + [first_action_seconds])[-8:]
provider['lastActionAt'] = now()
activity = item['type']
last_activity = time.monotonic()
if event['type'] == 'item.started':
commands += 1
for value, previous_stamp in list(observed_stamps.items()):
path = production_path(value)
if path is None:
continue
try:
stat = path.stat()
metadata = (stat.st_size, stat.st_mtime_ns)
except FileNotFoundError:
metadata = None
except OSError:
continue
if metadata == (previous_stamp[:2] if previous_stamp else None):
continue
try:
current_stamp = stamp(path)
except OSError:
continue
observed_stamps[value] = current_stamp
if ((current_stamp[2] if current_stamp else None) !=
(previous_stamp[2] if previous_stamp else None)):
self.observed_files.add(value)
last_source_change = time.monotonic()
source_idle_seconds = 0.0
if tool_output_bytes >= self.state['modelPayload']['toolOutputSoftBytes'] and output_budget_deadline is None:
output_budget_deadline = tick + 20
if self.child.poll() is not None:
# Drain events already written after the last heartbeat.
if self.log_offset < log_path.stat().st_size:
continue
break
if self.stop_requested or (DIRECTORY / 'stop.request').exists():
stop_reason = 'stop_requested'
elif output_budget_deadline is not None and tick >= output_budget_deadline and not active_commands:
stop_reason = 'worker_context_budget'
elif (provider.get('reconnecting') and
(provider_reconnect_seconds >= provider_reconnect_timeout or
(provider_reconnect_started is not None and
time.monotonic() - provider_reconnect_started >= provider_reconnect_timeout))):
stop_reason = 'provider_backoff_deadline'
elif elapsed >= self.args.worker_timeout:
stop_reason = ('provider_turn_failed' if provider.get('reconnecting')
and not active_commands else 'worker_deadline')
elif first_action_seconds is None and elapsed >= startup_timeout:
stop_reason = 'provider_no_actions'
elif (first_action_seconds is not None and not active_commands and
not provider.get('reconnecting') and
source_idle_seconds >= self.args.source_progress_timeout):
stop_reason = 'worker_source_progress_deadline'
elif (first_action_seconds is not None and not active_commands and
not provider.get('reconnecting') and
time.monotonic() - last_activity >= self.args.idle_timeout):
stop_reason = 'worker_no_source_activity'
elif (first_action_seconds is not None and not self.observed_files and
not active_commands and not artifact_commands and not provider.get('reconnecting') and
source_idle_seconds >= implementation_progress_seconds and
(source_read_commands >= self.args.source_read_command_budget or commands >= 8)):
# Repeated source reads are investigation, not progress.
# Require a production edit or artifact command after a
# bounded budget so a focus cannot consume a full turn
# without changing the native route.
stop_reason = 'worker_implementation_progress_deadline'
elif log_path.stat().st_size >= 64 * 1024 * 1024:
stop_reason = 'worker_log_limit'
self.checkpoint('running', elapsedSeconds=round(elapsed), activity=activity,
workState='executing_command' if active_commands else
'provider_reconnecting' if provider.get('reconnecting') else
'waiting_executor' if first_action_seconds is None else
'source_changes_observed' if self.observed_files else 'reading_sources',
firstActionSeconds=first_action_seconds, providerErrorKind=error_kind,
commands=commands, lastActionAgeSeconds=round(time.monotonic() - last_activity),
activeCommandCount=len(active_commands),
activeArtifactCommandCount=len(artifact_commands),
sourceProgressDeadlineSeconds=self.args.source_progress_timeout,
sourceIdleSeconds=round(source_idle_seconds),
providerReconnectSeconds=round(provider_reconnect_seconds),
toolOutputObservedBytes=tool_output_bytes,
sourceReadCommands=source_read_commands,
sourceReadCommandBudget=self.args.source_read_command_budget,
implementationProgressDeadlineSeconds=implementation_progress_seconds,
lastSourceChangeAgeSeconds=None if last_source_change is None else round(time.monotonic() - last_source_change),
observedProductionFiles=sorted(self.observed_files),
recentSourceReads=self.last_source_reads)
if stop_reason:
self.signal_child(signal.SIGTERM)
try:
self.child.wait(timeout=10)
except subprocess.TimeoutExpired:
self.signal_child(signal.SIGKILL)
self.child.wait()
break
time.sleep(5)
code = self.child.wait()
self.child = None
if not turn_completed and stop_reason is None:
stop_reason = 'provider_turn_failed'
error_kind = error_kind or 'transport'
write_json(directory / 'process.json', dict(exitCode=code, reason=stop_reason,
turnCompleted=turn_completed,
toolOutputObservedBytes=tool_output_bytes, submittedBytes=self.state['modelPayload']['submittedBytes'],
elapsedSeconds=round(time.monotonic() - started), firstActionSeconds=first_action_seconds,
startupDeadlineSeconds=startup_timeout, commands=commands,
sourceReadCommands=source_read_commands,
sourceReadCommandBudget=self.args.source_read_command_budget,
implementationProgressDeadlineSeconds=implementation_progress_seconds,
providerErrorKind=error_kind, changedProductionFiles=sorted(self.observed_files),
recentSourceReads=self.last_source_reads, validation='NOT_RUN'))
self.state.update(firstActionSeconds=first_action_seconds, providerErrorKind=error_kind,
sourceReadCommands=source_read_commands,
sourceReadCommandBudget=self.args.source_read_command_budget,
implementationProgressDeadlineSeconds=implementation_progress_seconds,
observedProductionFiles=sorted(self.observed_files))
provider['reconnecting'] = False
result = read_handoff(directory)
if result is not None and code == 0 and turn_completed and not self.stop_requested and stop_reason not in {'stop_requested', 'provider_turn_failed'}:
# A bounded supervisor stop can arrive after the worker has already
# written its structured handoff; consume that durable result first.
stop_reason = None
if stop_reason or code != 0:
return None, stop_reason or ('provider_exit_no_actions' if first_action_seconds is None else 'worker_exit_' + str(code))
if result is None:
return None, 'provider_exit_no_actions' if first_action_seconds is None else 'worker_handoff_missing'
detail = ' '.join(str(result.get(key, '')) for key in ('summary', 'nextFocus'))
detail += ' ' + str(result.get('delivery', {}).get('blockingRoute', ''))
if (result.get('status') == 'blocked' and not result.get('changedFiles') and
any(marker in detail.lower() for marker in (
'工具不可调用', '未提供可调用', '未提供文件读取', '未提供文件读写',
'未提供任何工具', 'no callable tools', 'tools are unavailable',
'no tools available', 'tools unavailable'))):
# A provider-reported tool failure must not replace the real native
# business handoff with an environment complaint and stale nextAction.
self.state['providerErrorKind'] = 'reported_tools_unavailable'
return None, 'provider_tools_unavailable'
update_budget(self.state, 'pressure' if provider.get('turnReconnectEvents') else 'stable')
provider.update(consecutiveFailures=0, retryAt=0, lastFailure=None, errorKind=None,
sameKindFailures=0, lastFailureKind=None, retryDelaySeconds=0)
return result, None
def restore(self, previous_state):
self.focuses = previous_state.get('focusHandoffs', {})
# Bootstrap old checkpoints from only the recent bounded handoffs.
cycle = int(previous_state.get('cycle', 0))
latest_complete = False
for number in range(max(1, cycle - 11), cycle + 1):
directory = DIRECTORY / f'cycle-{number:06d}'
result = read_handoff(directory)
process = read_json(directory / 'process.json', {})
if result and process.get('exitCode') == 0 and not process.get('reason'):
latest_complete = latest_complete or number == cycle
attempt = read_json(directory / 'attempt.json', {})
name = attempt.get('focus', FOCUSES[(number - 1) % len(FOCUSES)][0])
branch = self.focuses.setdefault(name, {})
if number > branch.get('lastCycle', 0):
branch.update(handoff=result, lastCycle=number, failures=0, retryAt=0)
if not latest_complete and previous_state.get('state') in {'starting', 'running', 'needs_reconcile', 'recovering'}:
name = previous_state.get('focus')
if name in {focus[0] for focus in FOCUSES}:
self.focuses.setdefault(name, {}).update(interrupted={
'cycle': cycle, 'directory': previous_state.get('cycleDirectory'),
'stageId': read_json(DIRECTORY / f'cycle-{cycle:06d}' / 'attempt.json', {}).get('deliveryStage'),
'reason': previous_state.get('reason') or 'supervisor_interrupted',
'changedFiles': previous_state.get('observedProductionFiles', []),
'recentSourceReads': previous_state.get('recentSourceReads', [])[-8:],
'instruction': 'Read and finish partial source edits; no rollback or blind command replay.',
}, retryAt=0)
self.focuses[name]['resumeFirst'] = True
pending = self.state.setdefault('pendingSourceCycles', [])
if previous_state.get('observedProductionFiles') and cycle not in pending:
pending.append(cycle)
def pause(self, seconds, work_state='waiting_business_retry'):
deadline = time.monotonic() + seconds
while time.monotonic() < deadline and not self.stop_requested:
request_path = DIRECTORY / 'foreground-candidate.json'
if (request_path.stat().st_mtime_ns if request_path.exists() else 0) != getattr(self, 'foreground_request_stamp', 0):
return
if Path(__file__).stat().st_mtime_ns != self.program_stamp or Path(__file__).with_name('wbv2_payload_budget.py').stat().st_mtime_ns != self.payload_program_stamp:
return
if (DIRECTORY / 'stop.request').exists():
self.stop_requested = True
break
self.checkpoint('waiting_retry', workState=work_state,
retryInSeconds=max(0, round(deadline - time.monotonic())), childPid=None)
time.sleep(min(5, max(0, deadline - time.monotonic())))
def run(self):
DIRECTORY.mkdir(parents=True, exist_ok=True)
if (DIRECTORY / 'stop.request').exists():
return 0
with Lease(ROOT / 'tests/golden/WBV2/autopilot/owner.lock'):
previous_state = read_json(DIRECTORY / 'state.json', {})
child_pid = previous_state.get('childPid')
if alive(child_pid) and identity(child_pid) == previous_state.get('childIdentity'):
raise RuntimeError('Previous implementation worker is still active; refusing a second writer')
fence = ROOT / 'tests/golden/WBV2/continuous-scheduler.lock'
owner = read_json(fence, {})
if (owner.get('runtime') == 'python-autopilot' and owner.get('identity') and
(not alive(owner.get('pid')) or identity(owner['pid']) != owner['identity'])):
fence.unlink()
with ProjectFence(ROOT):
self.owns_project = True
try:
return self.run_owned(previous_state)
finally:
self.owns_project = False
def run_owned(self, previous_state):
try:
self.state = dict(previous_state)
self.restore(previous_state)
context = queue_context(ROOT)
if context['task'] != TASK:
raise RuntimeError('Implementation focus is bound to ' + TASK)
canonical = canonical_continuation()
if canonical:
self.state['canonicalContinuation'] = canonical
else:
self.state.pop('canonicalContinuation', None)
baseline_parent = context['parentSha256']
cycle = int(previous_state.get('cycle', 0))
previous = previous_state.get('handoff', {})
if 'lastCombinedNativeBuild' not in self.state:
# Reuse small records already restored in memory; no history/source scan.
links = []
shared = self.state.get('sharedBuild', {})
if combined_native_build(shared):
links.append(dict(shared))
for name, branch in self.focuses.items():
build = branch.get('handoff', {}).get('build', {})
# lastCycle may refer to an interrupted attempt, not this handoff.
record = self.state.get('deliveryStages', {}).get(
branch.get('handoff', {}).get('delivery', {}).get('stageId'), {}).get(
'focuses', {}).get(name, {})
report_cycle = record.get('cycle')
if combined_native_build(build) and isinstance(report_cycle, int):
links.append(dict(build, cycle=report_cycle, focus=name,
reportPath=record.get('reportPath')))
if links:
self.state['lastCombinedNativeBuild'] = max(links, key=lambda value: value.get('cycle', 0))
# Bootstrap only bounded implementation handoffs, never a full source scan.
# Thereafter record actual observed source rounds, not skipped/failed IDs.
# Recover formerly ignored directory-shaped packages at a safe
# startup boundary. Newer source rounds remain pending below.
for stage_id, stage_progress in self.state.get('deliveryStages', {}).items():
for name, record in stage_progress.get('focuses', {}).items():
handoff = self.focuses.get(name, {}).get('handoff', {})
if handoff.get('delivery', {}).get('stageId') != stage_id:
continue
report_cycle = record.get('cycle')
if (not isinstance(report_cycle, int) or
report_cycle <= self.state.get('applicationCandidate', {}).get('cycle', 0)):
continue
paths = candidate_handoff_paths(handoff.get('build', {}), stage_id,
handoff.get('changedFiles', []))
if paths:
self.state['applicationCandidate'] = dict(
cycle=report_cycle, path=paths[-1],
basis='restored explicit package handoff; unverified')
candidate_cycle = self.state.get('applicationCandidate', {}).get('cycle', 0)
if 'pendingSourceCycles' not in self.state:
first_cycle = max(candidate_cycle + 1, cycle - 23)
self.state['pendingSourceCycles'] = []
for number in range(first_cycle, cycle + 1):
result = read_handoff(DIRECTORY / f'cycle-{number:06d}')
if result and any(production_path(value) for value in result.get('changedFiles', [])):
self.state['pendingSourceCycles'].append(number)
self.state['pendingSourceHistoryIncomplete'] = first_cycle > candidate_cycle + 1
else:
self.state['pendingSourceCycles'] = [number for number in self.state['pendingSourceCycles']
if number > candidate_cycle]
# Recover explicit source edits that a worker declared when
# filesystem stamps were unavailable in the same turn.
for number in range(max(candidate_cycle + 1, cycle - 23), cycle + 1):
if number in self.state['pendingSourceCycles']:
continue
directory = DIRECTORY / f'cycle-{number:06d}'
process = read_json(directory / 'process.json', {})
result = read_handoff(directory)
if (process.get('exitCode') == 0 and not process.get('reason') and result and
any(production_path(value) for value in result.get('changedFiles', []))):
self.state['pendingSourceCycles'].append(number)
request = self.state.get('batchIntegrationRequested', {})
if request and request.get('cycle', 0) <= candidate_cycle:
self.state.pop('batchIntegrationRequested', None)
watched = set(ANCHORS) | set(previous.get('changedFiles', []))
for branch in self.focuses.values():
watched.update(branch.get('handoff', {}).get('changedFiles', []))
watched = {value for original in watched if (value := workspace_relative(original))}
no_progress = int(previous_state.get('noProgressCycles', 0))
completed = 0
while not self.stop_requested and (not self.args.max_iterations or completed < self.args.max_iterations):
if (DIRECTORY / 'hold-after-handoff.request').exists():
self.checkpoint('stopped', reason='root_source_repair_after_handoff', childPid=None)
return 0
if Path(__file__).stat().st_mtime_ns != self.program_stamp or Path(__file__).with_name('wbv2_payload_budget.py').stat().st_mtime_ns != self.payload_program_stamp:
self.checkpoint('reloading', reason='implementation_loop_updated', childPid=None)
return 75
if (DIRECTORY / 'stop.request').exists():
self.stop_requested = True
break
current = queue_context(ROOT)
if current['task'] != TASK or current['parentSha256'] != baseline_parent:
self.checkpoint('stopped', reason='queue_or_parent_changed'); return 2
self.consume_foreground_candidate()
no_progress = int(self.state.get('noProgressCycles', no_progress))
if no_progress < 128:
self.state.pop('globalNoProgressCooldownAt', None)
if (no_progress >= 128 and not self.state.get('pendingSourceCycles') and
not self.integration_reason() and
not self.state.get('globalNoProgressCooldownAt')):
self.state['globalNoProgressCooldownAt'] = time.time()
self.checkpoint('waiting_retry', workState='global_no_progress_cooldown',
noProgressCycles=no_progress)
self.pause(120, 'global_no_progress_cooldown')
continue
self.delivery_stage = self.next_delivery_stage()
if self.delivery_stage is None:
self.checkpoint('awaiting_user_verification', reason='all_delivery_assignments_reported_implemented_unverified', childPid=None)
return 0
provider = self.state.setdefault('provider', {})
if provider.get('retryAt', 0) > time.time():
self.pause(provider['retryAt'] - time.time(), 'waiting_provider_retry')
continue
stage = self.delivery_stage
candidate_due_now = bool(self.integration_reason()) and not self.state.get('canonicalContinuation')
if candidate_due_now:
self.state['integrationUrgent'] = {
'reason': self.integration_reason(),
'pendingSourceCycles': list(self.state.get('pendingSourceCycles', [])),
'action': 'integrate existing native pair and package one matching candidate before another unrelated source batch',
}
else:
self.state.pop('integrationUrgent', None)
stage_progress = self.state.get('deliveryStages', {}).get(stage['id'], {}).get('focuses', {})
stage_attempts = self.state.get('deliveryStages', {}).get(stage['id'], {}).get('attempts', {})
candidates = [next(focus for focus in FOCUSES if focus[0] == name)
for name in stage['focusOrder']
if not stage_progress.get(name, {}).get('reportedImplemented')]
now_seconds = time.time()
if not candidates:
self.checkpoint('waiting_retry', workState='waiting_strategy_rotation', noProgressCycles=no_progress)
self.pause(30, 'waiting_strategy_rotation')
continue
available = [item for item in candidates
if self.focuses.get(item[0], {}).get('retryAt', 0) <= now_seconds]
ready = [item for item in available if
self.focuses.get(item[0], {}).get('resumeFirst') or
self.focuses.get(item[0], {}).get('circuitStageId') != stage['id'] or
self.focuses.get(item[0], {}).get('circuitOpenUntil', 0) <= now_seconds]
if no_progress >= 8 and len(candidates) > 1:
eligible = [item for item in candidates if
self.focuses.get(item[0], {}).get('retryAt', 0) <= now_seconds and
self.focuses.get(item[0], {}).get('circuitOpenUntil', 0) <= now_seconds]
if not eligible:
self.checkpoint('waiting_retry', workState='waiting_strategy_rotation',
noProgressCycles=no_progress, strategyWaits=self.state.get('strategyWaits', 0))
self.pause(30, 'waiting_strategy_rotation')
ready = []
else:
forced = min(eligible, key=lambda item: self.focuses.get(item[0], {}).get('noProgressRounds', 0))
forced_branch = self.focuses[forced[0]]
forced_branch['recoveryDirection'] = 'Global no-progress rotation: use the saved exact source paths and a new bounded operation; do not repeat scanning.'
ready = [forced]
if not ready:
budget_interrupted = any(
(self.focuses.get(item[0], {}).get('interrupted') or {}).get('reason') == 'worker_context_budget'
for item in candidates)
if budget_interrupted:
fallback = min(candidates, key=lambda item: (
self.focuses.get(item[0], {}).get('noProgressRounds', 0),
stage_attempts.get(item[0], 0)))
fallback_branch = self.focuses[fallback[0]]
fallback_branch['retryAt'] = 0
fallback_branch['circuitOpenUntil'] = 0
fallback_branch['recoveryDirection'] = 'Context budget interruption: rotate to a bounded source-backed operation immediately; preserve pending and old candidate.'
self.state['strategyWaits'] = 0
ready = [fallback]
elif available and all(self.focuses.get(item[0], {}).get('noProgressRounds', 0) < 6 for item in available):
# A business circuit must not idle the entire project when all
# routes are merely waiting on their normal retry windows.
fallback = min(available, key=lambda item: stage_attempts.get(item[0], 0))
fallback_branch = self.focuses[fallback[0]]
fallback_branch['recoveryDirection'] = (
'All ready business families exhausted their repeated-reading budget. '
'Use the saved exact source breadcrumbs and identify a different concrete '
'operation/dependency in this family; do not replay the timed-out search. '
'Preserve all unresolved gaps and any partial edits.')
ready = [fallback]
else:
strategy_waits = self.state.get('strategyWaits', 0) + 1
self.state['strategyWaits'] = strategy_waits
if strategy_waits >= 3:
fallback = min(candidates, key=lambda item: (
self.focuses.get(item[0], {}).get('noProgressRounds', 0),
stage_attempts.get(item[0], 0)))
fallback_branch = self.focuses[fallback[0]]
fallback_branch['retryAt'] = 0
fallback_branch['circuitOpenUntil'] = 0
fallback_branch['circuitStageId'] = stage['id']
fallback_branch['recoveryDirection'] = ('Strategy wait cap reached. Choose a different concrete unfinished '
'operation/dependency from exact source paths; do not replay the previous investigation or package unchanged inputs.')
self.state['strategyWaits'] = 0
ready = [fallback]
else:
retry_points = [max(self.focuses.get(item[0], {}).get('retryAt', 0),
self.focuses.get(item[0], {}).get('circuitOpenUntil', 0))
for item in candidates]
self.checkpoint('waiting_retry', workState='waiting_strategy_rotation',
noProgressCycles=no_progress, strategyWaits=strategy_waits)
self.pause(max(0, min(30, min(retry_points) - time.time())), 'waiting_strategy_rotation')
continue
if not ready:
self.checkpoint('waiting_retry', workState='waiting_strategy_rotation', noProgressCycles=no_progress)
self.pause(30, 'waiting_strategy_rotation')
continue
focus = min(ready, key=lambda item: (
not self.focuses.get(item[0], {}).get('resumeFirst', False),
not (bool(self.focuses.get(item[0], {}).get('efficiencyUrgent'))
and stage_attempts.get(item[0], 0) < max(stage_attempts.values(), default=1)),
self.state.get('efficiency', {}).get('focusReviewedAt', {}).get(item[0], 0) if self.efficiency_due() else 0,
stage_attempts.get(item[0], stage_progress.get(item[0], {}).get('cycle', 0))))
continuation = self.state.get('workflowContinuation', {})
# Keep a productive native workflow warm for a bounded group;
# then rotate normally so the other entrypoints cannot starve.
follow = next((item for item in ready if item[0] == continuation.get('focus')), None)
follow_branch = self.focuses.get(follow[0], {}) if follow else {}
if (follow and continuation.get('stageId') == stage['id'] and
continuation.get('turns', 0) < COHERENT_FOCUS_TURNS and
not candidate_due_now and
not follow_branch.get('noProgressRounds', 0) and
not follow_branch.get('unchangedBlockerRounds', 0) and
not any(self.focuses.get(item[0], {}).get('resumeFirst') for item in ready)):
# A different focus's stale/repeated failure flag must not
# fragment this productive family. Recovery still gets its
# normal priority when this bounded continuation ends.
focus = follow
branch = self.focuses.setdefault(focus[0], {})
branch.pop('resumeFirst', None)
cycle += 1
while (DIRECTORY / f'cycle-{cycle:06d}').exists():
cycle += 1
directory = DIRECTORY / f'cycle-{cycle:06d}'
directory.mkdir(exist_ok=False)
self.state.setdefault('deliveryStages', {}).setdefault(stage['id'], {'focuses': {}}).setdefault('attempts', {})[focus[0]] = cycle
rotation = self.state.setdefault('gapStageRotation', {})
visit_turns = rotation.get('turns', 0) + 1 if rotation.get('stageId') == stage['id'] else 1
rotation.update(stageId=stage['id'], turns=visit_turns)
rotation.setdefault('lastVisited', {})[stage['id']] = cycle
write_json(directory / 'attempt.json', dict(cycle=cycle, focus=focus[0], deliveryStage=stage['id'], startedAt=now(), validation='NOT_RUN'))
watched.update(stage.get('sourcePaths', {}).get(focus[0], []))
handoff = branch.get('handoff', {})
same_stage = handoff.get('delivery', {}).get('stageId') == stage['id']
selected_gap = next((item for item in self.concrete_gap_items(focus[0])
if item.get('status') != 'implemented_unverified'), {})
selected_gap_phase = 'implement' if candidate_due_now else selected_gap.get('phase', 'implement')
interrupted = branch.get('interrupted') or {}
monitor_paths = list(dict.fromkeys(
value for original in (
((interrupted.get('changedFiles', []) + interrupted.get('recentSourceReads', [])) if interrupted.get('stageId') == stage['id'] else [])
+ selected_gap.get('sourcePaths', [])
+ self.state.get('canonicalContinuation', {}).get('nextSourcePaths', [])
+ (handoff.get('nextSourcePaths', []) if same_stage else [])
+ stage.get('sourcePaths', {}).get(focus[0], []))
if (value := workspace_relative(original)) and production_path(value)))[:8]
watched.update(monitor_paths)
before = {value: stamp(path) for value in watched if (path := production_path(value))}
started_ns = time.time_ns()
self.checkpoint('starting', cycle=cycle, focus=focus[0], phase=selected_gap_phase,
phaseLabel=('定位源码' if selected_gap_phase == 'locate' else '实现接线'),
handoff=previous,
cycleDirectory=str(directory.relative_to(ROOT)), observedProductionFiles=[],
activeCommandCount=0, lastSourceChangeAgeSeconds=None,
activeArtifactCommandCount=0, sourceIdleSeconds=0,
childPid=None, reason=None, noProgressCycles=no_progress)
self.state['deliveryStage'] = dict(id=stage['id'], title=stage['title'],
assignment=stage['focuses'][focus[0]])
self.prepare_efficiency_review(focus[0])
try:
result, reason = self.run_worker(
directory, self.prompt(focus, previous, cycle),
before)
except OSError as error:
if self.child is not None:
raise
result, reason = None, f'provider_launch_failed: {error}'
branch['lastCycle'] = cycle
if selected_gap_phase == 'locate' and result is not None:
locate_changed = any(production_path(value) for value in result.get('changedFiles', [])) or bool(self.observed_files)
locate_build = result.get('build', {}).get('status') not in (None, 'not_run')
if locate_changed or locate_build:
reason = 'locate_boundary_violation'
result = dict(result)
result['status'] = 'blocked'
result['summary'] = ('Locate phase attempted production/build work; held blocked and retained the route. '
'No locate turn may publish a source fix or candidate.')
result['delivery'] = dict(result.get('delivery') or {},
status='blocked',
blockingRoute='LOCATE_BOUNDARY_VIOLATION: production edit/build attempted during locate phase',
nextAction='Repeat only bounded source tracing; promote to implement phase only after an exact missing edge is recorded.')
self.state['locateBoundaryViolation'] = dict(cycle=cycle, focus=focus[0],
changedFiles=sorted(self.observed_files),
build=result.get('build', {}))
if reason:
if reason in {'worker_source_progress_deadline', 'worker_implementation_progress_deadline',
'worker_no_source_activity', 'worker_deadline', 'worker_handoff_missing', 'worker_context_budget'}:
branch['efficiencyUrgent'] = reason
no_progress = 0 if self.observed_files else no_progress + 1
branch['noProgressRounds'] = 0 if self.observed_files else branch.get('noProgressRounds', 0) + 1
if reason in {'worker_source_progress_deadline', 'worker_implementation_progress_deadline', 'worker_context_budget'}:
update_budget(self.state, 'pressure')
stalled_scope = selected_gap.get('id') or stage.get('closureContracts', {}).get(
focus[0], {}).get('id') or stage['id'] + ':' + focus[0]
repeated_scope = branch.get('stalledScope') == stalled_scope
branch['stalledScopeAttempts'] = (0 if self.observed_files else
branch.get('stalledScopeAttempts', 0) + 1 if repeated_scope else 1)
branch['stalledScope'] = stalled_scope
branch['recoveryDirection'] = ('Source-reading deadline reached. Read the interrupted edits and bounded log tail, '
'then implement a different immediate native dependency or independent unfinished business operation. '
'Do not repeat the previous exploration sequence.')
if branch['stalledScopeAttempts'] >= 2 and not self.observed_files:
# Circuit-break a repeated source-reading loop. Keep
# the gap and exact interrupted breadcrumbs, but let
# another concrete batch make progress for one hour.
branch['circuitOpenUntil'] = time.time() + 3600
branch['circuitStageId'] = stage['id']
branch['recoveryDirection'] += (
' Defer this repeated path for at most 1h while other work is ready. '
'New shared-source edits or partial-edit recovery may resume it earlier.')
provider_failure = reason in {'provider_no_actions', 'provider_turn_failed', 'provider_exit_no_actions', 'provider_tools_unavailable', 'provider_backoff_deadline', 'provider_payload_budget'} or reason.startswith('provider_launch_failed:')
provider_delay = None
if provider_failure:
failures = provider.get('consecutiveFailures', 0) + 1
error_kind = self.state.get('providerErrorKind')
update_budget(self.state, 'pressure' if error_kind in {'transport', 'payload_limit'} else 'unstable')
same_kind = provider.get('lastFailureKind') == error_kind
kind_failures = provider.get('sameKindFailures', 0) + 1 if same_kind else 1
if error_kind in {'transport', 'reported_tools_unavailable'}:
provider_delay = min(1800, 30 * 2 ** min(kind_failures - 1, 6))
else:
initial_delay = 180 if error_kind in {'overloaded', 'rate_limit'} else 60
provider_delay = min(1800, initial_delay * 2 ** min(kind_failures - 1, 5))
provider.update(consecutiveFailures=failures, lastFailure=reason,
errorKind=error_kind, lastFailureKind=error_kind,
sameKindFailures=kind_failures, retryDelaySeconds=provider_delay,
retryAt=time.time() + provider_delay)
failures = branch.get('failures', 0) + 1
branch_delay = provider_delay if provider_failure else min(900, 30 * 2 ** min(failures - 1, 5))
retained_interruption = branch.get('interrupted') if provider_failure and not self.observed_files and not getattr(self, 'last_source_reads', []) else None
branch.update(failures=failures, retryAt=time.time() + branch_delay,
interrupted=dict(cycle=cycle, directory=str(directory.relative_to(ROOT)),
stageId=stage['id'],
reason=reason, changedFiles=sorted(self.observed_files),
recentSourceReads=getattr(self, 'last_source_reads', []),
instruction='Finish partial edits from this directory; do not replay commands blindly.'))
if retained_interruption:
branch['interrupted'] = retained_interruption
if self.observed_files and cycle not in self.state['pendingSourceCycles']:
self.state['pendingSourceCycles'].append(cycle)
watched.update(self.observed_files)
self.checkpoint('recovering', reason=reason, childPid=None, noProgressCycles=no_progress)
completed += 1
if self.stop_requested or reason == 'stop_requested':
return 0
continue
declared = list(dict.fromkeys(value for original in result.get('changedFiles', []) + sorted(self.observed_files)
if (value := workspace_relative(original))))
declared_production = [value for value in declared if production_path(value)]
if declared_production and cycle not in self.state['pendingSourceCycles']:
self.state['pendingSourceCycles'].append(cycle)
changed = []
for value in declared:
path = production_path(value)
if not path:
continue
after = stamp(path)
if ((value in before and after is not None and
(before[value] is None or after[2] != before[value][2])) or
(value not in before and after and after[1] >= started_ns)):
changed.append(value)
build = result.get('build', {})
# Packaging followed by a change to any known pending source
# must not clear that source merely because this handoff omitted it.
candidate_paths = candidate_handoff_paths(build, stage['id'], set(before) | set(declared))
integration_progress = (
bool(self.state.get('pendingSourceCycles')) and
build.get('status') == 'artifact_generated' and
not build.get('blockingSymbols') and bool(candidate_paths) and
candidate_paths[-1] != self.state.get('applicationCandidate', {}).get('path'))
productive = bool(changed) or integration_progress
if self.state.get('executionMode') == 'INTEGRATE_PENDING' and not productive:
self.state['integrationRetry'] = dict(retryAt=time.time() + 900, cycle=cycle, stageId=stage['id'], reason=str(result.get('delivery', {}).get('blockingRoute') or result.get('summary', 'integration_without_progress'))[:400])
elif integration_progress:
self.state.pop('integrationRetry', None)
no_progress = 0 if productive else no_progress + 1
self.record_efficiency(focus[0], result, cycle, changed)
self.record_delivery(focus[0], result, cycle, changed)
if not productive:
branch['efficiencyUrgent'] = 'handoff_without_observed_source_change'
elif branch.get('efficiencyUrgent') == 'handoff_without_observed_source_change':
branch.pop('efficiencyUrgent', None)
branch['noProgressRounds'] = 0 if productive else branch.get('noProgressRounds', 0) + 1
if (not productive and branch['noProgressRounds'] >= 2 and
branch.get('circuitOpenUntil', 0) <= time.time()):
# Two no-op handoffs are a strategy loop. Let another
# ready focus run for a bounded window before retrying.
branch['circuitOpenUntil'] = time.time() + (1200 if branch['noProgressRounds'] >= 6 else 300)
branch['circuitStageId'] = stage['id']
branch['noProgressWarning'] = (
f"连续无源码进展 {branch['noProgressRounds']} 轮;已隔离并要求改变策略。")
branch['recoveryDirection'] = (
'Two consecutive handoffs made no production edit or artifact. '
'Defer this focus for 15m; resume with exact nextSourcePaths '
'and a different native operation or dependency strategy.')
if productive:
branch.pop('circuitOpenUntil', None)
branch.pop('circuitStageId', None)
branch.pop('stalledScope', None)
branch.pop('stalledScopeAttempts', None)
branch.pop('noProgressWarning', None)
if changed:
if cycle not in self.state['pendingSourceCycles']:
self.state['pendingSourceCycles'].append(cycle)
if result.get('batchReadyForIntegration') is True and self.state.get('pendingSourceCycles'):
self.state['batchIntegrationRequested'] = dict(
cycle=cycle, focus=focus[0], businessEntry=result.get('businessEntry'),
reportPath=str((directory / 'result.json').relative_to(ROOT)))
if changed:
# A real same-stage source handoff can invalidate a
# circuit-breaker's stale exploration premise. Reopen
# other focuses only when they share a concrete source
# route; otherwise keep their bounded pause.
for peer_name, peer_branch in self.focuses.items():
if peer_name == focus[0]:
continue
if peer_branch.get('circuitStageId') != stage['id']:
continue
peer_paths = {path for value in peer_branch.get('handoff', {}).get('nextSourcePaths', [])
if (path := workspace_relative(value))}
if peer_paths.intersection(changed):
peer_branch.pop('circuitOpenUntil', None)
peer_branch.pop('circuitStageId', None)
peer_branch['retryAt'] = 0
self.state['latestImplementation'] = dict(cycle=cycle, focus=focus[0], at=now(),
changedFiles=changed, workflow=result['workflow'],
deliveryStage=stage['id'], businessEntry=result.get('businessEntry'))
last = self.state.get('workflowContinuation', {})
turns = last.get('turns', 0) + 1 if last.get('focus') == focus[0] and last.get('stageId') == stage['id'] else 1
self.state['workflowContinuation'] = dict(stageId=stage['id'], focus=focus[0], turns=turns)
else:
self.state.pop('workflowContinuation', None)
watched.update(declared)
previous = result
build = result.get('build', {})
if isinstance(build, dict) and build.get('status') in {'compile_blocked', 'link_blocked', 'artifact_generated'}:
blockers = sorted(set(build.get('blockingSymbols', [])))[:24]
previous_build = branch.get('handoff', {}).get('build', {})
same_blockers = blockers and blockers == sorted(set(previous_build.get('blockingSymbols', [])))[:24]
branch['unchangedBlockerRounds'] = branch.get('unchangedBlockerRounds', 0) + 1 if same_blockers else 0
self.state['sharedBuild'] = dict(cycle=cycle, focus=focus[0], **build)
if combined_native_build(build):
self.state['lastCombinedNativeBuild'] = dict(
self.state['sharedBuild'], reportPath=str((directory / 'result.json').relative_to(ROOT)))
if build['status'] == 'artifact_generated' and not blockers and candidate_paths:
self.state['applicationCandidate'] = dict(cycle=cycle, path=candidate_paths[-1],
basis='worker artifact-generation handoff; unverified')
self.state['pendingSourceCycles'] = []
self.state['pendingSourceHistoryIncomplete'] = False
self.state.pop('batchIntegrationRequested', None)
self.state.pop('integrationUrgent', None)
if build['status'] == 'artifact_generated':
for other_name, other_branch in self.focuses.items():
if other_name != focus[0] and other_branch.get('handoff', {}).get('build', {}).get('status') in {'compile_blocked', 'link_blocked'}:
other_branch['retryAt'] = 0
if not productive or branch.get('unchangedBlockerRounds', 0):
branch['recoveryDirection'] = ('Previous round produced no observed source change or repeated native blockers. '
'Use the newest shared build and exact nextSourcePaths; change the dependency repair strategy '
'or implement an independent unfinished operation. Do not rebuild unchanged inputs.')
else:
branch.pop('recoveryDirection', None)
branch.update(handoff=result, interrupted=None,
failures=0 if productive else branch.get('failures', 0) + 1)
branch['retryAt'] = 0 if productive else time.time() + min(900, 30 * 2 ** min(branch['failures'] - 1, 5))
self.checkpoint('handed_off', childPid=None, handoff=result, changedProductionFiles=changed,
phase=selected_gap_phase,
phaseLabel=('定位源码' if selected_gap_phase == 'locate' else '实现接线'),
noProgressCycles=no_progress,
integrationProgress=integration_progress)
completed += 1
self.checkpoint('stopped', reason='stop_requested' if self.stop_requested else 'iteration_limit', childPid=None)
return 0
except BaseException as error:
self.signal_child(signal.SIGTERM)
if self.child:
try:
self.child.wait(timeout=10)
except subprocess.TimeoutExpired:
self.signal_child(signal.SIGKILL)
self.child.wait()
self.checkpoint('needs_reconcile', reason=str(error), childPid=None)
raise
def main():
parser = argparse.ArgumentParser(description=__doc__)
parser.add_argument('--max-iterations', type=int, default=0)
parser.add_argument('--worker-timeout', type=int, default=3600)
# A healthy native build is excluded from this timer; source-only turns
# must make a concrete edit or handoff within a bounded window.
parser.add_argument('--idle-timeout', type=int, default=300)
parser.add_argument('--startup-timeout', type=int, default=120)
parser.add_argument('--source-progress-timeout', type=int, default=300)
parser.add_argument('--implementation-progress-timeout', type=int, default=120)
parser.add_argument('--source-read-command-budget', type=int, default=8)
parser.add_argument('--status', action='store_true')
args = parser.parse_args()
if (args.max_iterations < 0 or args.worker_timeout < 180 or args.idle_timeout < 180 or
not 120 <= args.startup_timeout <= args.worker_timeout or
args.source_progress_timeout < 180 or
not 120 <= args.implementation_progress_timeout <= args.source_progress_timeout or
args.source_read_command_budget < 8):
parser.error('Invalid iteration limit or worker timeout')
if args.status:
state = read_json(DIRECTORY / 'state.json', {})
running = alive(state.get('pid')) and identity(state['pid']) == state.get('identity')
worker_running = alive(state.get('childPid')) and identity(state['childPid']) == state.get('childIdentity')
try:
heartbeat_age = max(0, (datetime.now(timezone.utc) - datetime.fromisoformat(state['updatedAt'])).total_seconds())
except (KeyError, TypeError, ValueError):
heartbeat_age = None
work_state = state.get('workState', 'waiting_executor' if worker_running and not state.get('commands') else state.get('state', 'unknown'))
if not running:
work_state = 'supervisor_stopped'
elif heartbeat_age is None or heartbeat_age > 30:
work_state = 'supervisor_heartbeat_stale'
elif state.get('state') == 'running' and not worker_running:
work_state = 'worker_exited_pending_handoff'
print(json.dumps({
'task': TASK, 'state': state.get('state', 'not_started'), 'supervisorAlive': running,
'workerAlive': worker_running, 'pid': state.get('pid'), 'childPid': state.get('childPid'),
'cycle': state.get('cycle'), 'focus': state.get('focus'), 'updatedAt': state.get('updatedAt'),
'reason': state.get('reason'), 'commands': state.get('commands', 0),
'phase': state.get('phase'), 'phaseLabel': state.get('phaseLabel'),
'noProgressCycles': state.get('noProgressCycles', 0),
'noProgressWarning': state.get('noProgressWarning'),
'focusNoProgressWarning': state.get('focusNoProgressWarning'),
'lastActionAgeSeconds': state.get('lastActionAgeSeconds'),
'workState': work_state, 'heartbeatAgeSeconds': None if heartbeat_age is None else round(heartbeat_age, 1),
'firstActionSeconds': state.get('firstActionSeconds'),
'startupDeadlineSeconds': state.get('startupDeadlineSeconds'),
'provider': state.get('provider', {}),
'latestImplementation': state.get('latestImplementation'),
'deliveryStage': state.get('deliveryStage'),
'efficiency': {key: value for key, value in state.get('efficiency', {}).items() if key != 'issues'},
'openEfficiencyIssues': [value for value in state.get('efficiency', {}).get('issues', {}).values() if value.get('status') == 'open'][:8],
'deliveryProgress': state.get('deliveryStages', {}).get(state.get('deliveryStage', {}).get('id'), {}),
'changedProductionFiles': state.get('observedProductionFiles', []),
'activeCommandCount': state.get('activeCommandCount', 0),
'activeArtifactCommandCount': state.get('activeArtifactCommandCount', 0),
'sourceProgressDeadlineSeconds': state.get('sourceProgressDeadlineSeconds'),
'sourceIdleSeconds': state.get('sourceIdleSeconds'),
'sourceReadCommands': state.get('sourceReadCommands'),
'sourceReadCommandBudget': state.get('sourceReadCommandBudget'),
'implementationProgressDeadlineSeconds': state.get('implementationProgressDeadlineSeconds'),
'lastSourceChangeAgeSeconds': state.get('lastSourceChangeAgeSeconds'),
'sharedBuild': state.get('sharedBuild'),
'validation': 'NOT_RUN', 'acceptance': 'NOT_CLAIMED',
'focuses': {name: {'lastCycle': value.get('lastCycle'), 'failures': value.get('failures', 0),
'unchangedBlockerRounds': value.get('unchangedBlockerRounds', 0),
'noProgressRounds': value.get('noProgressRounds', 0),
'recoveryDirection': value.get('recoveryDirection'),
'nextFocus': value.get('handoff', {}).get('nextFocus'),
'interrupted': value.get('interrupted')}
for name, value in state.get('focusHandoffs', {}).items()},
}, ensure_ascii=False, indent=2))
return 0
runner = ImplementationLoop(args)
signal.signal(signal.SIGINT, runner.stop)
signal.signal(signal.SIGTERM, runner.stop)
try:
code = runner.run()
if code == 75:
# run() released both project locks after the worker's handoff.
os.execv(sys.executable, [sys.executable, str(Path(__file__).resolve()), *sys.argv[1:]])
return code
except BaseException as error:
runner.signal_child(signal.SIGTERM)
if runner.child:
try:
runner.child.wait(timeout=10)
except subprocess.TimeoutExpired:
runner.signal_child(signal.SIGKILL)
runner.child.wait()
# A rejected competing launcher must never overwrite the owner's state.
if runner.owns_project:
runner.checkpoint('needs_reconcile', reason=str(error))
raise
if __name__ == '__main__':
raise SystemExit(main())