') + ')', 'gi');
if (regex.test(text)) {
found = true;
var frag = document.createDocumentFragment();
var parts = text.split(regex);
parts.forEach(function(part, i) {
if (i % 2 === 0) {
frag.appendChild(document.createTextNode(part));
} else {
var span = document.createElement('span');
span.className = 'userscript-highlight';
span.textContent = part;
frag.appendChild(span);
}
});
node.parentNode.replaceChild(frag, node);
}
});
} else if (node.nodeType === 1 && node.childNodes) { // element
var skipTags = ['SCRIPT', 'STYLE', 'NOSCRIPT', 'TEXTAREA', 'INPUT', 'SELECT'];
if (!skipTags.includes(node.tagName)) {
Array.from(node.childNodes).forEach(highlight);
}
}
}
highlight(document.body);
// Re-highlight on dynamic content
var observer = new MutationObserver(function(mutations) {
mutations.forEach(function(m) {
m.addedNodes.forEach(function(node) {
if (node.nodeType === 1 || node.nodeType === 3) highlight(node);
});
});
});
observer.observe(document.body, { childList: true, subtree: true });
})();
}
} catch(__e) { console.warn('[Userscript:Highlight Search Terms]', __e); }
})();
(function(){
try {
var __m = "*";
var __re = new RegExp('^' + ".*" + ', 'i');
if (__m === '*' || __re.test(location.href)) {
// Strip utm_, fbclid, gclid, etc. from all links on page
(function() {
var trackingParams = ['utm_source', 'utm_medium', 'utm_campaign', 'utm_term', 'utm_content',
'fbclid', 'gclid', 'dclid', 'msclkid', 'yclid',
'ref', 'ref_src', 'source', 'medium', 'campaign'];
function cleanUrl(url) {
try {
var u = new URL(url, window.location.origin);
var changed = false;
trackingParams.forEach(function(p) {
if (u.searchParams.has(p)) {
u.searchParams.delete(p);
changed = true;
}
});
return changed ? u.toString() : url;
} catch (e) {
return url;
}
}
function cleanLinks() {
document.querySelectorAll('a[href]').forEach(function(a) {
var clean = cleanUrl(a.href);
if (clean !== a.href) a.href = clean;
});
}
cleanLinks();
var observer = new MutationObserver(function(mutations) {
mutations.forEach(function(m) {
m.addedNodes.forEach(function(node) {
if (node.nodeType === 1) {
if (node.tagName === 'A') cleanLinks();
node.querySelectorAll('a[href]').forEach(function(a) {
var clean = cleanUrl(a.href);
if (clean !== a.href) a.href = clean;
});
}
});
});
});
observer.observe(document.body, { childList: true, subtree: true });
})();
}
} catch(__e) { console.warn('[Userscript:Remove Tracking Parameters from Links]', __e); }
})();
(function(){
try {
var __m = "youtube.com";
var __re = new RegExp('^' + "youtube\\.com" + ', 'i');
if (__m === '*' || __re.test(location.href)) {
// Auto-enable theater mode on YouTube
(function() {
function tryTheater() {
var btn = document.querySelector('button[aria-label="Theater mode"], ytd-player #player button[title="Theater mode"]');
if (btn && !btn.classList.contains('activated')) {
btn.click();
}
}
// Try immediately
tryTheater();
// Try after navigation (SPA)
var lastUrl = location.href;
setInterval(function() {
if (location.href !== lastUrl) {
lastUrl = location.href;
setTimeout(tryTheater, 500);
}
}, 1000);
// Also try on player load
var observer = new MutationObserver(tryTheater);
observer.observe(document.body, { childList: true, subtree: true });
})();
}
} catch(__e) { console.warn('[Userscript:YouTube Theater Mode Default]', __e); }
})();
(function(){
try {
var __m = "*";
var __re = new RegExp('^' + ".*" + ', 'i');
if (__m === '*' || __re.test(location.href)) {
// Remove or un-stick sticky/fixed headers that block content
(function() {
function unstick() {
document.querySelectorAll('header, nav, [role="banner"], .header, .navbar, .sticky, .fixed-top, [style*="position: fixed"], [style*="position:sticky"]').forEach(function(el) {
if (el.style.position === 'fixed' || el.style.position === 'sticky' || getComputedStyle(el).position === 'fixed' || getComputedStyle(el).position === 'sticky') {
el.style.position = 'static';
el.style.top = 'auto';
el.style.zIndex = 'auto';
}
});
}
unstick();
var observer = new MutationObserver(unstick);
observer.observe(document.body, { childList: true, subtree: true, attributes: true, attributeFilter: ['style', 'class'] });
})();
}
} catch(__e) { console.warn('[Userscript:Kill Sticky Headers]', __e); }
})();
})();
[world-local] Fix resumeHook racing hook disposal being journaled after hook_disposed by VaguelySerious · Pull Request #2808 · vercel/workflow · GitHub
You signed in with another tab or window. Reload to refresh your session.You signed out in another tab or window. Reload to refresh your session.You switched accounts on another tab or window. Reload to refresh your session.Dismiss alert
Closes#2781. Also addresses the post-merge feedback on #2779 (guarded claim release).
1. hook_received journaled after hook_disposed (#2781)
A hook_received write only validated that the hook entity exists, and was not serialized against the hook_disposed write for the same hook. A resume that passed its existence check before the disposal began could be journaled afterhook_disposed — once that order is in the log, every replay of the owning run diverges at the post-disposal hook_received and the run escalates to CorruptedEventLogError.
hook_received and hook_disposed now share the per-hook in-process lock previously taken only by hook_created, making the resume's "validate, then append" atomic with the disposer's "dispose lock → entity delete → append" within a storage instance.
The hook_received write re-validates the durable dispose lock from [core] Fix hook token reuse after dispose() (same-run and cross-run) #2779 (at acceptance, and once more immediately before the event append, narrowing the cross-instance window to the single event write), rejecting with HookNotFoundError — the same surface as a resume that arrives after teardown finished. Acceptance now observes the same order replay will see.
Both in-flight releasers (the hook_disposed handler and the terminal-run deleteAllHooksForRun cleanup) deleted the token claim file unconditionally after reading the hook entity. A releaser stalled between those operations could outlive a claimant force-releasing its stale claim, and its deferred delete would then destroy the new claimant's live claim — transiently breaking token uniqueness. Releasers now re-read the claim and delete it only if it still points at their own (runId, hookId); a claim owned by someone else is left for the claimant-side force-release path to reap.
Tests
A deterministic mid-teardown acceptance test (dispose lock committed, hook entity still present) that fails without fix 1.
A resume-vs-dispose race loop asserting hook_received is never journaled after hook_disposed and that a rejected resume leaves no event behind.
Two claim-takeover tests (one per releaser) that rewrite the claim to a foreign (runId, hookId) and assert the release skips it; both fail without fix 2.
A hook_received write only validated that the hook entity exists, and was
not serialized against the hook_disposed write for the same hook. A resume
that passed its existence check before the disposal began could therefore
be journaled after hook_disposed — an order that makes every subsequent
replay of the owning run diverge at that event and escalate to
CorruptedEventLogError.
hook_received and hook_disposed now share the per-hook in-process lock
with hook_created, and the hook_received write re-validates the durable
dispose lock (both at acceptance and immediately before the event append),
rejecting with HookNotFoundError exactly like a resume that arrived after
teardown finished.
Closes#2781
Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
…tall
Both in-flight releasers (the hook_disposed handler and the terminal-run
deleteAllHooksForRun cleanup) deleted the claim file unconditionally after
reading the hook entity. A releaser stalled between those operations could
outlive a claimant force-releasing its stale claim, and its deferred delete
would then destroy the new claimant's live claim — transiently breaking
token uniqueness (a third claimant could claim the token too).
Releasers now re-read the claim and delete it only if it still points at
their own (runId, hookId). Still TOCTOU, but the window shrinks from "a
stall of any length" to adjacent file ops; a claim owned by someone else
is left for the claimant-side force-release path to reap.
Addresses post-merge feedback on #2779.
Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
The reason will be displayed to describe this comment to others. Learn more.
Reviewed and verified the fixes hands-on. Approving.
What I checked:
Fix 1 mechanism: the per-hook in-process lock now wraps the entirecreateImpl() for all three hook lifecycle events, so the resume's "validate → append" is atomic with the disposer's "dispose lock → entity delete → append" within an instance; the lock key (${runId}-${correlationId}.hook) matches on both sides. The disposer's whole sequence runs inside createImpl() with no nested events.create — no self-deadlock. Cross-instance, the durable dispose-lock re-check at acceptance + immediately before the append narrows the window to the single event write, as documented.
Fix 2: the ownership match on (runId, hookId) is safe for normal releases — the claim writer has always persisted both fields — and a skipped release is reaped by the claimant-side force-release loop, so tokens still free up.
Counterfactual: reverted events-storage.ts/hooks-storage.ts/helpers.ts to main while keeping the new tests — the mid-teardown acceptance test and both claim-takeover tests fail exactly as claimed, and pass again on the branch.
Suites: full @workflow/world-local 438/438 locally; CI fully green (103 checks, incl. the usually-flaky webpack lane).
Non-blocking observations:
The 20-round race-loop test passed even with the fix reverted on my machine — it's a soak, not a reliable regression guard; the deterministic mid-teardown test is the real one.
Corrupt (unparseable) claim files are no longer deleted by any path: the releaser now skips them (correctly — ownership is undeterminable), and the claimant loop's force-delete is only reachable with a parsed claim (readHookTokenClaim → null → continue), so an unreadable claim can block its token indefinitely. Very low likelihood; a "unparseable after N observations → delete" fallback in the claimant loop would close it if ever seen in practice.
Nit: hook_created still builds the claim path inline (~L1631) instead of using the new hookTokenClaimPath() helper — worth unifying so the layout can't drift.
The reason will be displayed to describe this comment to others. Learn more.
Reviewed both parts against the #2779 machinery. Approving.
Part 1 (hook_received vs hook_disposed, #2781): The two-layer approach is right: the per-hook in-process lock makes resume-vs-dispose atomic within a storage instance, and the durable dispose-lock re-validation — at acceptance and immediately before the event append — narrows the cross-instance window to the single event write. The comments are honest that this is a narrowing rather than full elimination; that matches the module's existing convention (on-disk state as source of truth, in-process locks as fast path), and going further would require holding an fs-level mutex across validate+append, which isn't worth it for this world. Rejecting with HookNotFoundError gives resumes that lose the race the same surface as resumes arriving after teardown — correct and unobservable to callers as a new state.
Part 2 (guarded claim release): Good catch — this closes a gap in the #2779 force-release design I didn't flag in my review: a releaser stalled between its entity read and claim delete could destroy the next claimant's live claim. The guarded release direction is right in both failure modes: a claim that's missing/unreadable/foreign is left alone (never delete what you can't verify), and genuinely stale debris still gets reaped by the claimant-side force-release. Both releasers (hook_disposed handler and deleteAllHooksForRun) are covered. The remaining read→unlink window is honestly documented and only reachable via a second takeover within adjacent fs ops — proportionate for this world.
I also checked the hookTokenClaimPath helper against the hook_created claim-write site: both use path.join with identical segments, so there's no absolute/relative path-construction mismatch (a hazard this module has hit before).
Verified locally: full world-local suite passes including the 4 new tests (both releaser-guard takeover tests, the mid-teardown acceptance rejection, and the resume-vs-dispose race loop asserting the journal-order invariant). CI fully green.
One non-blocking nit: the hook_created claim site still builds its constraintPath inline — now that hookTokenClaimPath exists, unifying on the helper would remove the last duplicated construction of that path.
Both fixes in this commit build directly on the #2779 durable dispose-lock and force-release/claim infrastructure that exists only on main: fix #1 re-validates via isHookDisposalCommitted/hookDisposeLockPath and fix #2 guards against the isHookTokenClaimReleasable force-release path — none of which exist on origin/stable (verified via git grep). The regression tests likewise depend on hookDisposeLockPath and the claim-takeover semantics absent on stable, so the change cannot apply cleanly or meaningfully there.
To override, re-run the Backport to stable workflow manually via workflow_dispatch and paste this commit SHA into the ref input:
Address #2808 review:
- The claimant loop now force-deletes a claim file that persists but never
parses (after a few observations) so a corrupt/orphan claim can't block its
token forever — the releaser correctly leaves such files alone, so nothing
else reaped them. A live hook's claim is still rebuilt from the event log, so
reaping a corrupt claim can't steal a token from a live hook.
- hook_created now builds the claim path via hookTokenClaimPath() instead of
inline, so the layout can't drift.
- Drop the 20-round resume-vs-disposal soak test: both orderings are valid so
it passed even with the fix reverted (a soak, not a regression guard). The
deterministic mid-teardown test remains the real guard.
Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Add this suggestion to a batch that can be applied as a single commit.This suggestion is invalid because no changes were made to the code.Suggestions cannot be applied while the pull request is closed.Suggestions cannot be applied while viewing a subset of changes.Only one suggestion per line can be applied in a batch.Add this suggestion to a batch that can be applied as a single commit.Applying suggestions on deleted lines is not supported.You must change the existing code in this line in order to create a valid suggestion.Outdated suggestions cannot be applied.This suggestion has been applied or marked resolved.Suggestions cannot be applied from pending reviews.Suggestions cannot be applied on multi-line comments.Suggestions cannot be applied while the pull request is queued to merge.Suggestion cannot be applied right now. Please check back later.
Closes#2781. Also addresses the post-merge feedback on #2779 (guarded claim release).
1.
hook_receivedjournaled afterhook_disposed(#2781)A
hook_receivedwrite only validated that the hook entity exists, and was not serialized against thehook_disposedwrite for the same hook. A resume that passed its existence check before the disposal began could be journaled afterhook_disposed— once that order is in the log, every replay of the owning run diverges at the post-disposalhook_receivedand the run escalates toCorruptedEventLogError.hook_receivedandhook_disposednow share the per-hook in-process lock previously taken only byhook_created, making the resume's "validate, then append" atomic with the disposer's "dispose lock → entity delete → append" within a storage instance.hook_receivedwrite re-validates the durable dispose lock from [core] Fix hook token reuse after dispose() (same-run and cross-run) #2779 (at acceptance, and once more immediately before the event append, narrowing the cross-instance window to the single event write), rejecting withHookNotFoundError— the same surface as a resume that arrives after teardown finished. Acceptance now observes the same order replay will see.2. Guarded token claim release (#2779 follow-up)
Both in-flight releasers (the
hook_disposedhandler and the terminal-rundeleteAllHooksForRuncleanup) deleted the token claim file unconditionally after reading the hook entity. A releaser stalled between those operations could outlive a claimant force-releasing its stale claim, and its deferred delete would then destroy the new claimant's live claim — transiently breaking token uniqueness. Releasers now re-read the claim and delete it only if it still points at their own(runId, hookId); a claim owned by someone else is left for the claimant-side force-release path to reap.Tests
hook_receivedis never journaled afterhook_disposedand that a rejected resume leaves no event behind.(runId, hookId)and assert the release skips it; both fail without fix 2.🤖 Generated with Claude Code