[GH-ISSUE #4667] [Bug] Lazy provider client can dispatch after close begins during acquisition handoff #2433

Closed
opened 2026-10-17 21:04:35 +00:00 by bsakaguchi · 2 comments
Owner

Originally created by @morluto on GitHub (Oct 17, 2026).
Original GitHub issue: https://github.com/morluto/rea/issues/4667

Before submitting

  • Searched open and closed lazy client close-race reports.
  • Removed credentials and sensitive target data.

Area

Provider or capability discovery

Observed behavior

A previously loaded LazyAnalysisProvider dispatches an operation after its owner has begun close. Compiled main e567033a71 returned success and called the recording child twice (warm health plus the pending operation), although close had already been requested.

Reproduction

Executed against an isolated clean compiled checkout. Create a LazyAnalysisProvider with a recording client; parse the running Node executable as the local BinaryTarget; await one health operation to load the child; synchronously start a second execute and call close before awaiting that execute. Observed JSON: {"ok":true,"operations":2,"closes":1}. No native engine was launched.

Expected behavior

Once close gates admission, a caller resuming from asynchronous client acquisition must not dispatch new work through that retired client. Existing active provider work remains the child's cleanup responsibility.

Impact and scope

Observed provider-neutral adapter admission race with a recording child. Actual native failures, user frequency and public CLI/MCP reproduction: NOT_ENOUGH_INFO; not measured.

REA version

6.5.0 source, e567033a71.

Runtime and platform

Node24.18.0, Darwin arm64; compiled production LazyAnalysisProvider with recording provider ports.

Target and artifact context

Running Node executable parsed read-only; no target execution or mutation.

Interface

Other: shared provider adapter boundary

Earliest failure stage

Operation dispatch after asynchronous client acquisition

Relevant output or evidence

LazyAnalysisProvider.ts: execute awaits load/waitForAbortable, then calls loaded.execute without checking owner admission again. close sets #closed; that flag is checked before loading, leaving the await handoff unguarded. The new deferred-owner regression also failed before its dispatch fence: returned ok:true instead of client_closed.

Typed outcome or recovery expected

An operation-attributed ProviderAdapterError with client_closed; retain the exact owned client through final cleanup and retry.

Analysis or suspected cause

Source-derived: the producing owner checks admission before acquisition but its consumer dispatches after an await without checking the current owner state. Draft fix reads the owner's current ready-client identity immediately before dispatch; ten deferred-owner controls pass, including independent peers, late handoff and exact cleanup retry. Unpublished at report time.

Additional context

Related #4561 migration reuses this generic deferred owner. No real-provider support or cleanup claim from the recording ports.

Originally created by @morluto on GitHub (Oct 17, 2026). Original GitHub issue: https://github.com/morluto/rea/issues/4667 ### Before submitting - [x] Searched open and closed lazy client close-race reports. - [x] Removed credentials and sensitive target data. ### Area Provider or capability discovery ### Observed behavior A previously loaded LazyAnalysisProvider dispatches an operation after its owner has begun close. Compiled main e567033a7137d5eebf49147ed325cad141cff5c2 returned success and called the recording child twice (warm health plus the pending operation), although close had already been requested. ### Reproduction Executed against an isolated clean compiled checkout. Create a LazyAnalysisProvider with a recording client; parse the running Node executable as the local BinaryTarget; await one health operation to load the child; synchronously start a second execute and call close before awaiting that execute. Observed JSON: `{"ok":true,"operations":2,"closes":1}`. No native engine was launched. ### Expected behavior Once close gates admission, a caller resuming from asynchronous client acquisition must not dispatch new work through that retired client. Existing active provider work remains the child's cleanup responsibility. ### Impact and scope Observed provider-neutral adapter admission race with a recording child. Actual native failures, user frequency and public CLI/MCP reproduction: NOT_ENOUGH_INFO; not measured. ### REA version 6.5.0 source, e567033a7137d5eebf49147ed325cad141cff5c2. ### Runtime and platform Node24.18.0, Darwin arm64; compiled production LazyAnalysisProvider with recording provider ports. ### Target and artifact context Running Node executable parsed read-only; no target execution or mutation. ### Interface Other: shared provider adapter boundary ### Earliest failure stage Operation dispatch after asynchronous client acquisition ### Relevant output or evidence `LazyAnalysisProvider.ts`: execute awaits load/waitForAbortable, then calls loaded.execute without checking owner admission again. close sets #closed; that flag is checked before loading, leaving the await handoff unguarded. The new deferred-owner regression also failed before its dispatch fence: returned ok:true instead of client_closed. ### Typed outcome or recovery expected An operation-attributed ProviderAdapterError with client_closed; retain the exact owned client through final cleanup and retry. ### Analysis or suspected cause Source-derived: the producing owner checks admission before acquisition but its consumer dispatches after an await without checking the current owner state. Draft fix reads the owner's current ready-client identity immediately before dispatch; ten deferred-owner controls pass, including independent peers, late handoff and exact cleanup retry. Unpublished at report time. ### Additional context Related #4561 migration reuses this generic deferred owner. No real-provider support or cleanup claim from the recording ports.
Author
Owner

@morluto commented on GitHub (Oct 17, 2026):

Published on main in fb146bf40b.

Deferred creation has a closeable owner before work starts. Dispatch checks the owner's current ready-client identity immediately after acquisition. The superseded lazy implementation is removed; independent waiters, late acquisition, final cleanup and exact failed-owner retry use the shared owner. Native health and local reuse admission remain separate.

Compiled production LazyAnalysisProvider with a parsed running executable and recording child: baseline e567033a7 returned success with 2 dispatched operations after close was requested; candidate returns ProviderAdapterError/client_closed with 1 dispatched operation. No native engine launched. Executed 34 focused owner/lazy/composite/session controls, including the new dispatch race; check:fast and the combined405-test run passed.

Two alternating warm completed-task repeats per build, plus separate first processes: elapsed 0.120/0.123s, CPU 0.123/0.128s, sampled process-family RSS 74.1/75.4MB, raw reply tokens 23/28 (cl100k_base). This is a correctness repair; no performance improvement claim. True cold, whole-agent and public transport costs unmeasured. Recording ports do not prove real-provider behavior.

<!-- gh-comment-id:6229060543 --> @morluto commented on GitHub (Oct 17, 2026): Published on main in fb146bf40b78e69fdc80d0fc9fff6eb14946f7d0. Deferred creation has a closeable owner before work starts. Dispatch checks the owner's current ready-client identity immediately after acquisition. The superseded lazy implementation is removed; independent waiters, late acquisition, final cleanup and exact failed-owner retry use the shared owner. Native health and local reuse admission remain separate. Compiled production LazyAnalysisProvider with a parsed running executable and recording child: baseline e567033a7 returned success with 2 dispatched operations after close was requested; candidate returns ProviderAdapterError/client_closed with 1 dispatched operation. No native engine launched. Executed 34 focused owner/lazy/composite/session controls, including the new dispatch race; check:fast and the combined405-test run passed. Two alternating warm completed-task repeats per build, plus separate first processes: elapsed 0.120/0.123s, CPU 0.123/0.128s, sampled process-family RSS 74.1/75.4MB, raw reply tokens 23/28 (cl100k_base). This is a correctness repair; no performance improvement claim. True cold, whole-agent and public transport costs unmeasured. Recording ports do not prove real-provider behavior.
Author
Owner

@morluto commented on GitHub (Oct 17, 2026):

Revalidated at main 19329e43b: all 16 deferred/lazy client checks passed across DeferredAnalysisClient.test.ts and LazyAnalysisProvider.test.ts. Fix fb146bf40 is an ancestor of current main. Source still fences dispatch by the exact owner's ready-client identity after asynchronous acquisition; close gates that owner before joining creation and cleanup.

Closing as fixed. Recording provider ports cover the admission race and cleanup controls; no native engine or target execution was exercised.

<!-- gh-comment-id:6242148108 --> @morluto commented on GitHub (Oct 17, 2026): Revalidated at main 19329e43b: all 16 deferred/lazy client checks passed across DeferredAnalysisClient.test.ts and LazyAnalysisProvider.test.ts. Fix fb146bf40 is an ancestor of current main. Source still fences dispatch by the exact owner's ready-client identity after asynchronous acquisition; close gates that owner before joining creation and cleanup. Closing as fixed. Recording provider ports cover the admission race and cleanup controls; no native engine or target execution was exercised.
Sign in to join this conversation.
No milestone
No project
No assignees
1 participant
Notifications
Due date
The due date is invalid or out of range. Please use the format "yyyy-mm-dd".

No due date set.

Dependencies

No dependencies set.

Reference
morluto/rea#2433
No description provided.