perf: keep the send path off the main thread's disk and git - #60

Merged
Tryanks merged 1 commit into
mainfrom
perf/send-path-main-thread-stall
Jul 17, 2026
Merged

perf: keep the send path off the main thread's disk and git#60
Tryanks merged 1 commit into
mainfrom
perf/send-path-main-thread-stall

Conversation

@Tryanks

Copy link
Copy Markdown
Owner

Problem

Sending a follow-up message stalled the whole UI for seconds — tens of seconds on a nearly-full disk — because dispatch_next_queued did three pieces of heavy synchronous I/O on the UI thread before the turn could start:

  1. event_count() read AND parsed the entire session JSONL just to count events — tens of MB for a long conversation, on every send.
  2. capture_checkpoint() ran six blocking git subprocesses (read-tree, add -A over the whole worktree, write-tree, commit-tree, update-ref), no timeout.
  3. persist_meta() reloaded and re-parsed the whole sessions.json index after every upsert.

Checkpoint refs also accumulated forever (one repo: 153 refs / 1.18 GiB of loose objects), themselves contributing to the disk pressure that made the stalls so bad.

Fix

  • event_count() is now a newline byte-scan — append_event writes exactly one \n-terminated line per event, so the count is identical. (A crash-corrupted line can make a revert boundary sit slightly deep, never over-truncate.)
  • The checkpoint capture runs on the background executor via blocking::unblock. Two new flags preserve semantics: recorded_undispatched (head recorded but not yet sent — no double-record on re-entry) and awaiting_checkpoint (dispatch held until the capture lands, so the snapshot still precedes any agent edits). Rewind, session parking, queue-strip drops, and provider death during a capture are all handled; a capture landing after a rewind drops its stale result.
  • persist_meta reflects the upsert in memory (kept newest-first) instead of reloading the index from disk.
  • Checkpoints are capped at 20 per session; older refs are deleted off-thread and their turns lose the Revert affordance.

The user message bubble still appears synchronously on send; only the turn start waits for the snapshot (typically milliseconds).

Tests

  • New: checkpoints_are_capped_and_old_refs_pruned.
  • Adjusted three tests that assumed send→dispatch was synchronous (the bubble is still synchronous; only the turn command now waits for the capture).
  • cargo test --workspace green, cargo clippy --workspace --all-targets clean.

🤖 Generated with Claude Code

Sending a follow-up stalled the whole UI for seconds (tens of seconds on
a nearly-full disk) because dispatch_next_queued did three pieces of
heavy synchronous I/O on the UI thread before the turn could start:
* event_count() read AND parsed the entire session JSONL just to count
events — tens of MB for a long conversation, every send. It now scans
bytes for newlines; append_event writes exactly one \n-terminated
line per event, so the count is identical (a crash-corrupted line can
make a revert boundary sit slightly deep, never over-truncate).
* capture_checkpoint ran six blocking git subprocesses (read-tree,
add -A over the whole worktree, write-tree, commit-tree, update-ref).
The capture now runs on the background executor via blocking::unblock;
the recorded head is held (recorded_undispatched/awaiting_checkpoint)
until the capture lands, so the snapshot still precedes any agent
edits and a second send can't interleave into the same capture window.
* persist_meta reloaded and re-parsed the whole sessions.json index
after every upsert. It now reflects the upsert in memory, keeping the
newest-first order.
Also cap retained checkpoints at 20 per session: refs (and the git
objects they pin) accumulated for the life of the repo — one repo had
1.18 GiB of checkpoint loose objects, itself contributing to the disk
pressure that made the stalls so bad. Older turns lose their Revert
affordance; their refs are deleted off-thread.
Rewind/queue-drop reset the new dispatch flags so a capture landing
after a rewind drops its stale result instead of resurrecting a turn.
Co-Authored-By: Claude <noreply@anthropic.com>
@Tryanks
Tryanks merged commit 3abad13 into mainJul 17, 2026
3 checks passed
@Tryanks
Tryanks deleted the perf/send-path-main-thread-stall branch July 17, 2026 19:27
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.

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

