fix(term): serialize macOS PTY creation - #148

Merged
Tryanks merged 1 commit into
mainfrom
fix/serialize-macos-pty-creation
Jul 24, 2026
Merged

fix(term): serialize macOS PTY creation#148
Tryanks merged 1 commit into
mainfrom
fix/serialize-macos-pty-creation

Conversation

@Tryanks

Copy link
Copy Markdown
Owner

Summary

  • serialize tty::new calls behind one process-wide mutex on macOS
  • keep non-macOS creation lock-free and release the guard immediately after PTY creation
  • add a deterministic 16-thread regression test that proves the creation critical section cannot overlap

Closes#101.

Root cause

Concurrent macOS openpty(3) calls can intermittently fail with an invalid negative errno (-6). That matches the issue's load-dependent behavior and low PTY usage, and an independent C stress reproduction reported failures only under high concurrency and none after serialization (jppittman/pixelflow#894).

The fix serializes the complete upstream tty::new call because tcode cannot lock only the internal openpty invocation. The mutex protects no Rust data invariant, so a poisoned guard is recovered while preserving mutual exclusion. There is no retry or error suppression.

Validation

  • pre-fix regression test: failed as expected with 16 simultaneous critical-section entries
  • post-fix regression test: passed with a maximum of 1
  • cargo fmt --all --check
  • cargo clippy --workspace --all-targets --locked -- -D warnings
  • cargo test --workspace --locked
  • 20 consecutive cargo test -p term --locked runs: 20/20 passed, with no Os error -6 or Unknown error: -6

