Retain K1 connection between scans and admit the next named acquisition
This commit is contained in:
@@ -3814,6 +3814,18 @@ class XgridsK1CompatibilityService:
|
||||
expected_session_generation: int | None = None,
|
||||
expected_state_revision: int | None = None,
|
||||
) -> None:
|
||||
current = self._application_control_session.snapshot()
|
||||
if current.get("state") == "completed" and current.get("control_socket_open") is True:
|
||||
self._application_control_session.close_prestart(
|
||||
expected_session_generation=expected_session_generation,
|
||||
expected_state_revision=expected_state_revision,
|
||||
)
|
||||
self._application_control_session.close()
|
||||
retired = self._application_control_session.snapshot()
|
||||
if retired.get("session_generation") != current.get("session_generation"):
|
||||
raise ApplicationAcceptanceError("control session changed during retirement")
|
||||
expected_session_generation = retired.get("session_generation")
|
||||
expected_state_revision = retired.get("state_revision")
|
||||
self._application_control_session.retire_for_network_change(
|
||||
allow_terminal_failure=allow_terminal_failure,
|
||||
expected_session_generation=expected_session_generation,
|
||||
@@ -27534,7 +27546,10 @@ class XgridsK1CompatibilityService:
|
||||
}
|
||||
supervisor = self._connection_supervisor.snapshot()
|
||||
if (
|
||||
control_state in active_control_states
|
||||
(control_state in active_control_states or (
|
||||
control_state == "completed"
|
||||
and application_control_session.get("control_socket_open") is True
|
||||
))
|
||||
and isinstance(verified_control, Mapping)
|
||||
and supervisor.intent is not None
|
||||
and supervisor.endpoint.target is not None
|
||||
@@ -27703,7 +27718,10 @@ class XgridsK1CompatibilityService:
|
||||
|
||||
supervisor = self._connection_supervisor.snapshot()
|
||||
if (
|
||||
control_state in {"idle", "completed", "closed", "failed"}
|
||||
(control_state in {"idle", "closed", "failed"} or (
|
||||
control_state == "completed"
|
||||
and application_control_session.get("control_socket_open") is not True
|
||||
))
|
||||
and supervisor.control_plane.state == "healthy"
|
||||
and supervisor.control_plane.session_id is not None
|
||||
and supervisor.intent is not None
|
||||
|
||||
@@ -241,12 +241,17 @@ class NodeK1Sensor:
|
||||
control = state.get("application_control_session") or {}
|
||||
phase = control.get("state")
|
||||
physical = control.get("physical_command") or state.get("physical_command") or {}
|
||||
if ("acquisition.start" in dispatched
|
||||
and phase in {"start-requested", "initializing", "scanning"}):
|
||||
# The just-admitted START owns its pending physical edge.
|
||||
# Returning its state does not dispatch or reconcile it again.
|
||||
return project_sensor(state, node_id)
|
||||
if physical.get("requires_reconciliation"):
|
||||
raise ValueError("Physical state requires explicit reconciliation")
|
||||
acquisition = state.get("acquisition") or {}
|
||||
payload = {"expected_snapshot_runtime_id": runtime}
|
||||
next_action = None
|
||||
if phase == "connection-ready":
|
||||
if phase in {"connection-ready", "completed"}:
|
||||
if control.get("inspection_only"):
|
||||
next_action = "application-control.session.open"
|
||||
payload.update(acceptance, timezone_name="UTC")
|
||||
@@ -276,7 +281,7 @@ class NodeK1Sensor:
|
||||
expected_state_revision=acquisition["state_revision"],
|
||||
physical_acceptance=acceptance,
|
||||
)
|
||||
elif phase in {"failed", "idle", "closed", "completed"}:
|
||||
elif phase in {"failed", "idle", "closed"}:
|
||||
raise ValueError("K1 control not ready")
|
||||
elif phase in {"start-requested", "initializing", "scanning"}:
|
||||
return project_sensor(state, node_id)
|
||||
|
||||
@@ -285,6 +285,7 @@ class PhysicalAcceptanceDialogueExecutor:
|
||||
self._active_authority: ApplicationControlAuthority | None = None
|
||||
self._active_binding: LiveDeviceControlBinding | None = None
|
||||
self._prepared_binding: LiveDeviceControlBinding | None = None
|
||||
self._standby_binding: LiveDeviceControlBinding | None = None
|
||||
self._checkpoint_owner = object()
|
||||
self._issued_checkpoint: str | None = None
|
||||
self._start_permit_snapshot: dict[str, object] | None = None
|
||||
@@ -801,9 +802,23 @@ class PhysicalAcceptanceDialogueExecutor:
|
||||
allowed_response_topics={MODELING_STATUS_RESPONSE_TOPIC},
|
||||
)
|
||||
self._dialogue_stage = "standby-confirmed"
|
||||
self._standby_binding = binding
|
||||
self._active_authority = None
|
||||
self._active_binding = None
|
||||
|
||||
def maintain_standby_until_next_acquisition(self, requested: Callable[[], bool]) -> None:
|
||||
"""Pump the retained socket after READY; issue no device commands."""
|
||||
if self._dialogue_stage != "standby-confirmed" or self._standby_binding is None:
|
||||
raise ApplicationAcceptanceError("next acquisition requires confirmed standby")
|
||||
while True:
|
||||
self._transport.maintain_open_for(
|
||||
CONTROL_NETWORK_PUMP_QUANTUM_SECONDS,
|
||||
allowed_response_topics={MODELING_STATUS_RESPONSE_TOPIC},
|
||||
)
|
||||
self._transport.validate_bound_status(self._standby_binding)
|
||||
if requested() and self._transport.pre_start_ready(self._standby_binding):
|
||||
return
|
||||
|
||||
def snapshot(self) -> dict[str, object]:
|
||||
return {
|
||||
"mode": "physical-acceptance-only",
|
||||
|
||||
@@ -170,8 +170,11 @@ class OperatorPresenceConfirmation:
|
||||
|
||||
|
||||
class InteractiveApplicationControlSession:
|
||||
"""Own one canonical K1 MQTT dialogue across explicit operator UI events.
|
||||
"""Own canonical acquisition dialogues and retain control between scans.
|
||||
|
||||
Each explicit new scan gets a fresh one-shot MQTT dialogue on the existing
|
||||
device/network binding. Completed scans keep pumping their idle connection
|
||||
until that request or explicit local retirement; no Wi-Fi setup is repeated.
|
||||
Only this background thread touches the MQTT client. UI requests merely
|
||||
release one named checkpoint. No checkpoint is advanced by elapsed time,
|
||||
and neither START nor STOP has an automatic retry path.
|
||||
@@ -361,7 +364,7 @@ class InteractiveApplicationControlSession:
|
||||
expected_session_generation=expected_session_generation,
|
||||
expected_state_revision=expected_state_revision,
|
||||
)
|
||||
self._require_phase_locked("connection-ready")
|
||||
self._require_workspace_entry_locked()
|
||||
if self._inspection_only and not self._inspection_promotion_allowed:
|
||||
raise ApplicationAcceptanceError(
|
||||
"read-only inspection has not completed its Verify boundary"
|
||||
@@ -376,7 +379,7 @@ class InteractiveApplicationControlSession:
|
||||
expected_session_generation=expected_session_generation,
|
||||
expected_state_revision=expected_state_revision,
|
||||
)
|
||||
self._require_phase_locked("connection-ready")
|
||||
self._require_workspace_entry_locked()
|
||||
if self._inspection_only and not self._inspection_promotion_allowed:
|
||||
raise ApplicationAcceptanceError(
|
||||
"read-only inspection has not completed its Verify boundary"
|
||||
@@ -385,6 +388,12 @@ class InteractiveApplicationControlSession:
|
||||
self._workspace_requested.set()
|
||||
return self.snapshot()
|
||||
|
||||
def _require_workspace_entry_locked(self) -> None:
|
||||
if (self._phase == "completed" and self._transport is not None
|
||||
and not self._cancel_requested):
|
||||
return
|
||||
self._require_phase_locked("connection-ready")
|
||||
|
||||
def validate_connection_binding(self) -> None:
|
||||
"""Fail closed when the DeviceInfo-bound route lost command authority."""
|
||||
|
||||
@@ -695,6 +704,7 @@ class InteractiveApplicationControlSession:
|
||||
"connection-ready",
|
||||
"workspace-ready",
|
||||
"project-ready",
|
||||
"completed",
|
||||
}:
|
||||
raise ApplicationAcceptanceError(
|
||||
"control session can be closed safely only between pre-START checkpoints"
|
||||
@@ -825,9 +835,13 @@ class InteractiveApplicationControlSession:
|
||||
"state": phase,
|
||||
"session_generation": self._run_generation,
|
||||
"state_revision": self._state_revision,
|
||||
"control_socket_open": phase not in {"idle", "completed", "closed", "failed"},
|
||||
"control_socket_open": (
|
||||
self._transport is not None and phase not in {"idle", "closed", "failed"}
|
||||
),
|
||||
"can_open": self._can_open_locked(),
|
||||
"can_enter_workspace": phase == "connection-ready",
|
||||
"can_enter_workspace": phase == "connection-ready" or (
|
||||
phase == "completed" and self._transport is not None
|
||||
),
|
||||
"can_prepare_project": phase == "workspace-ready",
|
||||
"can_start": phase == "project-ready",
|
||||
"can_stop": phase == "scanning",
|
||||
@@ -854,6 +868,7 @@ class InteractiveApplicationControlSession:
|
||||
executor: PhysicalAcceptanceDialogueExecutor | None = None
|
||||
transport: ReviewedApplicationMqttTransport | None = None
|
||||
stop_publish_attempts_before_dispatch: int | None = None
|
||||
completed_acquisition = False
|
||||
try:
|
||||
with self._lock:
|
||||
host = self._host
|
||||
@@ -862,265 +877,301 @@ class InteractiveApplicationControlSession:
|
||||
raise ApplicationAcceptanceError("control session inputs are unavailable")
|
||||
|
||||
authority = self._authority_loader.load()
|
||||
transport = self._transport_factory(host)
|
||||
coordinator = self._physical_command_coordinator
|
||||
if coordinator is not None:
|
||||
transport.install_evidence_observer(coordinator)
|
||||
if self._connection_path_validator is not None:
|
||||
self._validate_connection_path("control-open-preflight")
|
||||
transport.install_dispatch_guard(self._acquire_connection_dispatch_lease)
|
||||
with self._lock:
|
||||
self._transport = transport
|
||||
transport.open()
|
||||
orchestrator = ShadowApplicationBootstrapOrchestrator(
|
||||
authority,
|
||||
epoch_seconds=self._epoch_seconds(),
|
||||
timezone_name=timezone_name,
|
||||
)
|
||||
executor = PhysicalAcceptanceDialogueExecutor(transport)
|
||||
with self._lock:
|
||||
inspection_only = self._inspection_only
|
||||
# DeviceInfo (ordinal 1) is the only bootstrap request that may
|
||||
# cross the socket before durable physical-target admission. In
|
||||
# particular, ordinal 4 mutates the K1 clock, so the legacy
|
||||
# collapsed ordinals 1-6 path must never run before the coordinator
|
||||
# can reject a retired identity discovered under a fresh BLE
|
||||
# transport UUID.
|
||||
binding = executor.run_read_only_inspection_stage(orchestrator)
|
||||
control_session_id = f"application-control-{generation}-{time.monotonic_ns()}"
|
||||
with self._lock:
|
||||
connection_binding = self._connection_binding
|
||||
if coordinator is not None:
|
||||
if connection_binding is None:
|
||||
raise ApplicationAcceptanceError(
|
||||
"durable physical control requires an exact connection binding"
|
||||
)
|
||||
coordinator.bind_control_session(
|
||||
PhysicalCommandRuntimeBinding(
|
||||
vendor_device_id_sha256=hash_physical_identity(binding.vendor_device_id),
|
||||
device_serial_sha256=hash_physical_identity(binding.device_serial),
|
||||
compatibility_profile_id=COMPATIBILITY_PROFILE_ID,
|
||||
intent_id=connection_binding.intent_id,
|
||||
transport_ref=connection_binding.transport_ref,
|
||||
connection_mode=connection_binding.connection_mode,
|
||||
target_ipv4=connection_binding.target_ipv4,
|
||||
target_port=connection_binding.target_port,
|
||||
host_path_epoch=connection_binding.host_path_epoch,
|
||||
control_session_id=control_session_id,
|
||||
producer_generation=generation,
|
||||
)
|
||||
while True:
|
||||
transport = self._transport_factory(host)
|
||||
if coordinator is not None:
|
||||
transport.install_evidence_observer(coordinator)
|
||||
if self._connection_path_validator is not None:
|
||||
self._validate_connection_path("control-open-preflight")
|
||||
transport.install_dispatch_guard(self._acquire_connection_dispatch_lease)
|
||||
with self._lock:
|
||||
self._transport = transport
|
||||
transport.open()
|
||||
completed_acquisition = False
|
||||
orchestrator = ShadowApplicationBootstrapOrchestrator(
|
||||
authority,
|
||||
epoch_seconds=self._epoch_seconds(),
|
||||
timezone_name=timezone_name,
|
||||
)
|
||||
if not inspection_only:
|
||||
binding = executor.complete_connection_stage(
|
||||
orchestrator,
|
||||
expected_binding=binding,
|
||||
)
|
||||
# Publish the identity proof and the phase under one lock so a
|
||||
# consumer cannot observe connection-ready without its DeviceInfo
|
||||
# evidence (or evidence while still claiming to be connecting).
|
||||
with self._lock:
|
||||
self._control_authority = authority
|
||||
self._live_control_binding = binding
|
||||
transport_snapshot = self._live_transport_snapshot_locked()
|
||||
self._verified_control = {
|
||||
"logical_device_id": binding.vendor_device_id,
|
||||
"compatibility_profile_id": COMPATIBILITY_PROFILE_ID,
|
||||
"control_session_id": control_session_id,
|
||||
"producer_generation": generation,
|
||||
"source": "mqtt-device-info",
|
||||
**self._control_proof_fields(transport_snapshot),
|
||||
**(
|
||||
{
|
||||
"intent_id": connection_binding.intent_id,
|
||||
"transport_ref": connection_binding.transport_ref,
|
||||
"host_path_epoch": connection_binding.host_path_epoch,
|
||||
"target_ipv4": connection_binding.target_ipv4,
|
||||
"target_port": connection_binding.target_port,
|
||||
"connection_mode": connection_binding.connection_mode,
|
||||
}
|
||||
if connection_binding is not None
|
||||
else {}
|
||||
),
|
||||
}
|
||||
self._set_phase_locked("connection-ready")
|
||||
|
||||
workspace = executor.wait_for_operator_checkpoint(
|
||||
"workspace-entered",
|
||||
self._workspace_requested.is_set,
|
||||
reconciled_active_observed=self._active_recovery_requested.is_set,
|
||||
)
|
||||
if workspace is None:
|
||||
self._validate_connection_binding("active-recovery-adoption")
|
||||
executor.adopt_reconciled_scanning(
|
||||
authority=authority,
|
||||
binding=binding,
|
||||
)
|
||||
with self._scanning_transition_gate:
|
||||
# Recovery adopts an already-active physical acquisition.
|
||||
# Its checkpoint deliberately retains an open transport
|
||||
# gap until the post-Rerun PCL confirmation hook closes it;
|
||||
# nevertheless an operator STOP must remain admissible in
|
||||
# this interval and can cease that gap with terminal READY.
|
||||
self._scanning_observer_confirmed = True
|
||||
self._set_phase("scanning")
|
||||
else:
|
||||
if inspection_only:
|
||||
self._validate_connection_binding_snapshot(
|
||||
"inspection-promotion-pre-dispatch"
|
||||
executor = PhysicalAcceptanceDialogueExecutor(transport)
|
||||
with self._lock:
|
||||
inspection_only = self._inspection_only
|
||||
# DeviceInfo (ordinal 1) is the only bootstrap request that may
|
||||
# cross the socket before durable physical-target admission. In
|
||||
# particular, ordinal 4 mutates the K1 clock, so the legacy
|
||||
# collapsed ordinals 1-6 path must never run before the coordinator
|
||||
# can reject a retired identity discovered under a fresh BLE
|
||||
# transport UUID.
|
||||
binding = executor.run_read_only_inspection_stage(orchestrator)
|
||||
control_session_id = f"application-control-{generation}-{time.monotonic_ns()}"
|
||||
with self._lock:
|
||||
connection_binding = self._connection_binding
|
||||
if coordinator is not None:
|
||||
if connection_binding is None:
|
||||
raise ApplicationAcceptanceError(
|
||||
"durable physical control requires an exact connection binding"
|
||||
)
|
||||
coordinator.bind_control_session(
|
||||
PhysicalCommandRuntimeBinding(
|
||||
vendor_device_id_sha256=hash_physical_identity(binding.vendor_device_id),
|
||||
device_serial_sha256=hash_physical_identity(binding.device_serial),
|
||||
compatibility_profile_id=COMPATIBILITY_PROFILE_ID,
|
||||
intent_id=connection_binding.intent_id,
|
||||
transport_ref=connection_binding.transport_ref,
|
||||
connection_mode=connection_binding.connection_mode,
|
||||
target_ipv4=connection_binding.target_ipv4,
|
||||
target_port=connection_binding.target_port,
|
||||
host_path_epoch=connection_binding.host_path_epoch,
|
||||
control_session_id=control_session_id,
|
||||
producer_generation=generation,
|
||||
)
|
||||
)
|
||||
if not inspection_only:
|
||||
binding = executor.complete_connection_stage(
|
||||
orchestrator,
|
||||
expected_binding=binding,
|
||||
)
|
||||
self._validate_connection_binding_snapshot(
|
||||
"inspection-promotion-post-response"
|
||||
# Publish the identity proof and the phase under one lock so a
|
||||
# consumer cannot observe connection-ready without its DeviceInfo
|
||||
# evidence (or evidence while still claiming to be connecting).
|
||||
with self._lock:
|
||||
self._control_authority = authority
|
||||
self._live_control_binding = binding
|
||||
transport_snapshot = self._live_transport_snapshot_locked()
|
||||
self._verified_control = {
|
||||
"logical_device_id": binding.vendor_device_id,
|
||||
"compatibility_profile_id": COMPATIBILITY_PROFILE_ID,
|
||||
"control_session_id": control_session_id,
|
||||
"producer_generation": generation,
|
||||
"source": "mqtt-device-info",
|
||||
**self._control_proof_fields(transport_snapshot),
|
||||
**(
|
||||
{
|
||||
"intent_id": connection_binding.intent_id,
|
||||
"transport_ref": connection_binding.transport_ref,
|
||||
"host_path_epoch": connection_binding.host_path_epoch,
|
||||
"target_ipv4": connection_binding.target_ipv4,
|
||||
"target_port": connection_binding.target_port,
|
||||
"connection_mode": connection_binding.connection_mode,
|
||||
}
|
||||
if connection_binding is not None
|
||||
else {}
|
||||
),
|
||||
}
|
||||
self._set_phase_locked(
|
||||
"workspace-requested" if self._workspace_requested.is_set()
|
||||
else "connection-ready"
|
||||
)
|
||||
self._validate_connection_binding_snapshot("workspace-entry-pre-dispatch")
|
||||
executor.run_workspace_entry_stage(
|
||||
orchestrator,
|
||||
workspace,
|
||||
dispatch_guard=lambda: self._validate_connection_binding_snapshot(
|
||||
"workspace-entry-dispatch"
|
||||
),
|
||||
)
|
||||
self._validate_connection_binding_snapshot("workspace-entry-post-response")
|
||||
self._set_phase("workspace-ready")
|
||||
|
||||
project = executor.wait_for_operator_checkpoint(
|
||||
"project-prompt-opened",
|
||||
self._project_requested.is_set,
|
||||
workspace = executor.wait_for_operator_checkpoint(
|
||||
"workspace-entered",
|
||||
self._workspace_requested.is_set,
|
||||
reconciled_active_observed=self._active_recovery_requested.is_set,
|
||||
)
|
||||
assert project is not None
|
||||
self._validate_connection_binding_snapshot("project-prompt-pre-dispatch")
|
||||
binding = executor.run_project_prompt_stage(
|
||||
orchestrator,
|
||||
project,
|
||||
dispatch_guard=lambda: self._validate_connection_binding_snapshot(
|
||||
"project-prompt-dispatch"
|
||||
),
|
||||
)
|
||||
self._validate_connection_binding_snapshot("project-prompt-post-response")
|
||||
self._set_phase("project-ready")
|
||||
|
||||
start_checkpoint = executor.wait_for_operator_checkpoint(
|
||||
"start-confirmed",
|
||||
self._start_requested.is_set,
|
||||
)
|
||||
assert start_checkpoint is not None
|
||||
start_command, start_confirmation = self._start_request()
|
||||
start_permit = PhysicalAcceptancePermit(
|
||||
start_confirmation.checklist(ModelingAction.START)
|
||||
)
|
||||
self._validate_connection_binding_snapshot("start-pre-dispatch")
|
||||
self._set_phase("initializing")
|
||||
start_active_observed = False
|
||||
start_transition_gate_acquired = False
|
||||
|
||||
def observe_start_active() -> None:
|
||||
nonlocal start_active_observed, start_transition_gate_acquired
|
||||
if start_active_observed:
|
||||
return
|
||||
self._scanning_transition_gate.acquire()
|
||||
start_transition_gate_acquired = True
|
||||
try:
|
||||
if coordinator is not None:
|
||||
coordinator.resolve("start")
|
||||
# Activate the durable recovery checkpoint at the same
|
||||
# exact SCANNING proof as the physical ledger. The
|
||||
# gate remains held while the read-only ordinals 13-14
|
||||
# finish, so STOP cannot overtake the later public
|
||||
# ``scanning`` transition.
|
||||
if not self._scanning_observer_confirmed:
|
||||
self._scanning_observer_confirmed = (
|
||||
self._notify_scanning_observer()
|
||||
)
|
||||
start_active_observed = True
|
||||
except BaseException:
|
||||
self._scanning_transition_gate.release()
|
||||
start_transition_gate_acquired = False
|
||||
raise
|
||||
|
||||
try:
|
||||
executor.execute_canonical_start(
|
||||
start_command,
|
||||
build_canonical_post_start_observation(authority, binding),
|
||||
if workspace is None:
|
||||
self._validate_connection_binding("active-recovery-adoption")
|
||||
executor.adopt_reconciled_scanning(
|
||||
authority=authority,
|
||||
binding=binding,
|
||||
permit=start_permit,
|
||||
checkpoint=start_checkpoint,
|
||||
dispatch_guard=lambda: self._validate_connection_binding_snapshot(
|
||||
"start-dispatch"
|
||||
),
|
||||
start_active_observer=observe_start_active,
|
||||
)
|
||||
# Compatibility executors used by lower-level integrations may
|
||||
# return without invoking the new proof callback. A normal
|
||||
# production executor invokes it before the read-only refresh;
|
||||
# this idempotent fallback remains strictly post-success.
|
||||
observe_start_active()
|
||||
self._validate_connection_binding_snapshot("start-post-response")
|
||||
self._set_phase("scanning")
|
||||
finally:
|
||||
if start_transition_gate_acquired:
|
||||
self._scanning_transition_gate.release()
|
||||
with self._scanning_transition_gate:
|
||||
# Recovery adopts an already-active physical acquisition.
|
||||
# Its checkpoint deliberately retains an open transport
|
||||
# gap until the post-Rerun PCL confirmation hook closes it;
|
||||
# nevertheless an operator STOP must remain admissible in
|
||||
# this interval and can cease that gap with terminal READY.
|
||||
self._scanning_observer_confirmed = True
|
||||
self._set_phase("scanning")
|
||||
else:
|
||||
if inspection_only:
|
||||
self._validate_connection_binding_snapshot(
|
||||
"inspection-promotion-pre-dispatch"
|
||||
)
|
||||
binding = executor.complete_connection_stage(
|
||||
orchestrator,
|
||||
expected_binding=binding,
|
||||
)
|
||||
self._validate_connection_binding_snapshot(
|
||||
"inspection-promotion-post-response"
|
||||
)
|
||||
self._validate_connection_binding_snapshot("workspace-entry-pre-dispatch")
|
||||
executor.run_workspace_entry_stage(
|
||||
orchestrator,
|
||||
workspace,
|
||||
dispatch_guard=lambda: self._validate_connection_binding_snapshot(
|
||||
"workspace-entry-dispatch"
|
||||
),
|
||||
)
|
||||
self._validate_connection_binding_snapshot("workspace-entry-post-response")
|
||||
self._set_phase("workspace-ready")
|
||||
|
||||
executor.maintain_active_until_stop_requested(self._stop_requested.is_set)
|
||||
(
|
||||
stop_command,
|
||||
stop_confirmation,
|
||||
stop_dispatch_admission_deadline_reached,
|
||||
) = self._stop_request()
|
||||
stop_permit = PhysicalAcceptancePermit(stop_confirmation.checklist(ModelingAction.STOP))
|
||||
# Capture the exact transport counter before any remaining
|
||||
# read-only validation. A deadline that expires during one of
|
||||
# those checks is still deterministic zero-publish evidence.
|
||||
stop_transport_before, stop_transport_before_available = (
|
||||
self._transport_snapshot_safely(transport)
|
||||
)
|
||||
if stop_transport_before_available:
|
||||
stop_publish_attempts_before_dispatch = self._json_int_or_none(
|
||||
stop_transport_before.get("publish_attempts")
|
||||
project = executor.wait_for_operator_checkpoint(
|
||||
"project-prompt-opened",
|
||||
self._project_requested.is_set,
|
||||
)
|
||||
assert project is not None
|
||||
self._validate_connection_binding_snapshot("project-prompt-pre-dispatch")
|
||||
binding = executor.run_project_prompt_stage(
|
||||
orchestrator,
|
||||
project,
|
||||
dispatch_guard=lambda: self._validate_connection_binding_snapshot(
|
||||
"project-prompt-dispatch"
|
||||
),
|
||||
)
|
||||
self._validate_connection_binding_snapshot("project-prompt-post-response")
|
||||
self._set_phase("project-ready")
|
||||
|
||||
start_checkpoint = executor.wait_for_operator_checkpoint(
|
||||
"start-confirmed",
|
||||
self._start_requested.is_set,
|
||||
)
|
||||
assert start_checkpoint is not None
|
||||
start_command, start_confirmation = self._start_request()
|
||||
start_permit = PhysicalAcceptancePermit(
|
||||
start_confirmation.checklist(ModelingAction.START)
|
||||
)
|
||||
self._validate_connection_binding_snapshot("start-pre-dispatch")
|
||||
self._set_phase("initializing")
|
||||
start_active_observed = False
|
||||
start_transition_gate_acquired = False
|
||||
|
||||
def observe_start_active() -> None:
|
||||
nonlocal start_active_observed, start_transition_gate_acquired
|
||||
if start_active_observed:
|
||||
return
|
||||
self._scanning_transition_gate.acquire()
|
||||
start_transition_gate_acquired = True
|
||||
try:
|
||||
if coordinator is not None:
|
||||
coordinator.resolve("start")
|
||||
# Activate the durable recovery checkpoint at the same
|
||||
# exact SCANNING proof as the physical ledger. The
|
||||
# gate remains held while the read-only ordinals 13-14
|
||||
# finish, so STOP cannot overtake the later public
|
||||
# ``scanning`` transition.
|
||||
if not self._scanning_observer_confirmed:
|
||||
self._scanning_observer_confirmed = (
|
||||
self._notify_scanning_observer()
|
||||
)
|
||||
start_active_observed = True
|
||||
except BaseException:
|
||||
self._scanning_transition_gate.release()
|
||||
start_transition_gate_acquired = False
|
||||
raise
|
||||
|
||||
try:
|
||||
executor.execute_canonical_start(
|
||||
start_command,
|
||||
build_canonical_post_start_observation(authority, binding),
|
||||
authority=authority,
|
||||
binding=binding,
|
||||
permit=start_permit,
|
||||
checkpoint=start_checkpoint,
|
||||
dispatch_guard=lambda: self._validate_connection_binding_snapshot(
|
||||
"start-dispatch"
|
||||
),
|
||||
start_active_observer=observe_start_active,
|
||||
)
|
||||
# Compatibility executors used by lower-level integrations may
|
||||
# return without invoking the new proof callback. A normal
|
||||
# production executor invokes it before the read-only refresh;
|
||||
# this idempotent fallback remains strictly post-success.
|
||||
observe_start_active()
|
||||
self._validate_connection_binding_snapshot("start-post-response")
|
||||
self._set_phase("scanning")
|
||||
finally:
|
||||
if start_transition_gate_acquired:
|
||||
self._scanning_transition_gate.release()
|
||||
|
||||
executor.maintain_active_until_stop_requested(self._stop_requested.is_set)
|
||||
(
|
||||
stop_command,
|
||||
stop_confirmation,
|
||||
stop_dispatch_admission_deadline_reached,
|
||||
) = self._stop_request()
|
||||
stop_permit = PhysicalAcceptancePermit(
|
||||
stop_confirmation.checklist(ModelingAction.STOP)
|
||||
)
|
||||
self._require_stop_dispatch_deadline_open(
|
||||
stop_dispatch_admission_deadline_reached
|
||||
)
|
||||
self._validate_connection_binding_snapshot("stop-pre-dispatch")
|
||||
self._require_stop_dispatch_deadline_open(
|
||||
stop_dispatch_admission_deadline_reached
|
||||
)
|
||||
self._set_phase("stopping")
|
||||
logger.info(
|
||||
"K1 STOP timing checkpoint",
|
||||
extra={
|
||||
"event_code": "k1_stop_dispatch_timing",
|
||||
"operation_stage": "pre-dispatch-validation-complete",
|
||||
"device_command_sent": False,
|
||||
},
|
||||
)
|
||||
executor.execute_canonical_stop(
|
||||
stop_command,
|
||||
stop_permit,
|
||||
dispatch_guard=lambda: self._validate_connection_binding_snapshot(
|
||||
"stop-dispatch"
|
||||
),
|
||||
dispatch_admission_deadline_reached=(
|
||||
# Capture the exact transport counter before any remaining
|
||||
# read-only validation. A deadline that expires during one of
|
||||
# those checks is still deterministic zero-publish evidence.
|
||||
stop_transport_before, stop_transport_before_available = (
|
||||
self._transport_snapshot_safely(transport)
|
||||
)
|
||||
if stop_transport_before_available:
|
||||
stop_publish_attempts_before_dispatch = self._json_int_or_none(
|
||||
stop_transport_before.get("publish_attempts")
|
||||
)
|
||||
self._require_stop_dispatch_deadline_open(
|
||||
stop_dispatch_admission_deadline_reached
|
||||
),
|
||||
)
|
||||
logger.info(
|
||||
"K1 STOP timing checkpoint",
|
||||
extra={
|
||||
"event_code": "k1_stop_dispatch_timing",
|
||||
"operation_stage": "correlated-application-response",
|
||||
"device_command_sent": True,
|
||||
},
|
||||
)
|
||||
self._validate_connection_binding_snapshot("stop-post-response")
|
||||
self._set_phase("awaiting-standby-confirmation")
|
||||
executor.maintain_post_stop_until_standby()
|
||||
if coordinator is not None:
|
||||
coordinator.resolve("stop")
|
||||
self._set_phase("completed")
|
||||
)
|
||||
self._validate_connection_binding_snapshot("stop-pre-dispatch")
|
||||
self._require_stop_dispatch_deadline_open(
|
||||
stop_dispatch_admission_deadline_reached
|
||||
)
|
||||
self._set_phase("stopping")
|
||||
logger.info(
|
||||
"K1 STOP timing checkpoint",
|
||||
extra={
|
||||
"event_code": "k1_stop_dispatch_timing",
|
||||
"operation_stage": "pre-dispatch-validation-complete",
|
||||
"device_command_sent": False,
|
||||
},
|
||||
)
|
||||
executor.execute_canonical_stop(
|
||||
stop_command,
|
||||
stop_permit,
|
||||
dispatch_guard=lambda: self._validate_connection_binding_snapshot(
|
||||
"stop-dispatch"
|
||||
),
|
||||
dispatch_admission_deadline_reached=(
|
||||
stop_dispatch_admission_deadline_reached
|
||||
),
|
||||
)
|
||||
logger.info(
|
||||
"K1 STOP timing checkpoint",
|
||||
extra={
|
||||
"event_code": "k1_stop_dispatch_timing",
|
||||
"operation_stage": "correlated-application-response",
|
||||
"device_command_sent": True,
|
||||
},
|
||||
)
|
||||
self._validate_connection_binding_snapshot("stop-post-response")
|
||||
self._set_phase("awaiting-standby-confirmation")
|
||||
executor.maintain_post_stop_until_standby()
|
||||
if coordinator is not None:
|
||||
coordinator.resolve("stop")
|
||||
with self._lock:
|
||||
# Clear only the acquisition checkpoints before publishing READY.
|
||||
# Network ownership and DeviceInfo proof remain on this socket.
|
||||
self._workspace_requested.clear()
|
||||
self._active_recovery_requested.clear()
|
||||
self._project_requested.clear()
|
||||
self._start_requested.clear()
|
||||
self._stop_requested.clear()
|
||||
self._start_confirmation = None
|
||||
self._stop_confirmation = None
|
||||
self._stop_dispatch_admission_deadline_reached = None
|
||||
self._prepared_start_command = None
|
||||
self._prepared_stop_command = None
|
||||
self._project_name = None
|
||||
self._scanning_observer_confirmed = self._scanning_observer is None
|
||||
completed_acquisition = True
|
||||
self._set_phase_locked("completed")
|
||||
executor.maintain_standby_until_next_acquisition(self._workspace_requested.is_set)
|
||||
self._validate_connection_binding_snapshot("next-acquisition-preflight")
|
||||
# Operation keys and response correlations are one-shot per MQTT
|
||||
# dialogue. A new explicit scan gets a fresh socket, never reset
|
||||
# consumption sets or ambiguous reuse of the old response IDs.
|
||||
transport.close()
|
||||
with self._lock:
|
||||
self._transport = None
|
||||
self._verified_control = None
|
||||
self._run_generation += 1
|
||||
generation = self._run_generation
|
||||
executor = None
|
||||
stop_publish_attempts_before_dispatch = None
|
||||
except Exception as exc:
|
||||
dialogue_snapshot, dialogue_snapshot_available = self._executor_snapshot_safely(
|
||||
executor
|
||||
@@ -1217,7 +1268,9 @@ class InteractiveApplicationControlSession:
|
||||
and stop_command_attempted is False
|
||||
and not diagnostic_evidence_unavailable
|
||||
)
|
||||
outcome_unknown = not definite_stop_admission_rejected_before_publish and (
|
||||
outcome_unknown = not (
|
||||
definite_stop_admission_rejected_before_publish or completed_acquisition
|
||||
) and (
|
||||
isinstance(exc, ApplicationCommandOutcomeUnknown)
|
||||
or modeling_command_attempted is True
|
||||
or bool(diagnostic_evidence_unavailable)
|
||||
@@ -1288,7 +1341,7 @@ class InteractiveApplicationControlSession:
|
||||
)
|
||||
is None
|
||||
)
|
||||
safe_to_retry = status_reconciled_prestart_failure or (
|
||||
safe_to_retry = completed_acquisition or status_reconciled_prestart_failure or (
|
||||
not outcome_unknown
|
||||
and (
|
||||
not transport_created
|
||||
|
||||
Reference in New Issue
Block a user