perf: keep the send path off the main thread's disk and git - #60

Merged
Tryanks merged 1 commit into
mainfrom
perf/send-path-main-thread-stall
Jul 17, 2026
Merged

perf: keep the send path off the main thread's disk and git#60
Tryanks merged 1 commit into
mainfrom
perf/send-path-main-thread-stall

Conversation

@Tryanks

Copy link
Copy Markdown
Owner

Problem

Sending a follow-up message stalled the whole UI for seconds — tens of seconds on a nearly-full disk — because dispatch_next_queued did three pieces of heavy synchronous I/O on the UI thread before the turn could start:

  1. event_count() read AND parsed the entire session JSONL just to count events — tens of MB for a long conversation, on every send.
  2. capture_checkpoint() ran six blocking git subprocesses (read-tree, add -A over the whole worktree, write-tree, commit-tree, update-ref), no timeout.
  3. persist_meta() reloaded and re-parsed the whole sessions.json index after every upsert.

Checkpoint refs also accumulated forever (one repo: 153 refs / 1.18 GiB of loose objects), themselves contributing to the disk pressure that made the stalls so bad.

Fix

  • event_count() is now a newline byte-scan — append_event writes exactly one \n-terminated line per event, so the count is identical. (A crash-corrupted line can make a revert boundary sit slightly deep, never over-truncate.)
  • The checkpoint capture runs on the background executor via blocking::unblock. Two new flags preserve semantics: recorded_undispatched (head recorded but not yet sent — no double-record on re-entry) and awaiting_checkpoint (dispatch held until the capture lands, so the snapshot still precedes any agent edits). Rewind, session parking, queue-strip drops, and provider death during a capture are all handled; a capture landing after a rewind drops its stale result.
  • persist_meta reflects the upsert in memory (kept newest-first) instead of reloading the index from disk.
  • Checkpoints are capped at 20 per session; older refs are deleted off-thread and their turns lose the Revert affordance.

The user message bubble still appears synchronously on send; only the turn start waits for the snapshot (typically milliseconds).

Tests

  • New: checkpoints_are_capped_and_old_refs_pruned.
  • Adjusted three tests that assumed send→dispatch was synchronous (the bubble is still synchronous; only the turn command now waits for the capture).
  • cargo test --workspace green, cargo clippy --workspace --all-targets clean.

🤖 Generated with Claude Code

Sending a follow-up stalled the whole UI for seconds (tens of seconds on
a nearly-full disk) because dispatch_next_queued did three pieces of
heavy synchronous I/O on the UI thread before the turn could start:
* event_count() read AND parsed the entire session JSONL just to count
events — tens of MB for a long conversation, every send. It now scans
bytes for newlines; append_event writes exactly one \n-terminated
line per event, so the count is identical (a crash-corrupted line can
make a revert boundary sit slightly deep, never over-truncate).
* capture_checkpoint ran six blocking git subprocesses (read-tree,
add -A over the whole worktree, write-tree, commit-tree, update-ref).
The capture now runs on the background executor via blocking::unblock;
the recorded head is held (recorded_undispatched/awaiting_checkpoint)
until the capture lands, so the snapshot still precedes any agent
edits and a second send can't interleave into the same capture window.
* persist_meta reloaded and re-parsed the whole sessions.json index
after every upsert. It now reflects the upsert in memory, keeping the
newest-first order.
Also cap retained checkpoints at 20 per session: refs (and the git
objects they pin) accumulated for the life of the repo — one repo had
1.18 GiB of checkpoint loose objects, itself contributing to the disk
pressure that made the stalls so bad. Older turns lose their Revert
affordance; their refs are deleted off-thread.
Rewind/queue-drop reset the new dispatch flags so a capture landing
after a rewind drops its stale result instead of resurrecting a turn.
Co-Authored-By: Claude <noreply@anthropic.com>
@Tryanks
Tryanks merged commit 3abad13 into mainJul 17, 2026
3 checks passed
@Tryanks
Tryanks deleted the perf/send-path-main-thread-stall branch July 17, 2026 19:27
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.

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

