Skip to content
Closed
Show file tree
Hide file tree
Changes from all commits
Commits
Show all changes
51 commits
Select commit Hold shift + click to select a range
5be8761
test: reproduce Claude close teardown leak
austinywang Aug 7, 2026
85db658
fix: unblock Claude teardown when tabs close
austinywang Aug 7, 2026
6163126
Merge remote-tracking branch 'origin/main' into issue-9573-claude-pro…
austinywang Aug 7, 2026
5d21acb
test: reproduce stranded terminal close teardown
austinywang Aug 7, 2026
cebfba4
fix: isolate blocked terminal close teardowns
austinywang Aug 7, 2026
ef9c634
fix: respect Claude SessionEnd budget overrides
austinywang Aug 7, 2026
aedc1f7
fix: clear inherited Swift warning regressions
austinywang Aug 7, 2026
0e4bb1c
ci: canonicalize app-host config evidence paths
austinywang Aug 7, 2026
f998d9d
Merge remote-tracking branch 'origin/main' into issue-9573-claude-pro…
austinywang Aug 7, 2026
b1cd4d8
test: cover teardown beyond blocked close slots
austinywang Aug 7, 2026
aee1f97
fix: start process teardown before free admission
austinywang Aug 7, 2026
2de6814
Merge remote-tracking branch 'origin/main' into issue-9573-claude-pro…
austinywang Aug 7, 2026
b962644
test: cover source-aware network command fixtures
austinywang Aug 7, 2026
e50e33d
fix: make network fixture masking source-aware
austinywang Aug 7, 2026
db465d9
build: pin GhosttyKit for bounded teardown
austinywang Aug 7, 2026
16b499f
revert: drop incomplete determinism lexer change
austinywang Aug 7, 2026
586a39b
fix: reap process-group descendants after child exit
austinywang Aug 7, 2026
46fa5c0
build: pin GhosttyKit for descendant teardown
austinywang Aug 7, 2026
35dbb8c
test: cover teardown racing native surface reads
austinywang Aug 7, 2026
3e05822
test: unblock native access regression execution
austinywang Aug 7, 2026
c9b543f
fix: gate native reads before surface teardown
austinywang Aug 7, 2026
be48e85
build: pin GhosttyKit for stable process groups
austinywang Aug 7, 2026
d31933e
Merge remote-tracking branch 'origin/main' into issue-9573-claude-pro…
austinywang Aug 7, 2026
6677811
refactor: tighten native access gate ownership
austinywang Aug 7, 2026
f4e112f
refactor: make native access gates runtime-owned
austinywang Aug 7, 2026
8fc0ece
test: cover one-shot native teardown admission
austinywang Aug 7, 2026
230a8e8
build: pin stable process-group GhosttyKit
austinywang Aug 7, 2026
db12524
fix: expose native borrow release within package
austinywang Aug 7, 2026
4db0fa3
fix: gate sharing conformance for Xcode 16
austinywang Aug 7, 2026
4f4b389
fix: keep native reads off teardown actor
austinywang Aug 7, 2026
c54d9d5
build: pin foreground process-group GhosttyKit
austinywang Aug 8, 2026
476f272
Merge branch 'main' of https://github.com/manaflow-ai/cmux into issue…
austinywang Aug 8, 2026
0810cc8
Merge remote-tracking branch 'origin/main' into issue-9573-claude-pro…
austinywang Aug 8, 2026
22a37de
Merge branch 'main' of https://github.com/manaflow-ai/cmux into issue…
austinywang Aug 9, 2026
1d2334a
Update Ghostty to main-integrated process reaping
austinywang Aug 9, 2026
bd266e2
Pin merged GhosttyKit archive
austinywang Aug 9, 2026
a348c70
Merge branch 'main' of https://github.com/manaflow-ai/cmux into issue…
austinywang Aug 9, 2026
52b914b
test: reproduce concurrent screen-tail admission
austinywang Aug 9, 2026
35b87c0
fix: serialize native screen-tail reads
austinywang Aug 9, 2026
c3d4e12
Update Ghostty process-group reuse safety
austinywang Aug 9, 2026
cb95bbb
test: keep queued reads from delaying teardown
austinywang Aug 9, 2026
0c6f524
fix: acquire native borrow after read admission
austinywang Aug 9, 2026
01720c7
build: pin process-group safety GhosttyKit
austinywang Aug 9, 2026
742d82e
ci: restore app-host validation from main
austinywang Aug 9, 2026
3fff7e4
Merge origin/main into issue-9573-claude-process-leak
austinywang Aug 9, 2026
81911f4
Merge origin/main into issue-9573-claude-process-leak
austinywang Aug 10, 2026
eace2af
Merge origin/main into issue-9573-claude-process-leak
austinywang Aug 10, 2026
deee27d
fix: pair terminal native teardown operations
austinywang Aug 10, 2026
75d6a0c
Merge remote-tracking branch 'origin/main' into issue-9573-claude-pro…
austinywang Aug 10, 2026
9ac0ef5
test: cover native read cleanup and hook timeout
austinywang Aug 10, 2026
f1335e6
test: count every native text free invocation
austinywang Aug 10, 2026
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
Original file line number Diff line number Diff line change
@@ -0,0 +1,45 @@
internal import CmuxFoundationAtomicsC