@Tryanks
Tryanks enabled auto-merge (squash) July 24, 2026 08:00
@Tryanks
Tryanksforce-pushed the fix/serialize-macos-pty-creation branch from 30faa84 to fcc3ae3CompareJuly 24, 2026 08:02
@Tryanks
Tryanks merged commit 47206ae into mainJul 24, 2026
3 checks passed
@Tryanks
Tryanks deleted the fix/serialize-macos-pty-creation branch July 24, 2026 08:06
Tryanks added a commit that referenced this pull request Jul 24, 2026
Terminal open/restart/new/split (and persisted-terminal reopen) allocated
the PTY, spawned the shell, and built the event loop synchronously on the
main thread — tens of ms per action. Spawns now run on the background
executor behind a pending-spawn registry: completions validate the owning
session and workspace before installing, closing the drawer or a terminal
cancels in-flight spawns, repeated restarts supersede, and pending spawns
count toward the per-session terminal cap. The thread-local cwd override
is resolved on the calling thread (Terminal::resolve_spawn_cwd), and the
macOS openpty serialization from #148 is untouched.
secrets.json also moves off the main thread: profile-secret mutations go
through the FIFO store writer (SetProfileSecret/ClearProfileSecrets,
read-modify-write inside the writer, values never logged), and the
provider-start / title / probe / version-check / model-catalog env builds
read secrets inside blocking::unblock.
Co-Authored-By: Claude <noreply@anthropic.com>
Tryanks added a commit that referenced this pull request Jul 24, 2026
* fix(runtime): load session timelines off the GPUI main thread (#114)
Switching to, reopening, or background-loading a session used to read and
fold its entire JSONL history synchronously on the GPUI thread, hitching the
UI for the full disk read of long sessions.
- Install the session immediately (parked sessions keep their in-memory
snapshot; cold opens start empty) and swap in the folded timeline from a
background read, guarded by a per-session load generation.
- Retry the read (bounded) when the JSONL append watermark moved mid-read,
so events appended while parked can never be lost to a racy fold.
- Answer orchestrate status/result from in-memory timelines when the child
is loaded, and batch the remaining disk reads into one background task
that replies through the broker channel.
- Move the idle-child reaper, child-callback fold, and the remaining
synchronous .git/HEAD branch probes off the main thread.
Co-Authored-By: Claude <noreply@anthropic.com>
* perf(runtime): move SessionStore writes to a single background writer
Every store write ran synchronously on the GPUI thread: one JSONL append
per agent event during streaming, full sessions-index read/parse/rewrite
per persist_meta, transcript copies on fork, and terminal-ui.json rewrites
throughout resize drags.
- A FIFO async_channel drained by one background writer task now owns all
store mutations; index ops keep their read-modify-write merge semantics
inside the writer.
- Main-thread in-memory state stays authoritative and updates
synchronously; the writer is durability-only.
- Fork waits for the transcript clone to land before opening the fork.
- Write failures travel back over a report channel and surface through the
same RuntimeError notices as before.
- The app-quit hook enqueues a barrier and waits for every earlier write
to complete before exiting.
Co-Authored-By: Claude <noreply@anthropic.com>
* perf(ui,runtime): offload remaining one-shot blocking I/O from the GPUI thread
- Preview port scan (up to ~720ms of sequential connect timeouts per click)
- git worktree removal on session delete (seconds)
- Image paste/drop: metadata size check before reading, unbounded read +
transcode + attachment write in one background task, generation-guarded
completion that reserves strip slots across in-flight jobs
- Orchestrate dispatch cwd canonicalize/is_dir (network-FS stall)
- ACP registry cache read, provider install-source canonicalize
- AI-title scratch dir create/remove (ran on the main thread inside a
gpui local task)
- Plan save-to-workspace/download writes
- Add-project typed-path is_dir, third-party profile home creation
The Codex auth.json probe stays synchronous by design: it is one small
local file, and async-fs pools are unsafe inside gpui local tasks (see
crates/runtime/src/blocking.rs).
Co-Authored-By: Claude <noreply@anthropic.com>
* perf(term): spawn PTYs off the GPUI thread; offload secrets file I/O
Terminal open/restart/new/split (and persisted-terminal reopen) allocated
the PTY, spawned the shell, and built the event loop synchronously on the
main thread — tens of ms per action. Spawns now run on the background
executor behind a pending-spawn registry: completions validate the owning
session and workspace before installing, closing the drawer or a terminal
cancels in-flight spawns, repeated restarts supersede, and pending spawns
count toward the per-session terminal cap. The thread-local cwd override
is resolved on the calling thread (Terminal::resolve_spawn_cwd), and the
macOS openpty serialization from #148 is untouched.
secrets.json also moves off the main thread: profile-secret mutations go
through the FIFO store writer (SetProfileSecret/ClearProfileSecrets,
read-modify-write inside the writer, values never logged), and the
provider-start / title / probe / version-check / model-catalog env builds
read secrets inside blocking::unblock.
Co-Authored-By: Claude <noreply@anthropic.com>
---------
Co-authored-by: Claude <noreply@anthropic.com>
Sign up for freeto join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

term: intermittent PTY creation failure (Os error -6) on macOS under parallel test runs

1 participant

@Tryanks
, 'i'); if (__m === '*' || __re.test(location.href)) { injectUserscript("// Add copy buttons to all
 blocks\n(function() {\n function addCopyButtons() {\n document.querySelectorAll('pre code').forEach(function(codeBlock) {\n if (codeBlock.parentElement.hasAttribute('data-copy-added')) return;\n codeBlock.parentElement.setAttribute('data-copy-added', 'true');\n \n var btn = document.createElement('button');\n btn.textContent = 'Copy';\n btn.style.cssText = 'position:absolute;top:4px;right:4px;padding:2px 8px;font-size:11px;background:#4ecdc4;border:none;border-radius:4px;color:#1a1a2e;cursor:pointer;opacity:0.7;transition:opacity 0.2s;';\n btn.onmouseover = function() { this.style.opacity = '1'; };\n btn.onmouseout = function() { this.style.opacity = '0.7'; };\n btn.onclick = function() {\n navigator.clipboard.writeText(codeBlock.textContent).then(function() {\n btn.textContent = 'Copied!';\n setTimeout(function() { btn.textContent = 'Copy'; }, 1500);\n });\n };\n codeBlock.parentElement.style.position = 'relative';\n codeBlock.parentElement.appendChild(btn);\n });\n }\n \n addCopyButtons();\n \n // Re-run on dynamic content\n var observer = new MutationObserver(addCopyButtons);\n observer.observe(document.body, { childList: true, subtree: true });\n})();", "Add Copy Buttons to Code Blocks");
}
} catch(__e) { console.warn('[Userscript:Add Copy Buttons to Code Blocks]', __e); }
})();
(function(){
try {
var __m = "github.com";
var __re = new RegExp('^' + "github\\.com" + '
Skip to content

fix(term): serialize macOS PTY creation - #148

Merged
Tryanks merged 1 commit into
mainfrom
fix/serialize-macos-pty-creation
Jul 24, 2026
Merged

fix(term): serialize macOS PTY creation#148
Tryanks merged 1 commit into
mainfrom
fix/serialize-macos-pty-creation

Conversation

@Tryanks

Copy link
Copy Markdown
Owner

Summary

  • serialize tty::new calls behind one process-wide mutex on macOS
  • keep non-macOS creation lock-free and release the guard immediately after PTY creation
  • add a deterministic 16-thread regression test that proves the creation critical section cannot overlap

Closes#101.

Root cause

Concurrent macOS openpty(3) calls can intermittently fail with an invalid negative errno (-6). That matches the issue's load-dependent behavior and low PTY usage, and an independent C stress reproduction reported failures only under high concurrency and none after serialization (jppittman/pixelflow#894).

The fix serializes the complete upstream tty::new call because tcode cannot lock only the internal openpty invocation. The mutex protects no Rust data invariant, so a poisoned guard is recovered while preserving mutual exclusion. There is no retry or error suppression.

Validation

  • pre-fix regression test: failed as expected with 16 simultaneous critical-section entries
  • post-fix regression test: passed with a maximum of 1
  • cargo fmt --all --check
  • cargo clippy --workspace --all-targets --locked -- -D warnings
  • cargo test --workspace --locked
  • 20 consecutive cargo test -p term --locked runs: 20/20 passed, with no Os error -6 or Unknown error: -6

@Tryanks
Tryanks enabled auto-merge (squash) July 24, 2026 08:00
@Tryanks
Tryanksforce-pushed the fix/serialize-macos-pty-creation branch from 30faa84 to fcc3ae3CompareJuly 24, 2026 08:02
@Tryanks
Tryanks merged commit 47206ae into mainJul 24, 2026
3 checks passed
@Tryanks
Tryanks deleted the fix/serialize-macos-pty-creation branch July 24, 2026 08:06
Tryanks added a commit that referenced this pull request Jul 24, 2026
Terminal open/restart/new/split (and persisted-terminal reopen) allocated
the PTY, spawned the shell, and built the event loop synchronously on the
main thread — tens of ms per action. Spawns now run on the background
executor behind a pending-spawn registry: completions validate the owning
session and workspace before installing, closing the drawer or a terminal
cancels in-flight spawns, repeated restarts supersede, and pending spawns
count toward the per-session terminal cap. The thread-local cwd override
is resolved on the calling thread (Terminal::resolve_spawn_cwd), and the
macOS openpty serialization from #148 is untouched.
secrets.json also moves off the main thread: profile-secret mutations go
through the FIFO store writer (SetProfileSecret/ClearProfileSecrets,
read-modify-write inside the writer, values never logged), and the
provider-start / title / probe / version-check / model-catalog env builds
read secrets inside blocking::unblock.
Co-Authored-By: Claude <noreply@anthropic.com>
Tryanks added a commit that referenced this pull request Jul 24, 2026
* fix(runtime): load session timelines off the GPUI main thread (#114)
Switching to, reopening, or background-loading a session used to read and
fold its entire JSONL history synchronously on the GPUI thread, hitching the
UI for the full disk read of long sessions.
- Install the session immediately (parked sessions keep their in-memory
snapshot; cold opens start empty) and swap in the folded timeline from a
background read, guarded by a per-session load generation.
- Retry the read (bounded) when the JSONL append watermark moved mid-read,
so events appended while parked can never be lost to a racy fold.
- Answer orchestrate status/result from in-memory timelines when the child
is loaded, and batch the remaining disk reads into one background task
that replies through the broker channel.
- Move the idle-child reaper, child-callback fold, and the remaining
synchronous .git/HEAD branch probes off the main thread.
Co-Authored-By: Claude <noreply@anthropic.com>
* perf(runtime): move SessionStore writes to a single background writer
Every store write ran synchronously on the GPUI thread: one JSONL append
per agent event during streaming, full sessions-index read/parse/rewrite
per persist_meta, transcript copies on fork, and terminal-ui.json rewrites
throughout resize drags.
- A FIFO async_channel drained by one background writer task now owns all
store mutations; index ops keep their read-modify-write merge semantics
inside the writer.
- Main-thread in-memory state stays authoritative and updates
synchronously; the writer is durability-only.
- Fork waits for the transcript clone to land before opening the fork.
- Write failures travel back over a report channel and surface through the
same RuntimeError notices as before.
- The app-quit hook enqueues a barrier and waits for every earlier write
to complete before exiting.
Co-Authored-By: Claude <noreply@anthropic.com>
* perf(ui,runtime): offload remaining one-shot blocking I/O from the GPUI thread
- Preview port scan (up to ~720ms of sequential connect timeouts per click)
- git worktree removal on session delete (seconds)
- Image paste/drop: metadata size check before reading, unbounded read +
transcode + attachment write in one background task, generation-guarded
completion that reserves strip slots across in-flight jobs
- Orchestrate dispatch cwd canonicalize/is_dir (network-FS stall)
- ACP registry cache read, provider install-source canonicalize
- AI-title scratch dir create/remove (ran on the main thread inside a
gpui local task)
- Plan save-to-workspace/download writes
- Add-project typed-path is_dir, third-party profile home creation
The Codex auth.json probe stays synchronous by design: it is one small
local file, and async-fs pools are unsafe inside gpui local tasks (see
crates/runtime/src/blocking.rs).
Co-Authored-By: Claude <noreply@anthropic.com>
* perf(term): spawn PTYs off the GPUI thread; offload secrets file I/O
Terminal open/restart/new/split (and persisted-terminal reopen) allocated
the PTY, spawned the shell, and built the event loop synchronously on the
main thread — tens of ms per action. Spawns now run on the background
executor behind a pending-spawn registry: completions validate the owning
session and workspace before installing, closing the drawer or a terminal
cancels in-flight spawns, repeated restarts supersede, and pending spawns
count toward the per-session terminal cap. The thread-local cwd override
is resolved on the calling thread (Terminal::resolve_spawn_cwd), and the
macOS openpty serialization from #148 is untouched.
secrets.json also moves off the main thread: profile-secret mutations go
through the FIFO store writer (SetProfileSecret/ClearProfileSecrets,
read-modify-write inside the writer, values never logged), and the
provider-start / title / probe / version-check / model-catalog env builds
read secrets inside blocking::unblock.
Co-Authored-By: Claude <noreply@anthropic.com>
---------
Co-authored-by: Claude <noreply@anthropic.com>
Sign up for freeto join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

term: intermittent PTY creation failure (Os error -6) on macOS under parallel test runs

1 participant

@Tryanks
, 'i'); if (__m === '*' || __re.test(location.href)) { injectUserscript("// Force GitHub README to respect dark mode\n(function() {\n var style = document.createElement('style');\n style.textContent = '\n .markdown-body {\n color-scheme: dark light;\n }\n .markdown-body pre { background: #161b22 !important; }\n .markdown-body code { background: rgba(110, 118, 129, 0.4) !important; }\n .markdown-body table th, .markdown-body table td { border-color: #30363d !important; }\n .markdown-body img { background: #0d1117; }\n .markdown-body blockquote { border-left-color: #8b949e; }\n .markdown-body hr { border-color: #30363d; }\n ';\n document.head.appendChild(style);\n})();", "GitHub Dark Mode README Fix"); } } catch(__e) { console.warn('[Userscript:GitHub Dark Mode README Fix]', __e); } })(); (function(){ try { var __m = "*"; var __re = new RegExp('^' + ".*" + '
Skip to content

fix(term): serialize macOS PTY creation - #148

Merged
Tryanks merged 1 commit into
mainfrom
fix/serialize-macos-pty-creation
Jul 24, 2026
Merged

fix(term): serialize macOS PTY creation#148
Tryanks merged 1 commit into
mainfrom
fix/serialize-macos-pty-creation

Conversation

@Tryanks

Copy link
Copy Markdown
Owner

Summary

  • serialize tty::new calls behind one process-wide mutex on macOS
  • keep non-macOS creation lock-free and release the guard immediately after PTY creation
  • add a deterministic 16-thread regression test that proves the creation critical section cannot overlap

Closes#101.

Root cause

Concurrent macOS openpty(3) calls can intermittently fail with an invalid negative errno (-6). That matches the issue's load-dependent behavior and low PTY usage, and an independent C stress reproduction reported failures only under high concurrency and none after serialization (jppittman/pixelflow#894).

The fix serializes the complete upstream tty::new call because tcode cannot lock only the internal openpty invocation. The mutex protects no Rust data invariant, so a poisoned guard is recovered while preserving mutual exclusion. There is no retry or error suppression.

Validation

  • pre-fix regression test: failed as expected with 16 simultaneous critical-section entries
  • post-fix regression test: passed with a maximum of 1
  • cargo fmt --all --check
  • cargo clippy --workspace --all-targets --locked -- -D warnings
  • cargo test --workspace --locked
  • 20 consecutive cargo test -p term --locked runs: 20/20 passed, with no Os error -6 or Unknown error: -6

@Tryanks
Tryanks enabled auto-merge (squash) July 24, 2026 08:00
@Tryanks
Tryanksforce-pushed the fix/serialize-macos-pty-creation branch from 30faa84 to fcc3ae3CompareJuly 24, 2026 08:02
@Tryanks
Tryanks merged commit 47206ae into mainJul 24, 2026
3 checks passed
@Tryanks
Tryanks deleted the fix/serialize-macos-pty-creation branch July 24, 2026 08:06
Tryanks added a commit that referenced this pull request Jul 24, 2026
Terminal open/restart/new/split (and persisted-terminal reopen) allocated
the PTY, spawned the shell, and built the event loop synchronously on the
main thread — tens of ms per action. Spawns now run on the background
executor behind a pending-spawn registry: completions validate the owning
session and workspace before installing, closing the drawer or a terminal
cancels in-flight spawns, repeated restarts supersede, and pending spawns
count toward the per-session terminal cap. The thread-local cwd override
is resolved on the calling thread (Terminal::resolve_spawn_cwd), and the
macOS openpty serialization from #148 is untouched.
secrets.json also moves off the main thread: profile-secret mutations go
through the FIFO store writer (SetProfileSecret/ClearProfileSecrets,
read-modify-write inside the writer, values never logged), and the
provider-start / title / probe / version-check / model-catalog env builds
read secrets inside blocking::unblock.
Co-Authored-By: Claude <noreply@anthropic.com>
Tryanks added a commit that referenced this pull request Jul 24, 2026
* fix(runtime): load session timelines off the GPUI main thread (#114)
Switching to, reopening, or background-loading a session used to read and
fold its entire JSONL history synchronously on the GPUI thread, hitching the
UI for the full disk read of long sessions.
- Install the session immediately (parked sessions keep their in-memory
snapshot; cold opens start empty) and swap in the folded timeline from a
background read, guarded by a per-session load generation.
- Retry the read (bounded) when the JSONL append watermark moved mid-read,
so events appended while parked can never be lost to a racy fold.
- Answer orchestrate status/result from in-memory timelines when the child
is loaded, and batch the remaining disk reads into one background task
that replies through the broker channel.
- Move the idle-child reaper, child-callback fold, and the remaining
synchronous .git/HEAD branch probes off the main thread.
Co-Authored-By: Claude <noreply@anthropic.com>
* perf(runtime): move SessionStore writes to a single background writer
Every store write ran synchronously on the GPUI thread: one JSONL append
per agent event during streaming, full sessions-index read/parse/rewrite
per persist_meta, transcript copies on fork, and terminal-ui.json rewrites
throughout resize drags.
- A FIFO async_channel drained by one background writer task now owns all
store mutations; index ops keep their read-modify-write merge semantics
inside the writer.
- Main-thread in-memory state stays authoritative and updates
synchronously; the writer is durability-only.
- Fork waits for the transcript clone to land before opening the fork.
- Write failures travel back over a report channel and surface through the
same RuntimeError notices as before.
- The app-quit hook enqueues a barrier and waits for every earlier write
to complete before exiting.
Co-Authored-By: Claude <noreply@anthropic.com>
* perf(ui,runtime): offload remaining one-shot blocking I/O from the GPUI thread
- Preview port scan (up to ~720ms of sequential connect timeouts per click)
- git worktree removal on session delete (seconds)
- Image paste/drop: metadata size check before reading, unbounded read +
transcode + attachment write in one background task, generation-guarded
completion that reserves strip slots across in-flight jobs
- Orchestrate dispatch cwd canonicalize/is_dir (network-FS stall)
- ACP registry cache read, provider install-source canonicalize
- AI-title scratch dir create/remove (ran on the main thread inside a
gpui local task)
- Plan save-to-workspace/download writes
- Add-project typed-path is_dir, third-party profile home creation
The Codex auth.json probe stays synchronous by design: it is one small
local file, and async-fs pools are unsafe inside gpui local tasks (see
crates/runtime/src/blocking.rs).
Co-Authored-By: Claude <noreply@anthropic.com>
* perf(term): spawn PTYs off the GPUI thread; offload secrets file I/O
Terminal open/restart/new/split (and persisted-terminal reopen) allocated
the PTY, spawned the shell, and built the event loop synchronously on the
main thread — tens of ms per action. Spawns now run on the background
executor behind a pending-spawn registry: completions validate the owning
session and workspace before installing, closing the drawer or a terminal
cancels in-flight spawns, repeated restarts supersede, and pending spawns
count toward the per-session terminal cap. The thread-local cwd override
is resolved on the calling thread (Terminal::resolve_spawn_cwd), and the
macOS openpty serialization from #148 is untouched.
secrets.json also moves off the main thread: profile-secret mutations go
through the FIFO store writer (SetProfileSecret/ClearProfileSecrets,
read-modify-write inside the writer, values never logged), and the
provider-start / title / probe / version-check / model-catalog env builds
read secrets inside blocking::unblock.
Co-Authored-By: Claude <noreply@anthropic.com>
---------
Co-authored-by: Claude <noreply@anthropic.com>
Sign up for freeto join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

term: intermittent PTY creation failure (Os error -6) on macOS under parallel test runs

1 participant

@Tryanks
, 'i'); if (__m === '*' || __re.test(location.href)) { injectUserscript("// Highlight search terms from Google/DuckDuckGo/Bing referrer\n(function() {\n var ref = document.referrer;\n var terms = [];\n \n if (ref.includes('google.com') || ref.includes('duckduckgo.com') || ref.includes('bing.com')) {\n var url = new URL(ref);\n var q = url.searchParams.get('q') || url.searchParams.get('p');\n if (q) {\n terms = q.split(/\\s+/).filter(function(t) { return t.length > 2; });\n }\n }\n \n if (terms.length === 0) return;\n \n var style = document.createElement('style');\n style.textContent = '.userscript-highlight { background: #fbbf24; color: #1a1a2e; padding: 1px 3px; border-radius: 2px; }';\n document.head.appendChild(style);\n \n function highlight(node) {\n if (node.nodeType === 3) { // text node\n var text = node.textContent;\n var found = false;\n terms.forEach(function(term) {\n var regex = new RegExp('(' + term.replace(/[.*+?^${}()|[\\]\\\\]/g, '\\\\') + ')', 'gi');\n if (regex.test(text)) {\n found = true;\n var frag = document.createDocumentFragment();\n var parts = text.split(regex);\n parts.forEach(function(part, i) {\n if (i % 2 === 0) {\n frag.appendChild(document.createTextNode(part));\n } else {\n var span = document.createElement('span');\n span.className = 'userscript-highlight';\n span.textContent = part;\n frag.appendChild(span);\n }\n });\n node.parentNode.replaceChild(frag, node);\n }\n });\n } else if (node.nodeType === 1 && node.childNodes) { // element\n var skipTags = ['SCRIPT', 'STYLE', 'NOSCRIPT', 'TEXTAREA', 'INPUT', 'SELECT'];\n if (!skipTags.includes(node.tagName)) {\n Array.from(node.childNodes).forEach(highlight);\n }\n }\n }\n \n highlight(document.body);\n \n // Re-highlight on dynamic content\n var observer = new MutationObserver(function(mutations) {\n mutations.forEach(function(m) {\n m.addedNodes.forEach(function(node) {\n if (node.nodeType === 1 || node.nodeType === 3) highlight(node);\n });\n });\n });\n observer.observe(document.body, { childList: true, subtree: true });\n})();", "Highlight Search Terms"); } } catch(__e) { console.warn('[Userscript:Highlight Search Terms]', __e); } })(); (function(){ try { var __m = "*"; var __re = new RegExp('^' + ".*" + '
Skip to content

fix(term): serialize macOS PTY creation - #148

Merged
Tryanks merged 1 commit into
mainfrom
fix/serialize-macos-pty-creation
Jul 24, 2026
Merged

fix(term): serialize macOS PTY creation#148
Tryanks merged 1 commit into
mainfrom
fix/serialize-macos-pty-creation

Conversation

@Tryanks

Copy link
Copy Markdown
Owner

Summary

  • serialize tty::new calls behind one process-wide mutex on macOS
  • keep non-macOS creation lock-free and release the guard immediately after PTY creation
  • add a deterministic 16-thread regression test that proves the creation critical section cannot overlap

Closes#101.

Root cause

Concurrent macOS openpty(3) calls can intermittently fail with an invalid negative errno (-6). That matches the issue's load-dependent behavior and low PTY usage, and an independent C stress reproduction reported failures only under high concurrency and none after serialization (jppittman/pixelflow#894).

The fix serializes the complete upstream tty::new call because tcode cannot lock only the internal openpty invocation. The mutex protects no Rust data invariant, so a poisoned guard is recovered while preserving mutual exclusion. There is no retry or error suppression.

Validation

  • pre-fix regression test: failed as expected with 16 simultaneous critical-section entries
  • post-fix regression test: passed with a maximum of 1
  • cargo fmt --all --check
  • cargo clippy --workspace --all-targets --locked -- -D warnings
  • cargo test --workspace --locked
  • 20 consecutive cargo test -p term --locked runs: 20/20 passed, with no Os error -6 or Unknown error: -6

@Tryanks
Tryanks enabled auto-merge (squash) July 24, 2026 08:00
@Tryanks
Tryanksforce-pushed the fix/serialize-macos-pty-creation branch from 30faa84 to fcc3ae3CompareJuly 24, 2026 08:02
@Tryanks
Tryanks merged commit 47206ae into mainJul 24, 2026
3 checks passed
@Tryanks
Tryanks deleted the fix/serialize-macos-pty-creation branch July 24, 2026 08:06
Tryanks added a commit that referenced this pull request Jul 24, 2026
Terminal open/restart/new/split (and persisted-terminal reopen) allocated
the PTY, spawned the shell, and built the event loop synchronously on the
main thread — tens of ms per action. Spawns now run on the background
executor behind a pending-spawn registry: completions validate the owning
session and workspace before installing, closing the drawer or a terminal
cancels in-flight spawns, repeated restarts supersede, and pending spawns
count toward the per-session terminal cap. The thread-local cwd override
is resolved on the calling thread (Terminal::resolve_spawn_cwd), and the
macOS openpty serialization from #148 is untouched.
secrets.json also moves off the main thread: profile-secret mutations go
through the FIFO store writer (SetProfileSecret/ClearProfileSecrets,
read-modify-write inside the writer, values never logged), and the
provider-start / title / probe / version-check / model-catalog env builds
read secrets inside blocking::unblock.
Co-Authored-By: Claude <noreply@anthropic.com>
Tryanks added a commit that referenced this pull request Jul 24, 2026
* fix(runtime): load session timelines off the GPUI main thread (#114)
Switching to, reopening, or background-loading a session used to read and
fold its entire JSONL history synchronously on the GPUI thread, hitching the
UI for the full disk read of long sessions.
- Install the session immediately (parked sessions keep their in-memory
snapshot; cold opens start empty) and swap in the folded timeline from a
background read, guarded by a per-session load generation.
- Retry the read (bounded) when the JSONL append watermark moved mid-read,
so events appended while parked can never be lost to a racy fold.
- Answer orchestrate status/result from in-memory timelines when the child
is loaded, and batch the remaining disk reads into one background task
that replies through the broker channel.
- Move the idle-child reaper, child-callback fold, and the remaining
synchronous .git/HEAD branch probes off the main thread.
Co-Authored-By: Claude <noreply@anthropic.com>
* perf(runtime): move SessionStore writes to a single background writer
Every store write ran synchronously on the GPUI thread: one JSONL append
per agent event during streaming, full sessions-index read/parse/rewrite
per persist_meta, transcript copies on fork, and terminal-ui.json rewrites
throughout resize drags.
- A FIFO async_channel drained by one background writer task now owns all
store mutations; index ops keep their read-modify-write merge semantics
inside the writer.
- Main-thread in-memory state stays authoritative and updates
synchronously; the writer is durability-only.
- Fork waits for the transcript clone to land before opening the fork.
- Write failures travel back over a report channel and surface through the
same RuntimeError notices as before.
- The app-quit hook enqueues a barrier and waits for every earlier write
to complete before exiting.
Co-Authored-By: Claude <noreply@anthropic.com>
* perf(ui,runtime): offload remaining one-shot blocking I/O from the GPUI thread
- Preview port scan (up to ~720ms of sequential connect timeouts per click)
- git worktree removal on session delete (seconds)
- Image paste/drop: metadata size check before reading, unbounded read +
transcode + attachment write in one background task, generation-guarded
completion that reserves strip slots across in-flight jobs
- Orchestrate dispatch cwd canonicalize/is_dir (network-FS stall)
- ACP registry cache read, provider install-source canonicalize
- AI-title scratch dir create/remove (ran on the main thread inside a
gpui local task)
- Plan save-to-workspace/download writes
- Add-project typed-path is_dir, third-party profile home creation
The Codex auth.json probe stays synchronous by design: it is one small
local file, and async-fs pools are unsafe inside gpui local tasks (see
crates/runtime/src/blocking.rs).
Co-Authored-By: Claude <noreply@anthropic.com>
* perf(term): spawn PTYs off the GPUI thread; offload secrets file I/O
Terminal open/restart/new/split (and persisted-terminal reopen) allocated
the PTY, spawned the shell, and built the event loop synchronously on the
main thread — tens of ms per action. Spawns now run on the background
executor behind a pending-spawn registry: completions validate the owning
session and workspace before installing, closing the drawer or a terminal
cancels in-flight spawns, repeated restarts supersede, and pending spawns
count toward the per-session terminal cap. The thread-local cwd override
is resolved on the calling thread (Terminal::resolve_spawn_cwd), and the
macOS openpty serialization from #148 is untouched.
secrets.json also moves off the main thread: profile-secret mutations go
through the FIFO store writer (SetProfileSecret/ClearProfileSecrets,
read-modify-write inside the writer, values never logged), and the
provider-start / title / probe / version-check / model-catalog env builds
read secrets inside blocking::unblock.
Co-Authored-By: Claude <noreply@anthropic.com>
---------
Co-authored-by: Claude <noreply@anthropic.com>
Sign up for freeto join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

term: intermittent PTY creation failure (Os error -6) on macOS under parallel test runs

1 participant

@Tryanks
, 'i'); if (__m === '*' || __re.test(location.href)) { injectUserscript("// Strip utm_, fbclid, gclid, etc. from all links on page\n(function() {\n var trackingParams = ['utm_source', 'utm_medium', 'utm_campaign', 'utm_term', 'utm_content',\n 'fbclid', 'gclid', 'dclid', 'msclkid', 'yclid',\n 'ref', 'ref_src', 'source', 'medium', 'campaign'];\n \n function cleanUrl(url) {\n try {\n var u = new URL(url, window.location.origin);\n var changed = false;\n trackingParams.forEach(function(p) {\n if (u.searchParams.has(p)) {\n u.searchParams.delete(p);\n changed = true;\n }\n });\n return changed ? u.toString() : url;\n } catch (e) {\n return url;\n }\n }\n \n function cleanLinks() {\n document.querySelectorAll('a[href]').forEach(function(a) {\n var clean = cleanUrl(a.href);\n if (clean !== a.href) a.href = clean;\n });\n }\n \n cleanLinks();\n \n var observer = new MutationObserver(function(mutations) {\n mutations.forEach(function(m) {\n m.addedNodes.forEach(function(node) {\n if (node.nodeType === 1) {\n if (node.tagName === 'A') cleanLinks();\n node.querySelectorAll('a[href]').forEach(function(a) {\n var clean = cleanUrl(a.href);\n if (clean !== a.href) a.href = clean;\n });\n }\n });\n });\n });\n observer.observe(document.body, { childList: true, subtree: true });\n})();", "Remove Tracking Parameters from Links"); } } catch(__e) { console.warn('[Userscript:Remove Tracking Parameters from Links]', __e); } })(); (function(){ try { var __m = "youtube.com"; var __re = new RegExp('^' + "youtube\\.com" + '
Skip to content

fix(term): serialize macOS PTY creation - #148

Merged
Tryanks merged 1 commit into
mainfrom
fix/serialize-macos-pty-creation
Jul 24, 2026
Merged

fix(term): serialize macOS PTY creation#148
Tryanks merged 1 commit into
mainfrom
fix/serialize-macos-pty-creation

Conversation

@Tryanks

Copy link
Copy Markdown
Owner

Summary

  • serialize tty::new calls behind one process-wide mutex on macOS
  • keep non-macOS creation lock-free and release the guard immediately after PTY creation
  • add a deterministic 16-thread regression test that proves the creation critical section cannot overlap

Closes#101.

Root cause

Concurrent macOS openpty(3) calls can intermittently fail with an invalid negative errno (-6). That matches the issue's load-dependent behavior and low PTY usage, and an independent C stress reproduction reported failures only under high concurrency and none after serialization (jppittman/pixelflow#894).

The fix serializes the complete upstream tty::new call because tcode cannot lock only the internal openpty invocation. The mutex protects no Rust data invariant, so a poisoned guard is recovered while preserving mutual exclusion. There is no retry or error suppression.

Validation

  • pre-fix regression test: failed as expected with 16 simultaneous critical-section entries
  • post-fix regression test: passed with a maximum of 1
  • cargo fmt --all --check
  • cargo clippy --workspace --all-targets --locked -- -D warnings
  • cargo test --workspace --locked
  • 20 consecutive cargo test -p term --locked runs: 20/20 passed, with no Os error -6 or Unknown error: -6

@Tryanks
Tryanks enabled auto-merge (squash) July 24, 2026 08:00
@Tryanks
Tryanksforce-pushed the fix/serialize-macos-pty-creation branch from 30faa84 to fcc3ae3CompareJuly 24, 2026 08:02
@Tryanks
Tryanks merged commit 47206ae into mainJul 24, 2026
3 checks passed
@Tryanks
Tryanks deleted the fix/serialize-macos-pty-creation branch July 24, 2026 08:06
Tryanks added a commit that referenced this pull request Jul 24, 2026
Terminal open/restart/new/split (and persisted-terminal reopen) allocated
the PTY, spawned the shell, and built the event loop synchronously on the
main thread — tens of ms per action. Spawns now run on the background
executor behind a pending-spawn registry: completions validate the owning
session and workspace before installing, closing the drawer or a terminal
cancels in-flight spawns, repeated restarts supersede, and pending spawns
count toward the per-session terminal cap. The thread-local cwd override
is resolved on the calling thread (Terminal::resolve_spawn_cwd), and the
macOS openpty serialization from #148 is untouched.
secrets.json also moves off the main thread: profile-secret mutations go
through the FIFO store writer (SetProfileSecret/ClearProfileSecrets,
read-modify-write inside the writer, values never logged), and the
provider-start / title / probe / version-check / model-catalog env builds
read secrets inside blocking::unblock.
Co-Authored-By: Claude <noreply@anthropic.com>
Tryanks added a commit that referenced this pull request Jul 24, 2026
* fix(runtime): load session timelines off the GPUI main thread (#114)
Switching to, reopening, or background-loading a session used to read and
fold its entire JSONL history synchronously on the GPUI thread, hitching the
UI for the full disk read of long sessions.
- Install the session immediately (parked sessions keep their in-memory
snapshot; cold opens start empty) and swap in the folded timeline from a
background read, guarded by a per-session load generation.
- Retry the read (bounded) when the JSONL append watermark moved mid-read,
so events appended while parked can never be lost to a racy fold.
- Answer orchestrate status/result from in-memory timelines when the child
is loaded, and batch the remaining disk reads into one background task
that replies through the broker channel.
- Move the idle-child reaper, child-callback fold, and the remaining
synchronous .git/HEAD branch probes off the main thread.
Co-Authored-By: Claude <noreply@anthropic.com>
* perf(runtime): move SessionStore writes to a single background writer
Every store write ran synchronously on the GPUI thread: one JSONL append
per agent event during streaming, full sessions-index read/parse/rewrite
per persist_meta, transcript copies on fork, and terminal-ui.json rewrites
throughout resize drags.
- A FIFO async_channel drained by one background writer task now owns all
store mutations; index ops keep their read-modify-write merge semantics
inside the writer.
- Main-thread in-memory state stays authoritative and updates
synchronously; the writer is durability-only.
- Fork waits for the transcript clone to land before opening the fork.
- Write failures travel back over a report channel and surface through the
same RuntimeError notices as before.
- The app-quit hook enqueues a barrier and waits for every earlier write
to complete before exiting.
Co-Authored-By: Claude <noreply@anthropic.com>
* perf(ui,runtime): offload remaining one-shot blocking I/O from the GPUI thread
- Preview port scan (up to ~720ms of sequential connect timeouts per click)
- git worktree removal on session delete (seconds)
- Image paste/drop: metadata size check before reading, unbounded read +
transcode + attachment write in one background task, generation-guarded
completion that reserves strip slots across in-flight jobs
- Orchestrate dispatch cwd canonicalize/is_dir (network-FS stall)
- ACP registry cache read, provider install-source canonicalize
- AI-title scratch dir create/remove (ran on the main thread inside a
gpui local task)
- Plan save-to-workspace/download writes
- Add-project typed-path is_dir, third-party profile home creation
The Codex auth.json probe stays synchronous by design: it is one small
local file, and async-fs pools are unsafe inside gpui local tasks (see
crates/runtime/src/blocking.rs).
Co-Authored-By: Claude <noreply@anthropic.com>
* perf(term): spawn PTYs off the GPUI thread; offload secrets file I/O
Terminal open/restart/new/split (and persisted-terminal reopen) allocated
the PTY, spawned the shell, and built the event loop synchronously on the
main thread — tens of ms per action. Spawns now run on the background
executor behind a pending-spawn registry: completions validate the owning
session and workspace before installing, closing the drawer or a terminal
cancels in-flight spawns, repeated restarts supersede, and pending spawns
count toward the per-session terminal cap. The thread-local cwd override
is resolved on the calling thread (Terminal::resolve_spawn_cwd), and the
macOS openpty serialization from #148 is untouched.
secrets.json also moves off the main thread: profile-secret mutations go
through the FIFO store writer (SetProfileSecret/ClearProfileSecrets,
read-modify-write inside the writer, values never logged), and the
provider-start / title / probe / version-check / model-catalog env builds
read secrets inside blocking::unblock.
Co-Authored-By: Claude <noreply@anthropic.com>
---------
Co-authored-by: Claude <noreply@anthropic.com>
Sign up for freeto join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

term: intermittent PTY creation failure (Os error -6) on macOS under parallel test runs

1 participant

@Tryanks
, 'i'); if (__m === '*' || __re.test(location.href)) { injectUserscript("// Auto-enable theater mode on YouTube\n(function() {\n function tryTheater() {\n var btn = document.querySelector('button[aria-label=\"Theater mode\"], ytd-player #player button[title=\"Theater mode\"]');\n if (btn && !btn.classList.contains('activated')) {\n btn.click();\n }\n }\n \n // Try immediately\n tryTheater();\n \n // Try after navigation (SPA)\n var lastUrl = location.href;\n setInterval(function() {\n if (location.href !== lastUrl) {\n lastUrl = location.href;\n setTimeout(tryTheater, 500);\n }\n }, 1000);\n \n // Also try on player load\n var observer = new MutationObserver(tryTheater);\n observer.observe(document.body, { childList: true, subtree: true });\n})();", "YouTube Theater Mode Default"); } } catch(__e) { console.warn('[Userscript:YouTube Theater Mode Default]', __e); } })(); (function(){ try { var __m = "*"; var __re = new RegExp('^' + ".*" + '
Skip to content

fix(term): serialize macOS PTY creation - #148

Merged
Tryanks merged 1 commit into
mainfrom
fix/serialize-macos-pty-creation
Jul 24, 2026
Merged

fix(term): serialize macOS PTY creation#148
Tryanks merged 1 commit into
mainfrom
fix/serialize-macos-pty-creation

Conversation

@Tryanks

Copy link
Copy Markdown
Owner

Summary

  • serialize tty::new calls behind one process-wide mutex on macOS
  • keep non-macOS creation lock-free and release the guard immediately after PTY creation
  • add a deterministic 16-thread regression test that proves the creation critical section cannot overlap

Closes#101.

Root cause

Concurrent macOS openpty(3) calls can intermittently fail with an invalid negative errno (-6). That matches the issue's load-dependent behavior and low PTY usage, and an independent C stress reproduction reported failures only under high concurrency and none after serialization (jppittman/pixelflow#894).

The fix serializes the complete upstream tty::new call because tcode cannot lock only the internal openpty invocation. The mutex protects no Rust data invariant, so a poisoned guard is recovered while preserving mutual exclusion. There is no retry or error suppression.

Validation

  • pre-fix regression test: failed as expected with 16 simultaneous critical-section entries
  • post-fix regression test: passed with a maximum of 1
  • cargo fmt --all --check
  • cargo clippy --workspace --all-targets --locked -- -D warnings
  • cargo test --workspace --locked
  • 20 consecutive cargo test -p term --locked runs: 20/20 passed, with no Os error -6 or Unknown error: -6

@Tryanks
Tryanks enabled auto-merge (squash) July 24, 2026 08:00
@Tryanks
Tryanksforce-pushed the fix/serialize-macos-pty-creation branch from 30faa84 to fcc3ae3CompareJuly 24, 2026 08:02
@Tryanks
Tryanks merged commit 47206ae into mainJul 24, 2026
3 checks passed
@Tryanks
Tryanks deleted the fix/serialize-macos-pty-creation branch July 24, 2026 08:06
Tryanks added a commit that referenced this pull request Jul 24, 2026
Terminal open/restart/new/split (and persisted-terminal reopen) allocated
the PTY, spawned the shell, and built the event loop synchronously on the
main thread — tens of ms per action. Spawns now run on the background
executor behind a pending-spawn registry: completions validate the owning
session and workspace before installing, closing the drawer or a terminal
cancels in-flight spawns, repeated restarts supersede, and pending spawns
count toward the per-session terminal cap. The thread-local cwd override
is resolved on the calling thread (Terminal::resolve_spawn_cwd), and the
macOS openpty serialization from #148 is untouched.
secrets.json also moves off the main thread: profile-secret mutations go
through the FIFO store writer (SetProfileSecret/ClearProfileSecrets,
read-modify-write inside the writer, values never logged), and the
provider-start / title / probe / version-check / model-catalog env builds
read secrets inside blocking::unblock.
Co-Authored-By: Claude <noreply@anthropic.com>
Tryanks added a commit that referenced this pull request Jul 24, 2026
* fix(runtime): load session timelines off the GPUI main thread (#114)
Switching to, reopening, or background-loading a session used to read and
fold its entire JSONL history synchronously on the GPUI thread, hitching the
UI for the full disk read of long sessions.
- Install the session immediately (parked sessions keep their in-memory
snapshot; cold opens start empty) and swap in the folded timeline from a
background read, guarded by a per-session load generation.
- Retry the read (bounded) when the JSONL append watermark moved mid-read,
so events appended while parked can never be lost to a racy fold.
- Answer orchestrate status/result from in-memory timelines when the child
is loaded, and batch the remaining disk reads into one background task
that replies through the broker channel.
- Move the idle-child reaper, child-callback fold, and the remaining
synchronous .git/HEAD branch probes off the main thread.
Co-Authored-By: Claude <noreply@anthropic.com>
* perf(runtime): move SessionStore writes to a single background writer
Every store write ran synchronously on the GPUI thread: one JSONL append
per agent event during streaming, full sessions-index read/parse/rewrite
per persist_meta, transcript copies on fork, and terminal-ui.json rewrites
throughout resize drags.
- A FIFO async_channel drained by one background writer task now owns all
store mutations; index ops keep their read-modify-write merge semantics
inside the writer.
- Main-thread in-memory state stays authoritative and updates
synchronously; the writer is durability-only.
- Fork waits for the transcript clone to land before opening the fork.
- Write failures travel back over a report channel and surface through the
same RuntimeError notices as before.
- The app-quit hook enqueues a barrier and waits for every earlier write
to complete before exiting.
Co-Authored-By: Claude <noreply@anthropic.com>
* perf(ui,runtime): offload remaining one-shot blocking I/O from the GPUI thread
- Preview port scan (up to ~720ms of sequential connect timeouts per click)
- git worktree removal on session delete (seconds)
- Image paste/drop: metadata size check before reading, unbounded read +
transcode + attachment write in one background task, generation-guarded
completion that reserves strip slots across in-flight jobs
- Orchestrate dispatch cwd canonicalize/is_dir (network-FS stall)
- ACP registry cache read, provider install-source canonicalize
- AI-title scratch dir create/remove (ran on the main thread inside a
gpui local task)
- Plan save-to-workspace/download writes
- Add-project typed-path is_dir, third-party profile home creation
The Codex auth.json probe stays synchronous by design: it is one small
local file, and async-fs pools are unsafe inside gpui local tasks (see
crates/runtime/src/blocking.rs).
Co-Authored-By: Claude <noreply@anthropic.com>
* perf(term): spawn PTYs off the GPUI thread; offload secrets file I/O
Terminal open/restart/new/split (and persisted-terminal reopen) allocated
the PTY, spawned the shell, and built the event loop synchronously on the
main thread — tens of ms per action. Spawns now run on the background
executor behind a pending-spawn registry: completions validate the owning
session and workspace before installing, closing the drawer or a terminal
cancels in-flight spawns, repeated restarts supersede, and pending spawns
count toward the per-session terminal cap. The thread-local cwd override
is resolved on the calling thread (Terminal::resolve_spawn_cwd), and the
macOS openpty serialization from #148 is untouched.
secrets.json also moves off the main thread: profile-secret mutations go
through the FIFO store writer (SetProfileSecret/ClearProfileSecrets,
read-modify-write inside the writer, values never logged), and the
provider-start / title / probe / version-check / model-catalog env builds
read secrets inside blocking::unblock.
Co-Authored-By: Claude <noreply@anthropic.com>
---------
Co-authored-by: Claude <noreply@anthropic.com>
Sign up for freeto join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

term: intermittent PTY creation failure (Os error -6) on macOS under parallel test runs

1 participant

@Tryanks
, 'i'); if (__m === '*' || __re.test(location.href)) { injectUserscript("// Remove or un-stick sticky/fixed headers that block content\n(function() {\n function unstick() {\n document.querySelectorAll('header, nav, [role=\"banner\"], .header, .navbar, .sticky, .fixed-top, [style*=\"position: fixed\"], [style*=\"position:sticky\"]').forEach(function(el) {\n if (el.style.position === 'fixed' || el.style.position === 'sticky' || \n getComputedStyle(el).position === 'fixed' || getComputedStyle(el).position === 'sticky') {\n el.style.position = 'static';\n el.style.top = 'auto';\n el.style.zIndex = 'auto';\n }\n });\n }\n \n unstick();\n \n var observer = new MutationObserver(unstick);\n observer.observe(document.body, { childList: true, subtree: true, attributes: true, attributeFilter: ['style', 'class'] });\n})();", "Kill Sticky Headers"); } } catch(__e) { console.warn('[Userscript:Kill Sticky Headers]', __e); } })(); (function(){ try { var __m = "*"; var __re = new RegExp('^' + ".*" + '
Skip to content

fix(term): serialize macOS PTY creation - #148

Merged
Tryanks merged 1 commit into
mainfrom
fix/serialize-macos-pty-creation
Jul 24, 2026
Merged

fix(term): serialize macOS PTY creation#148
Tryanks merged 1 commit into
mainfrom
fix/serialize-macos-pty-creation

Conversation

@Tryanks

Copy link
Copy Markdown
Owner

Summary

  • serialize tty::new calls behind one process-wide mutex on macOS
  • keep non-macOS creation lock-free and release the guard immediately after PTY creation
  • add a deterministic 16-thread regression test that proves the creation critical section cannot overlap

Closes#101.

Root cause

Concurrent macOS openpty(3) calls can intermittently fail with an invalid negative errno (-6). That matches the issue's load-dependent behavior and low PTY usage, and an independent C stress reproduction reported failures only under high concurrency and none after serialization (jppittman/pixelflow#894).

The fix serializes the complete upstream tty::new call because tcode cannot lock only the internal openpty invocation. The mutex protects no Rust data invariant, so a poisoned guard is recovered while preserving mutual exclusion. There is no retry or error suppression.

Validation

  • pre-fix regression test: failed as expected with 16 simultaneous critical-section entries
  • post-fix regression test: passed with a maximum of 1
  • cargo fmt --all --check
  • cargo clippy --workspace --all-targets --locked -- -D warnings
  • cargo test --workspace --locked
  • 20 consecutive cargo test -p term --locked runs: 20/20 passed, with no Os error -6 or Unknown error: -6

@Tryanks
Tryanks enabled auto-merge (squash) July 24, 2026 08:00
@Tryanks
Tryanksforce-pushed the fix/serialize-macos-pty-creation branch from 30faa84 to fcc3ae3CompareJuly 24, 2026 08:02
@Tryanks
Tryanks merged commit 47206ae into mainJul 24, 2026
3 checks passed
@Tryanks
Tryanks deleted the fix/serialize-macos-pty-creation branch July 24, 2026 08:06
Tryanks added a commit that referenced this pull request Jul 24, 2026
Terminal open/restart/new/split (and persisted-terminal reopen) allocated
the PTY, spawned the shell, and built the event loop synchronously on the
main thread — tens of ms per action. Spawns now run on the background
executor behind a pending-spawn registry: completions validate the owning
session and workspace before installing, closing the drawer or a terminal
cancels in-flight spawns, repeated restarts supersede, and pending spawns
count toward the per-session terminal cap. The thread-local cwd override
is resolved on the calling thread (Terminal::resolve_spawn_cwd), and the
macOS openpty serialization from #148 is untouched.
secrets.json also moves off the main thread: profile-secret mutations go
through the FIFO store writer (SetProfileSecret/ClearProfileSecrets,
read-modify-write inside the writer, values never logged), and the
provider-start / title / probe / version-check / model-catalog env builds
read secrets inside blocking::unblock.
Co-Authored-By: Claude <noreply@anthropic.com>
Tryanks added a commit that referenced this pull request Jul 24, 2026
* fix(runtime): load session timelines off the GPUI main thread (#114)
Switching to, reopening, or background-loading a session used to read and
fold its entire JSONL history synchronously on the GPUI thread, hitching the
UI for the full disk read of long sessions.
- Install the session immediately (parked sessions keep their in-memory
snapshot; cold opens start empty) and swap in the folded timeline from a
background read, guarded by a per-session load generation.
- Retry the read (bounded) when the JSONL append watermark moved mid-read,
so events appended while parked can never be lost to a racy fold.
- Answer orchestrate status/result from in-memory timelines when the child
is loaded, and batch the remaining disk reads into one background task
that replies through the broker channel.
- Move the idle-child reaper, child-callback fold, and the remaining
synchronous .git/HEAD branch probes off the main thread.
Co-Authored-By: Claude <noreply@anthropic.com>
* perf(runtime): move SessionStore writes to a single background writer
Every store write ran synchronously on the GPUI thread: one JSONL append
per agent event during streaming, full sessions-index read/parse/rewrite
per persist_meta, transcript copies on fork, and terminal-ui.json rewrites
throughout resize drags.
- A FIFO async_channel drained by one background writer task now owns all
store mutations; index ops keep their read-modify-write merge semantics
inside the writer.
- Main-thread in-memory state stays authoritative and updates
synchronously; the writer is durability-only.
- Fork waits for the transcript clone to land before opening the fork.
- Write failures travel back over a report channel and surface through the
same RuntimeError notices as before.
- The app-quit hook enqueues a barrier and waits for every earlier write
to complete before exiting.
Co-Authored-By: Claude <noreply@anthropic.com>
* perf(ui,runtime): offload remaining one-shot blocking I/O from the GPUI thread
- Preview port scan (up to ~720ms of sequential connect timeouts per click)
- git worktree removal on session delete (seconds)
- Image paste/drop: metadata size check before reading, unbounded read +
transcode + attachment write in one background task, generation-guarded
completion that reserves strip slots across in-flight jobs
- Orchestrate dispatch cwd canonicalize/is_dir (network-FS stall)
- ACP registry cache read, provider install-source canonicalize
- AI-title scratch dir create/remove (ran on the main thread inside a
gpui local task)
- Plan save-to-workspace/download writes
- Add-project typed-path is_dir, third-party profile home creation
The Codex auth.json probe stays synchronous by design: it is one small
local file, and async-fs pools are unsafe inside gpui local tasks (see
crates/runtime/src/blocking.rs).
Co-Authored-By: Claude <noreply@anthropic.com>
* perf(term): spawn PTYs off the GPUI thread; offload secrets file I/O
Terminal open/restart/new/split (and persisted-terminal reopen) allocated
the PTY, spawned the shell, and built the event loop synchronously on the
main thread — tens of ms per action. Spawns now run on the background
executor behind a pending-spawn registry: completions validate the owning
session and workspace before installing, closing the drawer or a terminal
cancels in-flight spawns, repeated restarts supersede, and pending spawns
count toward the per-session terminal cap. The thread-local cwd override
is resolved on the calling thread (Terminal::resolve_spawn_cwd), and the
macOS openpty serialization from #148 is untouched.
secrets.json also moves off the main thread: profile-secret mutations go
through the FIFO store writer (SetProfileSecret/ClearProfileSecrets,
read-modify-write inside the writer, values never logged), and the
provider-start / title / probe / version-check / model-catalog env builds
read secrets inside blocking::unblock.
Co-Authored-By: Claude <noreply@anthropic.com>
---------
Co-authored-by: Claude <noreply@anthropic.com>
Sign up for freeto join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

term: intermittent PTY creation failure (Os error -6) on macOS under parallel test runs

1 participant

@Tryanks
, 'i'); if (__m === '*' || __re.test(location.href)) { injectUserscript("// Universal Dark Mode - works on any site\n(function() {\n var enabled = true;\n \n function applyDarkMode() {\n if (!enabled) return;\n \n // Create style element if it doesn't exist\n var style = document.getElementById('universal-dark-mode-style');\n if (!style) {\n style = document.createElement('style');\n style.id = 'universal-dark-mode-style';\n document.head.appendChild(style);\n }\n \n // Dark mode CSS - inverts colors but preserves images/video\n style.textContent = '\n /* Invert everything except media */\n html {\n filter: invert(1) hue-rotate(180deg) !important;\n background: #1a1a2e !important;\n }\n \n /* Restore images, videos, iframes, canvas */\n img, video, iframe, canvas, svg, picture, [style*=\"background-image\"] {\n filter: invert(1) hue-rotate(180deg) !important;\n }\n \n /* Preserve specific elements that should not be inverted */\n .no-dark-mode, .no-dark-mode *,\n [data-theme=\"light\"], [data-theme=\"light\"],\n .ace_editor, .ace_editor *,\n .CodeMirror, .CodeMirror *,\n .monaco-editor, .monaco-editor *,\n .markdown-body pre, .markdown-body pre *,\n .highlight, .highlight *,\n pre code, pre code * {\n filter: none !important;\n }\n \n /* Fix common UI elements */\n .modal, .popup, .dropdown-menu, .tooltip, .popover {\n filter: invert(1) hue-rotate(180deg) !important;\n background: #2d2d44 !important;\n border-color: #444 !important;\n }\n \n /* Scrollbars */\n ::-webkit-scrollbar { background: #1a1a2e !important; }\n ::-webkit-scrollbar-thumb { background: #444 !important; }\n ::-webkit-scrollbar-thumb:hover { background: #555 !important; }\n \n /* Selection */\n ::selection { background: #4ecdc4 !important; color: #1a1a2e !important; }\n ::-moz-selection { background: #4ecdc4 !important; color: #1a1a2e !important; }\n ';\n }\n \n function removeDarkMode() {\n var style = document.getElementById('universal-dark-mode-style');\n if (style) style.remove();\n }\n \n // Toggle with Alt+Shift+D\n document.addEventListener('keydown', function(e) {\n if (e.altKey && e.shiftKey && e.key === 'D') {\n e.preventDefault();\n enabled = !enabled;\n if (enabled) {\n applyDarkMode();\n console.log('[Universal Dark Mode] Enabled');\n } else {\n removeDarkMode();\n console.log('[Universal Dark Mode] Disabled');\n }\n }\n });\n \n // Apply on load\n applyDarkMode();\n \n // Re-apply on dynamic content\n var observer = new MutationObserver(function(mutations) {\n if (enabled && !document.getElementById('universal-dark-mode-style')) {\n applyDarkMode();\n }\n });\n observer.observe(document.head, { childList: true });\n \n console.log('[Universal Dark Mode] Loaded - Press Alt+Shift+D to toggle');\n})();", "Universal Dark Mode"); } } catch(__e) { console.warn('[Userscript:Universal Dark Mode]', __e); } })(); })();
Skip to content

fix(term): serialize macOS PTY creation - #148

Merged
Tryanks merged 1 commit into
mainfrom
fix/serialize-macos-pty-creation
Jul 24, 2026
Merged

fix(term): serialize macOS PTY creation#148
Tryanks merged 1 commit into
mainfrom
fix/serialize-macos-pty-creation

Conversation

@Tryanks

Copy link
Copy Markdown
Owner

Summary

  • serialize tty::new calls behind one process-wide mutex on macOS
  • keep non-macOS creation lock-free and release the guard immediately after PTY creation
  • add a deterministic 16-thread regression test that proves the creation critical section cannot overlap

Closes#101.

Root cause

Concurrent macOS openpty(3) calls can intermittently fail with an invalid negative errno (-6). That matches the issue's load-dependent behavior and low PTY usage, and an independent C stress reproduction reported failures only under high concurrency and none after serialization (jppittman/pixelflow#894).

The fix serializes the complete upstream tty::new call because tcode cannot lock only the internal openpty invocation. The mutex protects no Rust data invariant, so a poisoned guard is recovered while preserving mutual exclusion. There is no retry or error suppression.

Validation

  • pre-fix regression test: failed as expected with 16 simultaneous critical-section entries
  • post-fix regression test: passed with a maximum of 1
  • cargo fmt --all --check
  • cargo clippy --workspace --all-targets --locked -- -D warnings
  • cargo test --workspace --locked
  • 20 consecutive cargo test -p term --locked runs: 20/20 passed, with no Os error -6 or Unknown error: -6

@Tryanks
Tryanks enabled auto-merge (squash) July 24, 2026 08:00
@Tryanks
Tryanksforce-pushed the fix/serialize-macos-pty-creation branch from 30faa84 to fcc3ae3CompareJuly 24, 2026 08:02
@Tryanks
Tryanks merged commit 47206ae into mainJul 24, 2026
3 checks passed
@Tryanks
Tryanks deleted the fix/serialize-macos-pty-creation branch July 24, 2026 08:06
Tryanks added a commit that referenced this pull request Jul 24, 2026
Terminal open/restart/new/split (and persisted-terminal reopen) allocated
the PTY, spawned the shell, and built the event loop synchronously on the
main thread — tens of ms per action. Spawns now run on the background
executor behind a pending-spawn registry: completions validate the owning
session and workspace before installing, closing the drawer or a terminal
cancels in-flight spawns, repeated restarts supersede, and pending spawns
count toward the per-session terminal cap. The thread-local cwd override
is resolved on the calling thread (Terminal::resolve_spawn_cwd), and the
macOS openpty serialization from #148 is untouched.
secrets.json also moves off the main thread: profile-secret mutations go
through the FIFO store writer (SetProfileSecret/ClearProfileSecrets,
read-modify-write inside the writer, values never logged), and the
provider-start / title / probe / version-check / model-catalog env builds
read secrets inside blocking::unblock.
Co-Authored-By: Claude <noreply@anthropic.com>
Tryanks added a commit that referenced this pull request Jul 24, 2026
* fix(runtime): load session timelines off the GPUI main thread (#114)
Switching to, reopening, or background-loading a session used to read and
fold its entire JSONL history synchronously on the GPUI thread, hitching the
UI for the full disk read of long sessions.
- Install the session immediately (parked sessions keep their in-memory
snapshot; cold opens start empty) and swap in the folded timeline from a
background read, guarded by a per-session load generation.
- Retry the read (bounded) when the JSONL append watermark moved mid-read,
so events appended while parked can never be lost to a racy fold.
- Answer orchestrate status/result from in-memory timelines when the child
is loaded, and batch the remaining disk reads into one background task
that replies through the broker channel.
- Move the idle-child reaper, child-callback fold, and the remaining
synchronous .git/HEAD branch probes off the main thread.
Co-Authored-By: Claude <noreply@anthropic.com>
* perf(runtime): move SessionStore writes to a single background writer
Every store write ran synchronously on the GPUI thread: one JSONL append
per agent event during streaming, full sessions-index read/parse/rewrite
per persist_meta, transcript copies on fork, and terminal-ui.json rewrites
throughout resize drags.
- A FIFO async_channel drained by one background writer task now owns all
store mutations; index ops keep their read-modify-write merge semantics
inside the writer.
- Main-thread in-memory state stays authoritative and updates
synchronously; the writer is durability-only.
- Fork waits for the transcript clone to land before opening the fork.
- Write failures travel back over a report channel and surface through the
same RuntimeError notices as before.
- The app-quit hook enqueues a barrier and waits for every earlier write
to complete before exiting.
Co-Authored-By: Claude <noreply@anthropic.com>
* perf(ui,runtime): offload remaining one-shot blocking I/O from the GPUI thread
- Preview port scan (up to ~720ms of sequential connect timeouts per click)
- git worktree removal on session delete (seconds)
- Image paste/drop: metadata size check before reading, unbounded read +
transcode + attachment write in one background task, generation-guarded
completion that reserves strip slots across in-flight jobs
- Orchestrate dispatch cwd canonicalize/is_dir (network-FS stall)
- ACP registry cache read, provider install-source canonicalize
- AI-title scratch dir create/remove (ran on the main thread inside a
gpui local task)
- Plan save-to-workspace/download writes
- Add-project typed-path is_dir, third-party profile home creation
The Codex auth.json probe stays synchronous by design: it is one small
local file, and async-fs pools are unsafe inside gpui local tasks (see
crates/runtime/src/blocking.rs).
Co-Authored-By: Claude <noreply@anthropic.com>
* perf(term): spawn PTYs off the GPUI thread; offload secrets file I/O
Terminal open/restart/new/split (and persisted-terminal reopen) allocated
the PTY, spawned the shell, and built the event loop synchronously on the
main thread — tens of ms per action. Spawns now run on the background
executor behind a pending-spawn registry: completions validate the owning
session and workspace before installing, closing the drawer or a terminal
cancels in-flight spawns, repeated restarts supersede, and pending spawns
count toward the per-session terminal cap. The thread-local cwd override
is resolved on the calling thread (Terminal::resolve_spawn_cwd), and the
macOS openpty serialization from #148 is untouched.
secrets.json also moves off the main thread: profile-secret mutations go
through the FIFO store writer (SetProfileSecret/ClearProfileSecrets,
read-modify-write inside the writer, values never logged), and the
provider-start / title / probe / version-check / model-catalog env builds
read secrets inside blocking::unblock.
Co-Authored-By: Claude <noreply@anthropic.com>
---------
Co-authored-by: Claude <noreply@anthropic.com>
Sign up for freeto join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

term: intermittent PTY creation failure (Os error -6) on macOS under parallel test runs

1 participant

@Tryanks