perf: keep the send path off the main thread's disk and git - #60

Merged
Tryanks merged 1 commit into
mainfrom
perf/send-path-main-thread-stall
Jul 17, 2026
Merged

perf: keep the send path off the main thread's disk and git#60
Tryanks merged 1 commit into
mainfrom
perf/send-path-main-thread-stall

Conversation

@Tryanks

Copy link
Copy Markdown
Owner

Problem

Sending a follow-up message stalled the whole UI for seconds — tens of seconds on a nearly-full disk — because dispatch_next_queued did three pieces of heavy synchronous I/O on the UI thread before the turn could start:

  1. event_count() read AND parsed the entire session JSONL just to count events — tens of MB for a long conversation, on every send.
  2. capture_checkpoint() ran six blocking git subprocesses (read-tree, add -A over the whole worktree, write-tree, commit-tree, update-ref), no timeout.
  3. persist_meta() reloaded and re-parsed the whole sessions.json index after every upsert.

Checkpoint refs also accumulated forever (one repo: 153 refs / 1.18 GiB of loose objects), themselves contributing to the disk pressure that made the stalls so bad.

Fix

  • event_count() is now a newline byte-scan — append_event writes exactly one \n-terminated line per event, so the count is identical. (A crash-corrupted line can make a revert boundary sit slightly deep, never over-truncate.)
  • The checkpoint capture runs on the background executor via blocking::unblock. Two new flags preserve semantics: recorded_undispatched (head recorded but not yet sent — no double-record on re-entry) and awaiting_checkpoint (dispatch held until the capture lands, so the snapshot still precedes any agent edits). Rewind, session parking, queue-strip drops, and provider death during a capture are all handled; a capture landing after a rewind drops its stale result.
  • persist_meta reflects the upsert in memory (kept newest-first) instead of reloading the index from disk.
  • Checkpoints are capped at 20 per session; older refs are deleted off-thread and their turns lose the Revert affordance.

The user message bubble still appears synchronously on send; only the turn start waits for the snapshot (typically milliseconds).

Tests

  • New: checkpoints_are_capped_and_old_refs_pruned.
  • Adjusted three tests that assumed send→dispatch was synchronous (the bubble is still synchronous; only the turn command now waits for the capture).
  • cargo test --workspace green, cargo clippy --workspace --all-targets clean.

🤖 Generated with Claude Code

Sending a follow-up stalled the whole UI for seconds (tens of seconds on
a nearly-full disk) because dispatch_next_queued did three pieces of
heavy synchronous I/O on the UI thread before the turn could start:
* event_count() read AND parsed the entire session JSONL just to count
events — tens of MB for a long conversation, every send. It now scans
bytes for newlines; append_event writes exactly one \n-terminated
line per event, so the count is identical (a crash-corrupted line can
make a revert boundary sit slightly deep, never over-truncate).
* capture_checkpoint ran six blocking git subprocesses (read-tree,
add -A over the whole worktree, write-tree, commit-tree, update-ref).
The capture now runs on the background executor via blocking::unblock;
the recorded head is held (recorded_undispatched/awaiting_checkpoint)
until the capture lands, so the snapshot still precedes any agent
edits and a second send can't interleave into the same capture window.
* persist_meta reloaded and re-parsed the whole sessions.json index
after every upsert. It now reflects the upsert in memory, keeping the
newest-first order.
Also cap retained checkpoints at 20 per session: refs (and the git
objects they pin) accumulated for the life of the repo — one repo had
1.18 GiB of checkpoint loose objects, itself contributing to the disk
pressure that made the stalls so bad. Older turns lose their Revert
affordance; their refs are deleted off-thread.
Rewind/queue-drop reset the new dispatch flags so a capture landing
after a rewind drops its stale result instead of resurrecting a turn.
Co-Authored-By: Claude <noreply@anthropic.com>
@Tryanks
Tryanks merged commit 3abad13 into mainJul 17, 2026
3 checks passed
@Tryanks
Tryanks deleted the perf/send-path-main-thread-stall branch July 17, 2026 19:27
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.

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