/// A macOS 14-compatible atomic raw pointer that does not own its pointee.
///
/// C11 owns every storage access, so the wrapper is safe to send between
/// isolation domains. Callers remain responsible for retaining any object
/// represented by the pointer until a successful exchange removes it.
public final class AtomicRawPointerValue: @unchecked Sendable {
// The storage address never changes, and all pointee access occurs through
// the C11 atomic API rather than overlapping Swift `inout` accesses.
nonisolated(unsafe) private let storage:
UnsafeMutablePointer<CmuxAtomicRawPointerStorage>

/// Creates an atomic pointer value.
///
/// - Parameter initialValue: The unowned pointer returned until replaced.
public init(_ initialValue: UnsafeRawPointer? = nil) {
storage = .allocate(capacity: 1)
CmuxAtomicRawPointerInitialize(storage, initialValue)
}

deinit {
storage.deallocate()
}

/// Returns the current unowned pointer with acquire ordering.
@inline(__always)
public func loadAcquire() -> UnsafeRawPointer? {
CmuxAtomicRawPointerLoadAcquire(storage)
}

/// Atomically replaces `expected` with `desired` using acquire-release ordering.
///
/// - Parameters:
/// - expected: The pointer that must still be stored for replacement to occur.
/// - desired: The unowned replacement pointer.
/// - Returns: `true` when the replacement occurred, otherwise `false`.
@inline(__always)
public func compareExchange(
expected: UnsafeRawPointer?,
desired: UnsafeRawPointer?
) -> Bool {
CmuxAtomicRawPointerCompareExchange(storage, expected, desired)
}
}
Original file line number Diff line number Diff line change
Expand Up @@ -28,6 +28,12 @@ public final class AtomicUInt64Value: @unchecked Sendable {
CmuxAtomicUInt64LoadRelaxed(storage)
}

/// Returns the current value with acquire memory ordering.
@inline(__always)
public func loadAcquire() -> UInt64 {
CmuxAtomicUInt64LoadAcquire(storage)
}

/// Replaces the current value with relaxed memory ordering.
///
/// - Parameter value: The value subsequent loads should observe.
Expand All @@ -36,6 +42,17 @@ public final class AtomicUInt64Value: @unchecked Sendable {
CmuxAtomicUInt64StoreRelaxed(storage, value)
}

/// Atomically replaces `expected` with `desired` using acquire-release ordering.
///
/// - Parameters:
/// - expected: The value that must still be stored for the replacement to occur.
/// - desired: The replacement value.
/// - Returns: `true` when the replacement occurred, otherwise `false`.
@inline(__always)
public func compareExchange(expected: UInt64, desired: UInt64) -> Bool {
CmuxAtomicUInt64CompareExchange(storage, expected, desired)
}

