Skip to content

fix(server): serialize chokidar watcher batches to prevent heap growth - #1300

Open
Schrotti77 wants to merge 1 commit into
inkeep:mainfrom
Schrotti77:fix/chokidar-batch-serialization
Open

fix(server): serialize chokidar watcher batches to prevent heap growth#1300
Schrotti77 wants to merge 1 commit into
inkeep:mainfrom
Schrotti77:fix/chokidar-batch-serialization

Conversation

@Schrotti77

Copy link
Copy Markdown

Problem

On Windows the OK server uses the chokidar watcher fallback (@parcel/watcher crashes with STATUS_STACK_BUFFER_OVERRUN on long/non-ASCII paths, see #1207). chokidar (ReadDirectoryChangesW) emits more granular events than inotify, and each 50ms batch calls handleRawEvents, which readFile()s every changed file.

When a batch is still mid-drain (slow disk, many files, active sync/backup tool) and the next 50ms timer fires, the new batch launches independently — overlapping async work piles up and the Node heap grows without bound on event storms.

Fix

Serialize batches by chaining them on an inFlight promise in startChokidarWatcher. Batches drain strictly one at a time; a batch that lands while another is draining waits its turn. The .catch remains load-bearing: it keeps the chain alive after an error so later batches don't deadlock.

Regression test

file-watcher-chokidar-fallback.test.ts — new describe "chokidar backend — batch serialization":

  • onDiskEvent counts active drains (active++, hold 200ms, active--), tracks maxActive
  • write to existing file → wait until active === 1 (batch 1 draining)
  • write again → batch 2 lands mid-drain
  • wait 600ms, expect(maxActive).toBe(1)

Verified:

  • Without fix: test fails (maxActive reaches 2 — batches overlap)
  • With fix: 11/11 tests pass, typecheck clean

Tested with forceBackend: 'chokidar' on Linux — no Windows needed to exercise the path.

chokidar (Windows ReadDirectoryChangesW) emits more granular events than
inotify, so a 50ms batch can still be mid-drain (readFile-ing every change)
when the next timer fires. Launching batches independently piles up
overlapping async work → unbounded heap growth on event storms.

Chain batches on an inFlight promise so they drain strictly one at a time.
The .catch stays load-bearing: it keeps the chain alive after an error.

Regression test: maxActive must stay 1 when a second batch lands mid-drain.
Fails without the fix (maxActive=2), passes with it.
@Schrotti77
Schrotti77 deployed to inkeep-oss-sync August 20, 2026 09:09 — with GitHub Actions Active
@github-actions

Copy link
Copy Markdown

Thanks for the contribution!

What happens next:

  • A maintainer will review your PR.
  • If you don't hear back within a few business days, please comment here to nudge our team.
  • This repository is maintained through an internal mirror. When your change is accepted, this PR will close automatically. Don't be alarmed when it closes — that's how it merges, and your authorship is preserved.

@CLAassistant

Copy link
Copy Markdown

CLA assistant check
Thank you for your submission! We really appreciate it. Like many open source projects, we ask that you sign our Contributor License Agreement before we can accept your contribution.


Hermes (Marian's AI Agent) seems not to be a GitHub user. You need a GitHub account to be able to sign the CLA. If you have already a GitHub account, please add the email address used for this commit to your account.
You have signed the CLA already but the status is still pending? Let us recheck it.

@robert-inkeep robert-inkeep self-assigned this Aug 22, 2026
@robert-inkeep

Copy link
Copy Markdown
Contributor

hi @Schrotti77 could you follow the @CLAassistant instructions? and i'll see this through

inkeep-oss-sync Bot pushed a commit that referenced this pull request Aug 24, 2026
* feat: relay public OK pull requests into Linear tickets

Linear mirrors public GitHub issues into PRD tickets but not pull
requests, and it does not follow GitHub's own issue-to-PR link either:
PR #623 says "Closes #622" and is still absent from PRD-7349, the ticket
that #622 created. So community pull requests were invisible in Linear,
including the six of eight open ones that never crossed the contributor
bridge into agents-private.

The public repo relays a bare PR number here and this repo does the
Linear work with its own key. Every workflow on the public repo that
reads LINEAR_API_KEY today fires only on schedule, push or *_dispatch,
so nothing fork-reachable has ever held that key. Filing a ticket means
reading a contributor-controlled PR title and body, so the relay job
holds no Linear secret and checks out no code at all. The only thing
crossing the trust boundary is an integer, validated on both sides.

Creating a ticket is the last branch of a pure resolver, reachable only
when three lookups come back empty: an existing attachment on the PR
URL, a Linear ticket mirroring an issue the PR closes, and a PRD
identifier carried by branch, title or body. A bare cross-reference is
deliberately not a dedupe signal, so PR #1300 mentioning "see #1207"
does not attach it to an unrelated bug.

Dry run against live GitHub and Linear: #1177 resolves to PRD-7971,
#623 to PRD-7349, #345 to the hand-filed PRD-7321, and the six genuinely
untracked open PRs resolve to create.

The shared Linear transport moves to scripts/linear-graphql.mjs rather
than becoming a third private copy of the same fetch wrapper;
origin-link.mjs now uses it and its 25 tests still pass.

PR_LINEAR_SYNC_ARMED is "false", so nothing is written to Linear until
someone flips one line.

* fix: address Pullfrog review on the public PR Linear relay

Three real findings, all confirmed by execution before fixing.

Carrier lookup trusted contributor prose. extractIdentifiers was fed
headRef + title + body, but origin-link.mjs declares CARRIERS as branch,
title and attachment, so the docblock claim that the two agree was false
for the body. The consequence was worse than the inaccuracy: a fork PR
body containing "PRD-1234" would both suppress the ticket this workflow
exists to file and choose which internal ticket receives an attachment,
which is the same mention-as-a-claim shape the header rejects for
"see #1207". The body is dropped from the carrier text, matching
origin-link.mjs exactly, and the docblock now says what is actually true.

A missing Linear key swept silently. main() returned 0 when
LINEAR_API_KEY was absent, so a rotated secret would report the whole
sweep green having evaluated nothing. Unlike origin-link.mjs, which
still does its real work without Linear, every lookup here queries
Linear, so the key is now required and the job fails loud.

The shell integer check was weaker than the layer behind it. grep -qE
anchors per line, so a two-line payload whose first line is digits
passed it. Verified: printf '1379\nevil' passes the grep and is rejected
by the node regex, so nothing was exploitable, but a defence-in-depth
layer that is weaker than the one it backs is not one. Switched to the
bash [[ =~ ]] form, which anchors against the whole string.

The body-exclusion guard was mutation tested: reintroducing pr.body
turns the new test red, so it pins the property rather than merely
passing alongside it.

52 tests pass, actionlint is clean, and the live dry run is unchanged:
1177 to PRD-7971, 623 to PRD-7349, 345 to PRD-7321, six untracked PRs
to create.

* fix: close the duplicate and write-primitive paths found in review

Two majors from the claude review, plus the minors and considers.

Create and attach were not atomic. issueCreate followed by
attachmentCreate are two calls with no transaction, so a failure between
them left a ticket carrying none of the dedupe signals, and the next
sweep would file a genuine duplicate. That is the one invariant this
script exists to hold. Three changes: the created ticket is now logged
BEFORE the attach so the identifier survives on the failing path, the
attach is wrapped so the error names the orphan, and a third lookup
searches for a ticket carrying this PR's "(community PR #N)" title
suffix before creating. An interrupted run is now self healing: the next
run finds the orphan and attaches, so lookup 1 sees it from then on.
Multi target attaches also collect failures instead of throwing on the
first, because stopping early left later targets unattached while lookup
1 short circuited past them on the retry.

The carrier lookup is removed entirely. Scanning contributor text for
PRD-#### is right for internal PRs and wrong across this boundary.
Identifiers are not secret: AGENTS.md requires one in every PR title and
Copybara mirrors those subjects into the public repo's history, so any
contributor could both write attacker shaped text onto an internal
ticket of their choosing and suppress their own PR's triage ticket by
naming any resolvable one. The two remaining signals are GitHub
authoritative and covered every case in the dry run. Attachment titles
are now "PR #N" alone for the same reason: an attachment renders as
first party automation output, so its text must not be contributor
shaped.

Minors: the relay token is scoped to permission-contents write, since
POST /dispatches needs only that and the installation otherwise carries
workflows write on the private monorepo; resolveSyncContext now warns
when the label or project lookup misses, which previously degraded every
ticket in a run while still reporting success; and scripts/linear-graphql
gets the unit tests its own docstring justified, covering the swallowed
non 2xx body and the 200 carrying a GraphQL errors array.

Considers: every fetch now carries an AbortSignal timeout so one hung
call cannot consume the sweep's budget, CI.md documents both workflows
and LINEAR_API_KEY, and the create test asserts the ticket description
rather than only its metadata.

910 root script tests pass, actionlint is clean, and the live dry run is
unchanged: 1177 to PRD-7971, 623 to PRD-7349, 345 to PRD-7321, six
untracked PRs to create. Those six also exercised the new orphan query
against the real Linear API.

* fix: identify Copybara sync PRs by head repo, not branch name

A fork picks its own branch name, so keying the skip on the string
copybara/sync let any contributor name their branch that and drop out of
both the relay and the receiver's sweep filter, leaving their PR
permanently invisible to Linear. That is evasion of the tracking control
this workflow exists to be. "This repo talking to itself" is a property
of the head repository, which a stranger cannot forge.

The relay now also requires head.repo.full_name to equal the repository
before skipping, and the sweep excludes copybara/sync only for same-repo
PRs via isCrossRepository. Verified against the four cases: a real
same-repo copybara/sync PR is still excluded, a fork branch named
copybara/sync is now included, and both ordinary shapes are unaffected.

Adds a workflow-shape test file. Every property that keeps the Linear
key off a fork-reachable trigger is one line of YAML that a future edit
could revert without touching a test: no checkout in the relay, no
Linear credential in the relay, the scoped app token, the env-not-
interpolation payload, both provenance checks, the disarmed default, the
whole-string integer check, and the serialized concurrency group. The
no-checkout guard was mutation tested by adding a checkout, which turns
it red.

The absence assertions strip whole-line comments first, because both
workflows explain their own posture in prose and would otherwise match
the comment saying why the thing is absent.

919 root script tests pass, actionlint clean, subtree md-audit clean.

GitOrigin-RevId: 50dc33f876258793dd23d160914ce5f0267ab89f
inkeep-oss-sync Bot pushed a commit that referenced this pull request Aug 24, 2026
* feat: relay public OK pull requests into Linear tickets

Linear mirrors public GitHub issues into PRD tickets but not pull
requests, and it does not follow GitHub's own issue-to-PR link either:
PR #623 says "Closes #622" and is still absent from PRD-7349, the ticket
that #622 created. So community pull requests were invisible in Linear,
including the six of eight open ones that never crossed the contributor
bridge into agents-private.

The public repo relays a bare PR number here and this repo does the
Linear work with its own key. Every workflow on the public repo that
reads LINEAR_API_KEY today fires only on schedule, push or *_dispatch,
so nothing fork-reachable has ever held that key. Filing a ticket means
reading a contributor-controlled PR title and body, so the relay job
holds no Linear secret and checks out no code at all. The only thing
crossing the trust boundary is an integer, validated on both sides.

Creating a ticket is the last branch of a pure resolver, reachable only
when three lookups come back empty: an existing attachment on the PR
URL, a Linear ticket mirroring an issue the PR closes, and a PRD
identifier carried by branch, title or body. A bare cross-reference is
deliberately not a dedupe signal, so PR #1300 mentioning "see #1207"
does not attach it to an unrelated bug.

Dry run against live GitHub and Linear: #1177 resolves to PRD-7971,
#623 to PRD-7349, #345 to the hand-filed PRD-7321, and the six genuinely
untracked open PRs resolve to create.

The shared Linear transport moves to scripts/linear-graphql.mjs rather
than becoming a third private copy of the same fetch wrapper;
origin-link.mjs now uses it and its 25 tests still pass.

PR_LINEAR_SYNC_ARMED is "false", so nothing is written to Linear until
someone flips one line.

* fix: address Pullfrog review on the public PR Linear relay

Three real findings, all confirmed by execution before fixing.

Carrier lookup trusted contributor prose. extractIdentifiers was fed
headRef + title + body, but origin-link.mjs declares CARRIERS as branch,
title and attachment, so the docblock claim that the two agree was false
for the body. The consequence was worse than the inaccuracy: a fork PR
body containing "PRD-1234" would both suppress the ticket this workflow
exists to file and choose which internal ticket receives an attachment,
which is the same mention-as-a-claim shape the header rejects for
"see #1207". The body is dropped from the carrier text, matching
origin-link.mjs exactly, and the docblock now says what is actually true.

A missing Linear key swept silently. main() returned 0 when
LINEAR_API_KEY was absent, so a rotated secret would report the whole
sweep green having evaluated nothing. Unlike origin-link.mjs, which
still does its real work without Linear, every lookup here queries
Linear, so the key is now required and the job fails loud.

The shell integer check was weaker than the layer behind it. grep -qE
anchors per line, so a two-line payload whose first line is digits
passed it. Verified: printf '1379\nevil' passes the grep and is rejected
by the node regex, so nothing was exploitable, but a defence-in-depth
layer that is weaker than the one it backs is not one. Switched to the
bash [[ =~ ]] form, which anchors against the whole string.

The body-exclusion guard was mutation tested: reintroducing pr.body
turns the new test red, so it pins the property rather than merely
passing alongside it.

52 tests pass, actionlint is clean, and the live dry run is unchanged:
1177 to PRD-7971, 623 to PRD-7349, 345 to PRD-7321, six untracked PRs
to create.

* fix: close the duplicate and write-primitive paths found in review

Two majors from the claude review, plus the minors and considers.

Create and attach were not atomic. issueCreate followed by
attachmentCreate are two calls with no transaction, so a failure between
them left a ticket carrying none of the dedupe signals, and the next
sweep would file a genuine duplicate. That is the one invariant this
script exists to hold. Three changes: the created ticket is now logged
BEFORE the attach so the identifier survives on the failing path, the
attach is wrapped so the error names the orphan, and a third lookup
searches for a ticket carrying this PR's "(community PR #N)" title
suffix before creating. An interrupted run is now self healing: the next
run finds the orphan and attaches, so lookup 1 sees it from then on.
Multi target attaches also collect failures instead of throwing on the
first, because stopping early left later targets unattached while lookup
1 short circuited past them on the retry.

The carrier lookup is removed entirely. Scanning contributor text for
PRD-#### is right for internal PRs and wrong across this boundary.
Identifiers are not secret: AGENTS.md requires one in every PR title and
Copybara mirrors those subjects into the public repo's history, so any
contributor could both write attacker shaped text onto an internal
ticket of their choosing and suppress their own PR's triage ticket by
naming any resolvable one. The two remaining signals are GitHub
authoritative and covered every case in the dry run. Attachment titles
are now "PR #N" alone for the same reason: an attachment renders as
first party automation output, so its text must not be contributor
shaped.

Minors: the relay token is scoped to permission-contents write, since
POST /dispatches needs only that and the installation otherwise carries
workflows write on the private monorepo; resolveSyncContext now warns
when the label or project lookup misses, which previously degraded every
ticket in a run while still reporting success; and scripts/linear-graphql
gets the unit tests its own docstring justified, covering the swallowed
non 2xx body and the 200 carrying a GraphQL errors array.

Considers: every fetch now carries an AbortSignal timeout so one hung
call cannot consume the sweep's budget, CI.md documents both workflows
and LINEAR_API_KEY, and the create test asserts the ticket description
rather than only its metadata.

910 root script tests pass, actionlint is clean, and the live dry run is
unchanged: 1177 to PRD-7971, 623 to PRD-7349, 345 to PRD-7321, six
untracked PRs to create. Those six also exercised the new orphan query
against the real Linear API.

* fix: identify Copybara sync PRs by head repo, not branch name

A fork picks its own branch name, so keying the skip on the string
copybara/sync let any contributor name their branch that and drop out of
both the relay and the receiver's sweep filter, leaving their PR
permanently invisible to Linear. That is evasion of the tracking control
this workflow exists to be. "This repo talking to itself" is a property
of the head repository, which a stranger cannot forge.

The relay now also requires head.repo.full_name to equal the repository
before skipping, and the sweep excludes copybara/sync only for same-repo
PRs via isCrossRepository. Verified against the four cases: a real
same-repo copybara/sync PR is still excluded, a fork branch named
copybara/sync is now included, and both ordinary shapes are unaffected.

Adds a workflow-shape test file. Every property that keeps the Linear
key off a fork-reachable trigger is one line of YAML that a future edit
could revert without touching a test: no checkout in the relay, no
Linear credential in the relay, the scoped app token, the env-not-
interpolation payload, both provenance checks, the disarmed default, the
whole-string integer check, and the serialized concurrency group. The
no-checkout guard was mutation tested by adding a checkout, which turns
it red.

The absence assertions strip whole-line comments first, because both
workflows explain their own posture in prose and would otherwise match
the comment saying why the thing is absent.

919 root script tests pass, actionlint clean, subtree md-audit clean.

GitOrigin-RevId: 50dc33f876258793dd23d160914ce5f0267ab89f
Sign up for free to 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.

3 participants