perf: keep the send path off the main thread's disk and git - #60

Merged
Tryanks merged 1 commit into
mainfrom
perf/send-path-main-thread-stall
Jul 17, 2026
Merged

perf: keep the send path off the main thread's disk and git#60
Tryanks merged 1 commit into
mainfrom
perf/send-path-main-thread-stall

Conversation

@Tryanks

Copy link
Copy Markdown
Owner

Problem

Sending a follow-up message stalled the whole UI for seconds — tens of seconds on a nearly-full disk — because dispatch_next_queued did three pieces of heavy synchronous I/O on the UI thread before the turn could start:

  1. event_count() read AND parsed the entire session JSONL just to count events — tens of MB for a long conversation, on every send.
  2. capture_checkpoint() ran six blocking git subprocesses (read-tree, add -A over the whole worktree, write-tree, commit-tree, update-ref), no timeout.
  3. persist_meta() reloaded and re-parsed the whole sessions.json index after every upsert.

Checkpoint refs also accumulated forever (one repo: 153 refs / 1.18 GiB of loose objects), themselves contributing to the disk pressure that made the stalls so bad.

Fix

  • event_count() is now a newline byte-scan — append_event writes exactly one \n-terminated line per event, so the count is identical. (A crash-corrupted line can make a revert boundary sit slightly deep, never over-truncate.)
  • The checkpoint capture runs on the background executor via blocking::unblock. Two new flags preserve semantics: recorded_undispatched (head recorded but not yet sent — no double-record on re-entry) and awaiting_checkpoint (dispatch held until the capture lands, so the snapshot still precedes any agent edits). Rewind, session parking, queue-strip drops, and provider death during a capture are all handled; a capture landing after a rewind drops its stale result.
  • persist_meta reflects the upsert in memory (kept newest-first) instead of reloading the index from disk.
  • Checkpoints are capped at 20 per session; older refs are deleted off-thread and their turns lose the Revert affordance.

The user message bubble still appears synchronously on send; only the turn start waits for the snapshot (typically milliseconds).

Tests

  • New: checkpoints_are_capped_and_old_refs_pruned.
  • Adjusted three tests that assumed send→dispatch was synchronous (the bubble is still synchronous; only the turn command now waits for the capture).
  • cargo test --workspace green, cargo clippy --workspace --all-targets clean.

🤖 Generated with Claude Code

Sending a follow-up stalled the whole UI for seconds (tens of seconds on
a nearly-full disk) because dispatch_next_queued did three pieces of
heavy synchronous I/O on the UI thread before the turn could start:
* event_count() read AND parsed the entire session JSONL just to count
events — tens of MB for a long conversation, every send. It now scans
bytes for newlines; append_event writes exactly one \n-terminated
line per event, so the count is identical (a crash-corrupted line can
make a revert boundary sit slightly deep, never over-truncate).
* capture_checkpoint ran six blocking git subprocesses (read-tree,
add -A over the whole worktree, write-tree, commit-tree, update-ref).
The capture now runs on the background executor via blocking::unblock;
the recorded head is held (recorded_undispatched/awaiting_checkpoint)
until the capture lands, so the snapshot still precedes any agent
edits and a second send can't interleave into the same capture window.
* persist_meta reloaded and re-parsed the whole sessions.json index
after every upsert. It now reflects the upsert in memory, keeping the
newest-first order.
Also cap retained checkpoints at 20 per session: refs (and the git
objects they pin) accumulated for the life of the repo — one repo had
1.18 GiB of checkpoint loose objects, itself contributing to the disk
pressure that made the stalls so bad. Older turns lose their Revert
affordance; their refs are deleted off-thread.
Rewind/queue-drop reset the new dispatch flags so a capture landing
after a rewind drops its stale result instead of resurrecting a turn.
Co-Authored-By: Claude <noreply@anthropic.com>
@Tryanks
Tryanks merged commit 3abad13 into mainJul 17, 2026
3 checks passed
@Tryanks
Tryanks deleted the perf/send-path-main-thread-stall branch July 17, 2026 19:27
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.

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

