Skip to content

cache: strict cross-process gate for the warm session-cache refresh transaction#743

Merged
iamtoruk merged 3 commits into
getagentseal:mainfrom
avs-io:fix/645-warm-refresh-lock
Jul 20, 2026
Merged

cache: strict cross-process gate for the warm session-cache refresh transaction#743
iamtoruk merged 3 commits into
getagentseal:mainfrom
avs-io:fix/645-warm-refresh-lock

Conversation

@avs-io

@avs-io avs-io commented Jul 18, 2026

Copy link
Copy Markdown
Member

Draft on purpose: per the commitment in #645, this stays a draft until you give the go-ahead — it's here so the code is reviewable whenever bandwidth exists, not to jump the queue.

Implements the revised design (third comment on #645): first slice, warm session-cache transaction only.

What changed

  • hydrating.lock is untouched — still cold-only, advisory, fail-open.
  • New src/cache-refresh-lock.ts: a strict cross-process gate (session-refresh.lock) around the warm read→reconcile→parse→save transaction. Four explicit outcomes: acquired, completed-by-other (reload the now-fresh cache), timed-out / unavailable (serve the prior complete snapshot read-only — a correctness lock never fails open into mutation). Exclusive-create + random ownership token, 10s heartbeat, 90s staleness with a takeover-guard sidecar that never steals a heartbeating owner, in-process single-flight ahead of the filesystem lock, monotonic-clock wait deadlines, bounded Windows EPERM/EBUSY retries.
  • Publication fence: saveCache re-verifies ownership under the takeover guard immediately before the payload rename and aborts publication if displaced — a suspended/clock-jumped owner can no longer overwrite its successor's work.
  • Waiter budget is 30s, comfortably under the menubar's 45s subprocess timeout; a timed-out waiter serves the previous complete snapshot and never invalidates the holder.
  • Read-only serve path threads through parseAllSessions so timeout/stale serving performs no cache mutation at all.

How tested

  • Red-before: tests/cache-refresh-lock-process.test.ts is a real child-process lost-update regression — two warm processes parse disjoint changed files; with the lock wiring stashed it fails on stock code (later save drops the other's update), with the wiring it passes. Verified both directions locally.
  • Unit suite with injected clock/poll: every acquisition outcome, single-flight, heartbeat-prevents-takeover, exactly-one-takeover-winner, fence aborts a displaced save, timed-out waiter leaves the holder intact, token-scoped release.
  • Full suite: 2162 passed with dist built (the app/electron/cli.test.ts fresh-clone failures are app/electron/cli.test.ts fails on a fresh clone: dev-resolution tests require a built dist/cli.js #681, pre-existing).

Follow-ups per the design: daily-cache cross-process transaction (lock order daily → session), then #703's incremental parse runs inside this gate (draft PR opening alongside, stacked on this branch).

reviewer added 2 commits July 20, 2026 18:03
verifyStillOwner and the heartbeat tick both take the takeover guard;
without in-process serialization the fence could observe its own
heartbeat's guard file, read it as displacement, and abort a legitimate
publication. Fail-safe, but it discarded the parse the lock exists to
protect (~6% of verifies at a 1ms heartbeat in the repro). Owner-side
guard operations now run through one serializer; cross-process guard
semantics are unchanged. Regression test at heartbeatMs 1, mutation-
verified against the unserialized code.
@iamtoruk
iamtoruk marked this pull request as ready for review July 20, 2026 16:05
@iamtoruk

Copy link
Copy Markdown
Member

Full adversarial review before merge: spec-fit against the #645 revised design confirmed, all four outcomes exercised, and every concurrency attack survived without corruption or lost updates: SIGKILL mid-transaction, SIGSTOP past staleness with a resumed zombie fenced out of publishing over its successor, racing warm refreshes with exactly one parser, and a real 30s timeout serving the prior snapshot with provably zero cache writes. Both mutations (neutered heartbeat, disabled fence) are caught by tests. Real-data drive: two concurrent runs on a copy of a 109MB cache produced byte-identical totals.

Two amendments pushed: (1) the publication fence could race the owner's OWN heartbeat for the takeover guard and abort a legitimate save (reproduced 196/3000 at a 1ms heartbeat; fail-safe but it discards the parse this lock exists to protect) — owner-side guard operations now run through an in-process serializer, with a regression test at heartbeatMs 1 that fails against the unserialized code; (2) merged current main, resolving the readOnly gate against the incremental-append reconcile so readOnly never parses changed files while the append optimization is preserved, verified by both suites. The missing staleness marker on readOnly serves is accepted as bounded and self-healing for this release and tracked in the freshness-marker issue. Windows retry paths reviewed statically only; unverified on Windows.

@iamtoruk
iamtoruk merged commit e27fe36 into getagentseal:main Jul 20, 2026
3 checks passed
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.

2 participants