diff --git a/.agents/skills/manage-qt-pyside6-lifetimes/references/qt-pyside6-object-lifetime-guidelines.md b/.agents/skills/manage-qt-pyside6-lifetimes/references/qt-pyside6-object-lifetime-guidelines.md index 34da11e..d036493 100644 --- a/.agents/skills/manage-qt-pyside6-lifetimes/references/qt-pyside6-object-lifetime-guidelines.md +++ b/.agents/skills/manage-qt-pyside6-lifetimes/references/qt-pyside6-object-lifetime-guidelines.md @@ -224,7 +224,7 @@ Avoid unnecessary manual disconnect boilerplate when Qt already manages the conn Native PySide6 and a Nuitka-compiled application do not necessarily have the same Python-callable retention graph. In the locally selected and inspected toolchain -(Nuitka 4.1.3, PySide6 6.8.3), Nuitka's standard PySide6 package configuration patches +(Nuitka 4.2.1, PySide6 6.8.3), Nuitka's standard PySide6 package configuration patches `SignalInstance.connect()` and `QTimer.singleShot()`. When the callback is a compiled bound method, the generated post-import code protects it in a process-global list named `_protected` and may also expose its underlying function on the receiver class. The @@ -233,10 +233,9 @@ attribute. This protection keeps the bound receiver strongly reachable. Repeated transient receivers can therefore grow for the whole packaged-process lifetime even when native CPython destroys them. -The same protection pattern exists in current upstream Nuitka source. Related PySide6 -workaround behavior is documented for earlier Nuitka/PySide6 combinations, but do not -assume an exact introduction version without checking the selected release. Always -inspect the package configuration installed in the environment being shipped. +This observation is specific to the inspected local package configuration. Do not +assume an exact introduction version or unchanged upstream behavior; inspect the +package configuration installed in the environment being shipped. The following is prohibited for a transient or repeatedly created receiver: @@ -287,6 +286,14 @@ receiver itself is gone. When the sender is the receiver or a child/descendant, destruction already removes the connection and the extra destroyed-receiver hook is unnecessary. Supplying `sender=` still keeps only a weak sender reference. +That subtree check describes the ownership at registration time. When a child can be +detached or reused, its feature must disconnect its own stored connection handles +before reparenting and clear stale child references on native destruction. +`AppQMessageBox` button registration and its detach/reuse tests cover this boundary. +User callbacks can also synchronously destroy a sender or receiver during delivery; +recheck native validity before further Qt work, as the HTTP completion and abort +regressions in `test_service_runtime.py` demonstrate. + `singleShotWeakly(milliseconds, receiver, 'methodName')` uses the same named weak dispatcher without a sender. If the receiver wrapper is gone or its native QObject is invalid when the timer fires, delivery becomes a no-op. @@ -443,7 +450,7 @@ alone is not proof of no leak. Treat the exact retention workaround as version-sensitive. The repository currently ships several PySide6 versions across platform artifacts, while the inspected local -development combination is PySide6 6.8.3 with Nuitka 4.1.3. Re-inspect the selected +development combination is PySide6 6.8.3 with Nuitka 4.2.1. Re-inspect the selected Nuitka package configuration and run the representative compiled probe when a release toolchain changes; do not generalize one combination's private `_protected` visibility or behavior to every native or packaged build. diff --git a/Furious/Qt/AGENTS.md b/Furious/Qt/AGENTS.md index 04ff930..d76cafd 100644 --- a/Furious/Qt/AGENTS.md +++ b/Furious/Qt/AGENTS.md @@ -47,6 +47,9 @@ behavior, and lifetime primitives; pages and services consume them without creat `singleShotWeakly()` for deferred named-method delivery. A closure/partial that captures the receiver does not substitute for weak dispatch. Bounded dialog-completion closures may intentionally capture context; verify their native destruction/disconnection boundary and both owner-first and sender-first teardown. +- Detaching a child ends the shared QObject-tree lifetime assumption. Disconnect the registrations owned by that + feature before reparenting, preserve unrelated listeners, and remove default/escape/selection references when the + child dies. `AppQMessageBox` button reuse and native-destruction regressions exercise this boundary. - Direct connections are appropriate for deliberately shared persistent lifetimes; syntax alone does not prove a leak. Recheck the selected Nuitka/PySide6 callback protection when the toolchain changes. Static weak method names are runtime contracts, so renames must update registrations and tests. Weak dispatch itself does not marshal @@ -60,7 +63,9 @@ behavior, and lifetime primitives; pages and services consume them without creat - Every `QNetworkReply` has one manager/context owner, one freshness rule, and one terminal deletion path. Request context must also be released when native destruction skips `finished`, including manager-first teardown with retained Python wrappers. Use the shared network-manager tracking boundary; cleanup must not capture a reply - strongly. Do not attach ad-hoc attributes to third-party Qt objects or multiply timers/connections across show/hide cycles. + strongly. User hooks and signal delivery can synchronously destroy the reply or manager; recheck native validity + before subsequent hooks or Qt cleanup. `test_service_runtime.py` covers completion and abort reentrancy. + Do not attach ad-hoc attributes to third-party Qt objects or multiply timers/connections across show/hide cycles. - Queued delivery never transfers ownership implicitly. The sender may finish before delivery, so callbacks resolve a still-valid receiver and current generation in the receiver's Qt thread before touching widgets, models, or wrappers. diff --git a/tests/README.md b/tests/README.md index 75d136d..27f9932 100644 --- a/tests/README.md +++ b/tests/README.md @@ -52,7 +52,7 @@ strategy in an individual test. | Xray/Hysteria2 native-TUN ownership and proxy-only stripping | `test_native_tun_semantics.py` | | Rolling metrics, stable buckets, lazy rendering, and hover | `test_metrics_behavior.py` | | Proxy-only endpoint discovery, caching, and presentation | `test_endpoint_info.py` | -| Bounded service work, update validation, plugin UI, and worker lifetime | `test_service_runtime.py` | +| Bounded service work, update validation, plugin UI, worker lifetime, and reentrant HTTP completion/cancellation | `test_service_runtime.py` | | Frozenlib state helpers and mocked platform-operation boundaries | `test_frozenlib.py` | | Settings sandbox, navigation overlay behavior, public exports, and scale/theme isolation | `test_isolation_and_navigation.py`, `test_public_api.py`, `test_layout_matrix.py` | | Shared Fluent visual states, zero table padding and intact rounded list corners, rounded menu/combo popup transparency and interaction, and native line-edit clear-button alignment and theme changes | `test_stylesheet_states.py` | @@ -62,7 +62,7 @@ strategy in an individual test. | Editor mappings, lazy log rendering, routing-rule internal-move drag restrictions, Move menu/shortcuts and persisted order, message-box/connection UI | `test_ui_behavior.py` | | Bounded, incremental, cancellable QR export and snapshot/lifetime safety | `test_qr_export_scalability.py` | | Real keyboard/mouse/focus, proxy mapping, shared Home/Settings state, Home empty/filter recovery, shared menus and Tests-popup selection highlighting, direct small profile operations and batched imports/deletions with throttled progress and stable cancellation targets, and transient editor input | `test_qt_interactions.py` | -| Direct Qt ownership and destruction across independent UI families | `test_qt_lifetime.py` | +| Direct Qt ownership and destruction across independent UI families, message-box button detachment/reuse, and callback-driven owner destruction | `test_qt_lifetime.py` | | Batched real/probe Qt object, QR rendering/window lifecycle, handle, Python allocation, and RSS trends | `test_qt_stress.py` | | Repeated harmless subprocess, pipe, thread, handle, and RSS trends | `test_process_stress.py` | | Opt-in release-confidence counts (100 app children, 100 external cores, 100k metrics, 40k logs, 20k navigation, 5k plugins, 1k dialogs, 1k real QR tabs) | `test_very_heavy.py` |