perf: keep the send path off the main thread's disk and git - #60

Merged
Tryanks merged 1 commit into
mainfrom
perf/send-path-main-thread-stall
Jul 17, 2026
Merged

perf: keep the send path off the main thread's disk and git#60
Tryanks merged 1 commit into
mainfrom
perf/send-path-main-thread-stall

Conversation

@Tryanks

Copy link
Copy Markdown
Owner

Problem

Sending a follow-up message stalled the whole UI for seconds — tens of seconds on a nearly-full disk — because dispatch_next_queued did three pieces of heavy synchronous I/O on the UI thread before the turn could start:

  1. event_count() read AND parsed the entire session JSONL just to count events — tens of MB for a long conversation, on every send.
  2. capture_checkpoint() ran six blocking git subprocesses (read-tree, add -A over the whole worktree, write-tree, commit-tree, update-ref), no timeout.
  3. persist_meta() reloaded and re-parsed the whole sessions.json index after every upsert.

Checkpoint refs also accumulated forever (one repo: 153 refs / 1.18 GiB of loose objects), themselves contributing to the disk pressure that made the stalls so bad.

Fix

  • event_count() is now a newline byte-scan — append_event writes exactly one \n-terminated line per event, so the count is identical. (A crash-corrupted line can make a revert boundary sit slightly deep, never over-truncate.)
  • The checkpoint capture runs on the background executor via blocking::unblock. Two new flags preserve semantics: recorded_undispatched (head recorded but not yet sent — no double-record on re-entry) and awaiting_checkpoint (dispatch held until the capture lands, so the snapshot still precedes any agent edits). Rewind, session parking, queue-strip drops, and provider death during a capture are all handled; a capture landing after a rewind drops its stale result.
  • persist_meta reflects the upsert in memory (kept newest-first) instead of reloading the index from disk.
  • Checkpoints are capped at 20 per session; older refs are deleted off-thread and their turns lose the Revert affordance.

The user message bubble still appears synchronously on send; only the turn start waits for the snapshot (typically milliseconds).

Tests

  • New: checkpoints_are_capped_and_old_refs_pruned.
  • Adjusted three tests that assumed send→dispatch was synchronous (the bubble is still synchronous; only the turn command now waits for the capture).
  • cargo test --workspace green, cargo clippy --workspace --all-targets clean.

🤖 Generated with Claude Code

Sending a follow-up stalled the whole UI for seconds (tens of seconds on
a nearly-full disk) because dispatch_next_queued did three pieces of
heavy synchronous I/O on the UI thread before the turn could start:
* event_count() read AND parsed the entire session JSONL just to count
events — tens of MB for a long conversation, every send. It now scans
bytes for newlines; append_event writes exactly one \n-terminated
line per event, so the count is identical (a crash-corrupted line can
make a revert boundary sit slightly deep, never over-truncate).
* capture_checkpoint ran six blocking git subprocesses (read-tree,
add -A over the whole worktree, write-tree, commit-tree, update-ref).
The capture now runs on the background executor via blocking::unblock;
the recorded head is held (recorded_undispatched/awaiting_checkpoint)
until the capture lands, so the snapshot still precedes any agent
edits and a second send can't interleave into the same capture window.
* persist_meta reloaded and re-parsed the whole sessions.json index
after every upsert. It now reflects the upsert in memory, keeping the
newest-first order.
Also cap retained checkpoints at 20 per session: refs (and the git
objects they pin) accumulated for the life of the repo — one repo had
1.18 GiB of checkpoint loose objects, itself contributing to the disk
pressure that made the stalls so bad. Older turns lose their Revert
affordance; their refs are deleted off-thread.
Rewind/queue-drop reset the new dispatch flags so a capture landing
after a rewind drops its stale result instead of resurrecting a turn.
Co-Authored-By: Claude <noreply@anthropic.com>
@Tryanks
Tryanks merged commit 3abad13 into mainJul 17, 2026
3 checks passed
@Tryanks
Tryanks deleted the perf/send-path-main-thread-stall branch July 17, 2026 19:27
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.

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

