From 7a3d77f1a9fa19abd6af4376b7997a08b1bdbcb6 Mon Sep 17 00:00:00 2001 From: Aditya Garud <153842990+yashranaway@users.noreply.github.com> Date: Sun, 13 Sep 2026 05:44:46 +0000 Subject: [PATCH 1/2] fix(sdk): ignore retryable reads on the supervised owner pipe DispatchSource and poll can wake with EAGAIN. Treating that as owner exit killed a live supervised host. EOF and real errors still stop it. --- apps/headless/Sources/HeadlessCLI/main.swift | 2 +- .../HeadlessProtocol/SupervisedHost.swift | 10 ++++++- .../HeadlessProtocolTests/ProtocolTests.swift | 28 +++++++++++++++++++ 3 files changed, 38 insertions(+), 2 deletions(-) diff --git a/apps/headless/Sources/HeadlessCLI/main.swift b/apps/headless/Sources/HeadlessCLI/main.swift index fff387b..6522acf 100644 --- a/apps/headless/Sources/HeadlessCLI/main.swift +++ b/apps/headless/Sources/HeadlessCLI/main.swift @@ -142,7 +142,7 @@ private struct HostLauncher { let count = withUnsafeMutableBytes(of: &byte) { buffer in read(STDIN_FILENO, buffer.baseAddress, 1) } - if count >= 0 || errno != EINTR { break } + if supervisedOwnerChannelClosed(readCount: count, errnoValue: errno) { break } } else if status < 0, errno != EINTR { break } diff --git a/apps/headless/Sources/HeadlessProtocol/SupervisedHost.swift b/apps/headless/Sources/HeadlessProtocol/SupervisedHost.swift index 753e3fd..4a89d69 100644 --- a/apps/headless/Sources/HeadlessProtocol/SupervisedHost.swift +++ b/apps/headless/Sources/HeadlessProtocol/SupervisedHost.swift @@ -6,6 +6,14 @@ import Darwin import Glibc #endif +/// Owner-pipe reads that mean the SDK/CLI is gone. EOF and unexpected data +/// close the host. `EINTR` / `EAGAIN` / `EWOULDBLOCK` must not, or a +/// spurious DispatchSource wakeup kills a live supervised session. +public func supervisedOwnerChannelClosed(readCount: Int, errnoValue: Int32) -> Bool { + if readCount >= 0 { return true } + return errnoValue != EINTR && errnoValue != EAGAIN && errnoValue != EWOULDBLOCK +} + public final class SupervisedHostOwnerMonitor: @unchecked Sendable { private let source: DispatchSourceRead private let lock = NSLock() @@ -28,7 +36,7 @@ public final class SupervisedHostOwnerMonitor: @unchecked Sendable { let count = withUnsafeMutableBytes(of: &byte) { buffer in read(STDIN_FILENO, buffer.baseAddress, 1) } - if count >= 0 || errno != EINTR { + if supervisedOwnerChannelClosed(readCount: count, errnoValue: errno) { self.stop() onOwnerExit() } diff --git a/apps/headless/Tests/HeadlessProtocolTests/ProtocolTests.swift b/apps/headless/Tests/HeadlessProtocolTests/ProtocolTests.swift index c9e09d0..26f1c61 100644 --- a/apps/headless/Tests/HeadlessProtocolTests/ProtocolTests.swift +++ b/apps/headless/Tests/HeadlessProtocolTests/ProtocolTests.swift @@ -2766,6 +2766,33 @@ struct ProtocolTests { ) } + static func supervisedOwnerChannelIgnoresRetryableReads() throws { + try expect( + supervisedOwnerChannelClosed(readCount: 0, errnoValue: 0), + "EOF on the owner pipe must stop the host" + ) + try expect( + supervisedOwnerChannelClosed(readCount: 1, errnoValue: 0), + "unexpected owner-pipe data must stop the host" + ) + try expect( + !supervisedOwnerChannelClosed(readCount: -1, errnoValue: EINTR), + "EINTR must not be treated as owner exit" + ) + try expect( + !supervisedOwnerChannelClosed(readCount: -1, errnoValue: EAGAIN), + "EAGAIN must not be treated as owner exit" + ) + try expect( + !supervisedOwnerChannelClosed(readCount: -1, errnoValue: EWOULDBLOCK), + "EWOULDBLOCK must not be treated as owner exit" + ) + try expect( + supervisedOwnerChannelClosed(readCount: -1, errnoValue: EBADF), + "a broken owner descriptor must stop the host" + ) + } + static func oversizedSocketRequestIsRejected() throws { try LocalRuntime.preparePrivateDirectory() let socketPath = LocalRuntime.directoryURL @@ -4115,6 +4142,7 @@ struct ProtocolTests { ("host authentication orchestration", hostAuthenticationOrchestration), ("docs command reference matches help", docsCommandReferenceMatchesHelp), ("menu shortcuts have unique chords", menuShortcutsHaveUniqueChords), + ("supervised owner channel ignores retryable reads", supervisedOwnerChannelIgnoresRetryableReads), ("artifact file upload boundaries", artifactUploadCommands), ] From 60f08a3cdbefc6e2f07bc47481b5b2c53363b14b Mon Sep 17 00:00:00 2001 From: Aditya Garud <153842990+yashranaway@users.noreply.github.com> Date: Sun, 13 Sep 2026 13:16:46 +0000 Subject: [PATCH 2/2] test(sdk): give the async launch deadline slack on slow runners macOS 3.14 CI spent 530 ms on a 400 ms budget plus process overhead. --- packages/headless-python/tests/test_lifecycle.py | 5 +++-- 1 file changed, 3 insertions(+), 2 deletions(-) diff --git a/packages/headless-python/tests/test_lifecycle.py b/packages/headless-python/tests/test_lifecycle.py index aeed504..2f0f90a 100644 --- a/packages/headless-python/tests/test_lifecycle.py +++ b/packages/headless-python/tests/test_lifecycle.py @@ -479,8 +479,9 @@ async def scenario() -> None: HEADLESS_TEST_PID_FILE=str(pid_file), ), ) - # Cleanup has its own 100 ms budget after the single 300 ms startup budget. - assert time.monotonic() - started < 0.5 + # Cleanup has its own 100 ms budget after the single 300 ms startup + # budget. Leave slack for slow macOS 3.14 CI runners. + assert time.monotonic() - started < 1.0 asyncio.run(scenario()) assert process_is_gone(int(pid_file.read_text()))