/// Atomically increments the value with wrapping UInt64 arithmetic.
///
/// - Returns: The value after the increment.
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -37,10 +37,27 @@ uint64_t CmuxAtomicUInt64LoadRelaxed(const CmuxAtomicUInt64Storage *storage) {
return atomic_load_explicit(&storage->value, memory_order_relaxed);
}

uint64_t CmuxAtomicUInt64LoadAcquire(const CmuxAtomicUInt64Storage *storage) {
return atomic_load_explicit(&storage->value, memory_order_acquire);
}

void CmuxAtomicUInt64StoreRelaxed(CmuxAtomicUInt64Storage *storage, uint64_t value) {
atomic_store_explicit(&storage->value, value, memory_order_relaxed);
}

bool CmuxAtomicUInt64CompareExchange(
CmuxAtomicUInt64Storage *storage,
uint64_t expected,
uint64_t desired
) {
return atomic_compare_exchange_strong_explicit(
&storage->value,
&expected,
desired,
memory_order_acq_rel,
memory_order_acquire);
}

uint64_t CmuxAtomicUInt64IncrementRelaxed(CmuxAtomicUInt64Storage *storage) {
return atomic_fetch_add_explicit(&storage->value, 1, memory_order_relaxed) + 1;
}
Expand Down Expand Up @@ -93,3 +110,30 @@ bool CmuxAtomicUInt64DecrementIfPositive(CmuxAtomicUInt64Storage *storage) {
}
return false;
}

void CmuxAtomicRawPointerInitialize(
CmuxAtomicRawPointerStorage *storage,
const void *initialValue
) {
atomic_init(&storage->value, (uintptr_t)initialValue);
}

const void *CmuxAtomicRawPointerLoadAcquire(
const CmuxAtomicRawPointerStorage *storage
) {
return (const void *)atomic_load_explicit(&storage->value, memory_order_acquire);
}