perf: keep the send path off the main thread's disk and git - #60

Merged
Tryanks merged 1 commit into
mainfrom
perf/send-path-main-thread-stall
Jul 17, 2026
Merged

perf: keep the send path off the main thread's disk and git#60
Tryanks merged 1 commit into
mainfrom
perf/send-path-main-thread-stall

Conversation

@Tryanks

Copy link
Copy Markdown
Owner

Problem

Sending a follow-up message stalled the whole UI for seconds — tens of seconds on a nearly-full disk — because dispatch_next_queued did three pieces of heavy synchronous I/O on the UI thread before the turn could start:

  1. event_count() read AND parsed the entire session JSONL just to count events — tens of MB for a long conversation, on every send.
  2. capture_checkpoint() ran six blocking git subprocesses (read-tree, add -A over the whole worktree, write-tree, commit-tree, update-ref), no timeout.
  3. persist_meta() reloaded and re-parsed the whole sessions.json index after every upsert.

Checkpoint refs also accumulated forever (one repo: 153 refs / 1.18 GiB of loose objects), themselves contributing to the disk pressure that made the stalls so bad.

Fix

  • event_count() is now a newline byte-scan — append_event writes exactly one \n-terminated line per event, so the count is identical. (A crash-corrupted line can make a revert boundary sit slightly deep, never over-truncate.)
  • The checkpoint capture runs on the background executor via blocking::unblock. Two new flags preserve semantics: recorded_undispatched (head recorded but not yet sent — no double-record on re-entry) and awaiting_checkpoint (dispatch held until the capture lands, so the snapshot still precedes any agent edits). Rewind, session parking, queue-strip drops, and provider death during a capture are all handled; a capture landing after a rewind drops its stale result.
  • persist_meta reflects the upsert in memory (kept newest-first) instead of reloading the index from disk.
  • Checkpoints are capped at 20 per session; older refs are deleted off-thread and their turns lose the Revert affordance.

The user message bubble still appears synchronously on send; only the turn start waits for the snapshot (typically milliseconds).

Tests

  • New: checkpoints_are_capped_and_old_refs_pruned.
  • Adjusted three tests that assumed send→dispatch was synchronous (the bubble is still synchronous; only the turn command now waits for the capture).
  • cargo test --workspace green, cargo clippy --workspace --all-targets clean.

🤖 Generated with Claude Code

Sending a follow-up stalled the whole UI for seconds (tens of seconds on
a nearly-full disk) because dispatch_next_queued did three pieces of
heavy synchronous I/O on the UI thread before the turn could start:
* event_count() read AND parsed the entire session JSONL just to count
events — tens of MB for a long conversation, every send. It now scans
bytes for newlines; append_event writes exactly one \n-terminated
line per event, so the count is identical (a crash-corrupted line can
make a revert boundary sit slightly deep, never over-truncate).
* capture_checkpoint ran six blocking git subprocesses (read-tree,
add -A over the whole worktree, write-tree, commit-tree, update-ref).
The capture now runs on the background executor via blocking::unblock;
the recorded head is held (recorded_undispatched/awaiting_checkpoint)
until the capture lands, so the snapshot still precedes any agent
edits and a second send can't interleave into the same capture window.
* persist_meta reloaded and re-parsed the whole sessions.json index
after every upsert. It now reflects the upsert in memory, keeping the
newest-first order.
Also cap retained checkpoints at 20 per session: refs (and the git
objects they pin) accumulated for the life of the repo — one repo had
1.18 GiB of checkpoint loose objects, itself contributing to the disk
pressure that made the stalls so bad. Older turns lose their Revert
affordance; their refs are deleted off-thread.
Rewind/queue-drop reset the new dispatch flags so a capture landing
after a rewind drops its stale result instead of resurrecting a turn.
Co-Authored-By: Claude <noreply@anthropic.com>
@Tryanks
Tryanks merged commit 3abad13 into mainJul 17, 2026
3 checks passed
@Tryanks
Tryanks deleted the perf/send-path-main-thread-stall branch July 17, 2026 19:27
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.

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

perf: keep the send path off the main thread's disk and git - #60

Merged
Tryanks merged 1 commit into
mainfrom
perf/send-path-main-thread-stall
Jul 17, 2026
Merged

perf: keep the send path off the main thread's disk and git#60
Tryanks merged 1 commit into
mainfrom
perf/send-path-main-thread-stall

Conversation

@Tryanks

Copy link
Copy Markdown
Owner

Problem

Sending a follow-up message stalled the whole UI for seconds — tens of seconds on a nearly-full disk — because dispatch_next_queued did three pieces of heavy synchronous I/O on the UI thread before the turn could start:

  1. event_count() read AND parsed the entire session JSONL just to count events — tens of MB for a long conversation, on every send.
  2. capture_checkpoint() ran six blocking git subprocesses (read-tree, add -A over the whole worktree, write-tree, commit-tree, update-ref), no timeout.
  3. persist_meta() reloaded and re-parsed the whole sessions.json index after every upsert.

Checkpoint refs also accumulated forever (one repo: 153 refs / 1.18 GiB of loose objects), themselves contributing to the disk pressure that made the stalls so bad.

Fix

  • event_count() is now a newline byte-scan — append_event writes exactly one \n-terminated line per event, so the count is identical. (A crash-corrupted line can make a revert boundary sit slightly deep, never over-truncate.)
  • The checkpoint capture runs on the background executor via blocking::unblock. Two new flags preserve semantics: recorded_undispatched (head recorded but not yet sent — no double-record on re-entry) and awaiting_checkpoint (dispatch held until the capture lands, so the snapshot still precedes any agent edits). Rewind, session parking, queue-strip drops, and provider death during a capture are all handled; a capture landing after a rewind drops its stale result.
  • persist_meta reflects the upsert in memory (kept newest-first) instead of reloading the index from disk.
  • Checkpoints are capped at 20 per session; older refs are deleted off-thread and their turns lose the Revert affordance.

The user message bubble still appears synchronously on send; only the turn start waits for the snapshot (typically milliseconds).

Tests

  • New: checkpoints_are_capped_and_old_refs_pruned.
  • Adjusted three tests that assumed send→dispatch was synchronous (the bubble is still synchronous; only the turn command now waits for the capture).
  • cargo test --workspace green, cargo clippy --workspace --all-targets clean.

🤖 Generated with Claude Code

Sending a follow-up stalled the whole UI for seconds (tens of seconds on
a nearly-full disk) because dispatch_next_queued did three pieces of
heavy synchronous I/O on the UI thread before the turn could start:
* event_count() read AND parsed the entire session JSONL just to count
events — tens of MB for a long conversation, every send. It now scans
bytes for newlines; append_event writes exactly one \n-terminated
line per event, so the count is identical (a crash-corrupted line can
make a revert boundary sit slightly deep, never over-truncate).
* capture_checkpoint ran six blocking git subprocesses (read-tree,
add -A over the whole worktree, write-tree, commit-tree, update-ref).
The capture now runs on the background executor via blocking::unblock;
the recorded head is held (recorded_undispatched/awaiting_checkpoint)
until the capture lands, so the snapshot still precedes any agent
edits and a second send can't interleave into the same capture window.
* persist_meta reloaded and re-parsed the whole sessions.json index
after every upsert. It now reflects the upsert in memory, keeping the
newest-first order.
Also cap retained checkpoints at 20 per session: refs (and the git
objects they pin) accumulated for the life of the repo — one repo had
1.18 GiB of checkpoint loose objects, itself contributing to the disk
pressure that made the stalls so bad. Older turns lose their Revert
affordance; their refs are deleted off-thread.
Rewind/queue-drop reset the new dispatch flags so a capture landing
after a rewind drops its stale result instead of resurrecting a turn.
Co-Authored-By: Claude <noreply@anthropic.com>
@Tryanks
Tryanks merged commit 3abad13 into mainJul 17, 2026
3 checks passed
@Tryanks
Tryanks deleted the perf/send-path-main-thread-stall branch July 17, 2026 19:27
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.

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