bool CmuxAtomicRawPointerCompareExchange(
CmuxAtomicRawPointerStorage *storage,
const void *expected,
const void *desired
) {
uintptr_t expectedBits = (uintptr_t)expected;
return atomic_compare_exchange_strong_explicit(
&storage->value,
&expectedBits,
(uintptr_t)desired,
memory_order_acq_rel,
memory_order_acquire);
}
Original file line number Diff line number Diff line change
Expand Up @@ -25,7 +25,13 @@ typedef struct {

void CmuxAtomicUInt64Initialize(CmuxAtomicUInt64Storage *storage, uint64_t initialValue);
uint64_t CmuxAtomicUInt64LoadRelaxed(const CmuxAtomicUInt64Storage *storage);
uint64_t CmuxAtomicUInt64LoadAcquire(const CmuxAtomicUInt64Storage *storage);
void CmuxAtomicUInt64StoreRelaxed(CmuxAtomicUInt64Storage *storage, uint64_t value);
bool CmuxAtomicUInt64CompareExchange(
CmuxAtomicUInt64Storage *storage,
uint64_t expected,
uint64_t desired
);
uint64_t CmuxAtomicUInt64IncrementRelaxed(CmuxAtomicUInt64Storage *storage);
uint64_t CmuxAtomicUInt64AdvanceRelaxed(CmuxAtomicUInt64Storage *storage);
bool CmuxAtomicUInt64IncrementIfBelow(
Expand All @@ -34,4 +40,21 @@ bool CmuxAtomicUInt64IncrementIfBelow(
);
bool CmuxAtomicUInt64DecrementIfPositive(CmuxAtomicUInt64Storage *storage);

typedef struct {
_Atomic(uintptr_t) value;
} CmuxAtomicRawPointerStorage;

void CmuxAtomicRawPointerInitialize(
CmuxAtomicRawPointerStorage *storage,
const void *initialValue
);
const void *CmuxAtomicRawPointerLoadAcquire(
const CmuxAtomicRawPointerStorage *storage
);
bool CmuxAtomicRawPointerCompareExchange(
CmuxAtomicRawPointerStorage *storage,
const void *expected,
const void *desired
);

#endif
Original file line number Diff line number Diff line change
@@ -0,0 +1,22 @@
import Testing
@testable import CmuxFoundation

@Suite
struct AtomicRawPointerValueTests {
@Test func compareExchangeOnlyReplacesTheExpectedPointer() {
let first = UnsafeMutableRawPointer.allocate(byteCount: 1, alignment: 1)
let second = UnsafeMutableRawPointer.allocate(byteCount: 1, alignment: 1)
defer {
first.deallocate()
second.deallocate()
}
let value = AtomicRawPointerValue()

#expect(value.loadAcquire() == nil)
#expect(value.compareExchange(expected: nil, desired: first))
#expect(value.loadAcquire() == UnsafeRawPointer(first))
#expect(!value.compareExchange(expected: nil, desired: second))
#expect(value.compareExchange(expected: first, desired: second))
#expect(value.loadAcquire() == UnsafeRawPointer(second))
}
}
Original file line number Diff line number Diff line change
Expand Up @@ -7,12 +7,22 @@ struct AtomicUInt64ValueTests {
let value = AtomicUInt64Value(41)

#expect(value.loadRelaxed() == 41)
#expect(value.loadAcquire() == 41)
value.storeRelaxed(7)
#expect(value.loadRelaxed() == 7)
#expect(value.wrappingIncrementRelaxed() == 8)
#expect(value.loadRelaxed() == 8)
}

@Test func compareExchangeOnlyReplacesTheExpectedValue() {
let value = AtomicUInt64Value(7)

#expect(!value.compareExchange(expected: 6, desired: 8))
#expect(value.loadAcquire() == 7)
#expect(value.compareExchange(expected: 7, desired: 8))
#expect(value.loadAcquire() == 8)
}

@Test func concurrentIncrementsAreNotLost() async {
let value = AtomicUInt64Value()
await withTaskGroup(of: Void.self) { group in
Expand Down
Original file line number Diff line number Diff line change
@@ -0,0 +1,22 @@
internal import CmuxFoundation

/// A one-shot borrow that keeps one native runtime generation accessible.
final class TerminalSurfaceRuntimeNativeAccessBorrow: Sendable {
private let gate: TerminalSurfaceRuntimeNativeAccessGate
private let isActive = AtomicBooleanGate(true)

init(gate: TerminalSurfaceRuntimeNativeAccessGate) {
self.gate = gate
}

func release() {
guard isActive.compareExchange(expected: true, desired: false) else {
return
}
gate.releaseBorrow()
}

deinit {
release()
}
}
Original file line number Diff line number Diff line change
@@ -0,0 +1,118 @@
internal import CmuxFoundation

/// Orders native surface borrows before the teardown of one runtime generation.
///
/// The high state bit permanently closes borrow admission. Lower bits count
/// active borrows. A retained one-shot teardown action is published before the
/// close transition, so either the closer or the final borrower can claim and
/// run it synchronously without a task hop.
final class TerminalSurfaceRuntimeNativeAccessGate: Sendable {
private static let teardownRequestedMask: UInt64 = 1 << 63
private static let borrowCountMask = teardownRequestedMask - 1

private let state = AtomicUInt64Value()
private let pendingTeardownAction = AtomicRawPointerValue()

/// Acquires a borrow unless teardown has already claimed this generation.
func acquireBorrow() -> TerminalSurfaceRuntimeNativeAccessBorrow? {
while true {
let current = state.loadAcquire()
guard current & Self.teardownRequestedMask == 0,
current != Self.borrowCountMask else {
return nil
}
guard state.compareExchange(
expected: current,
desired: current + 1
) else {
continue
}
return TerminalSurfaceRuntimeNativeAccessBorrow(gate: self)
}
}

/// Closes admission and starts the first teardown after admitted borrows finish.
///
/// Later teardown requests are ignored because one native runtime generation
/// has exactly one terminal teardown transition.
func requestTeardown(start: @escaping @Sendable () -> Void) {
let retainedAction = Unmanaged.passRetained(
TerminalSurfaceRuntimeTeardownAction(start: start)
)
let actionPointer = UnsafeRawPointer(retainedAction.toOpaque())
guard pendingTeardownAction.compareExchange(
expected: nil,
desired: actionPointer
) else {
retainedAction.release()
return
}

while true {
let current = state.loadAcquire()
guard current & Self.teardownRequestedMask == 0 else {
discardPendingAction(actionPointer: actionPointer)
return
}
let closed = current | Self.teardownRequestedMask
guard state.compareExchange(expected: current, desired: closed) else {
continue
}
if closed & Self.borrowCountMask == 0 {
runPendingTeardown()
}
return
}
}

/// Releases one admitted borrow and starts any newly-unblocked teardown.
func releaseBorrow() {
while true {
let current = state.loadAcquire()
let borrowCount = current & Self.borrowCountMask
guard borrowCount > 0 else { return }
let released = current - 1
guard state.compareExchange(expected: current, desired: released) else {
continue
}
if released == Self.teardownRequestedMask {
runPendingTeardown()
}
return
}
}

private func runPendingTeardown() {
while true {
guard let actionPointer = pendingTeardownAction.loadAcquire() else {
return
}
guard pendingTeardownAction.compareExchange(
expected: actionPointer,
desired: nil
) else {
continue
}
takeRetainedAction(actionPointer: actionPointer).run()
return
}
}

private func discardPendingAction(actionPointer: UnsafeRawPointer) {
guard pendingTeardownAction.compareExchange(
expected: actionPointer,
desired: nil
) else {
return
}
_ = takeRetainedAction(actionPointer: actionPointer)
}

private func takeRetainedAction(
actionPointer: UnsafeRawPointer
) -> TerminalSurfaceRuntimeTeardownAction {
return Unmanaged<TerminalSurfaceRuntimeTeardownAction>
.fromOpaque(actionPointer)
.takeRetainedValue()
}
}
Original file line number Diff line number Diff line change
@@ -0,0 +1,19 @@
internal import GhosttyKit

/// Couples native process shutdown with the matching final surface free.
///
/// A runtime generation owns both operations as one value so a custom free
/// cannot accidentally invoke Ghostty's termination API on a foreign pointer.
struct TerminalSurfaceRuntimeNativeTeardown: Sendable {
let beginSurfaceTeardown: @Sendable (ghostty_surface_t) -> Void
let freeSurface: @Sendable (ghostty_surface_t) -> Void

static let ghostty = TerminalSurfaceRuntimeNativeTeardown(
beginSurfaceTeardown: { surface in
ghostty_surface_request_process_termination(surface)
},
freeSurface: { surface in
ghostty_surface_free(surface)
}
)
}
Original file line number Diff line number Diff line change
@@ -0,0 +1,16 @@
/// Serializes bounded screen-tail formatting across the terminal runtime.
///
/// Requests may wait here without borrowing their surface, so one blocked
/// formatter cannot defer another surface's process teardown. After admission,
/// the reader atomically acquires that runtime generation before dereferencing
/// its pointer. Cancelled or already-closing requests never enter Ghostty.
actor TerminalSurfaceRuntimeScreenTailReader {
func read(_ request: TerminalSurfaceRuntimeScreenTailRequest) -> String? {
guard !Task.isCancelled,
let borrow = request.nativeAccessGate.acquireBorrow() else {
return nil
}
defer { borrow.release() }
return request.read()
}
Comment thread
cursor[bot] marked this conversation as resolved.
}
Loading
Loading