perf: keep the send path off the main thread's disk and git - #60

Merged
Tryanks merged 1 commit into
mainfrom
perf/send-path-main-thread-stall
Jul 17, 2026
Merged

perf: keep the send path off the main thread's disk and git#60
Tryanks merged 1 commit into
mainfrom
perf/send-path-main-thread-stall

Conversation

@Tryanks

Copy link
Copy Markdown
Owner

Problem

Sending a follow-up message stalled the whole UI for seconds — tens of seconds on a nearly-full disk — because dispatch_next_queued did three pieces of heavy synchronous I/O on the UI thread before the turn could start:

  1. event_count() read AND parsed the entire session JSONL just to count events — tens of MB for a long conversation, on every send.
  2. capture_checkpoint() ran six blocking git subprocesses (read-tree, add -A over the whole worktree, write-tree, commit-tree, update-ref), no timeout.
  3. persist_meta() reloaded and re-parsed the whole sessions.json index after every upsert.

Checkpoint refs also accumulated forever (one repo: 153 refs / 1.18 GiB of loose objects), themselves contributing to the disk pressure that made the stalls so bad.

Fix

  • event_count() is now a newline byte-scan — append_event writes exactly one \n-terminated line per event, so the count is identical. (A crash-corrupted line can make a revert boundary sit slightly deep, never over-truncate.)
  • The checkpoint capture runs on the background executor via blocking::unblock. Two new flags preserve semantics: recorded_undispatched (head recorded but not yet sent — no double-record on re-entry) and awaiting_checkpoint (dispatch held until the capture lands, so the snapshot still precedes any agent edits). Rewind, session parking, queue-strip drops, and provider death during a capture are all handled; a capture landing after a rewind drops its stale result.
  • persist_meta reflects the upsert in memory (kept newest-first) instead of reloading the index from disk.
  • Checkpoints are capped at 20 per session; older refs are deleted off-thread and their turns lose the Revert affordance.

The user message bubble still appears synchronously on send; only the turn start waits for the snapshot (typically milliseconds).

Tests

  • New: checkpoints_are_capped_and_old_refs_pruned.
  • Adjusted three tests that assumed send→dispatch was synchronous (the bubble is still synchronous; only the turn command now waits for the capture).
  • cargo test --workspace green, cargo clippy --workspace --all-targets clean.

🤖 Generated with Claude Code

Sending a follow-up stalled the whole UI for seconds (tens of seconds on
a nearly-full disk) because dispatch_next_queued did three pieces of
heavy synchronous I/O on the UI thread before the turn could start:
* event_count() read AND parsed the entire session JSONL just to count
events — tens of MB for a long conversation, every send. It now scans
bytes for newlines; append_event writes exactly one \n-terminated
line per event, so the count is identical (a crash-corrupted line can
make a revert boundary sit slightly deep, never over-truncate).
* capture_checkpoint ran six blocking git subprocesses (read-tree,
add -A over the whole worktree, write-tree, commit-tree, update-ref).
The capture now runs on the background executor via blocking::unblock;
the recorded head is held (recorded_undispatched/awaiting_checkpoint)
until the capture lands, so the snapshot still precedes any agent
edits and a second send can't interleave into the same capture window.
* persist_meta reloaded and re-parsed the whole sessions.json index
after every upsert. It now reflects the upsert in memory, keeping the
newest-first order.
Also cap retained checkpoints at 20 per session: refs (and the git
objects they pin) accumulated for the life of the repo — one repo had
1.18 GiB of checkpoint loose objects, itself contributing to the disk
pressure that made the stalls so bad. Older turns lose their Revert
affordance; their refs are deleted off-thread.
Rewind/queue-drop reset the new dispatch flags so a capture landing
after a rewind drops its stale result instead of resurrecting a turn.
Co-Authored-By: Claude <noreply@anthropic.com>
@Tryanks
Tryanks merged commit 3abad13 into mainJul 17, 2026
3 checks passed
@Tryanks
Tryanks deleted the perf/send-path-main-thread-stall branch July 17, 2026 19:27
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.

1 participant

@Tryanks