The debounced auto-cookie checkpoint had two gaps. First, the trigger set
was a hand-maintained list that missed the actual cookie-mutating commands
(cookie, cookie-import, load-html) while listing a non-existent set-cookie.
Drive the decision off WRITE_COMMANDS (the real mutator registry) plus a
small extra set for js/eval and the meta commands (chain, state, newtab,
tab-each) that can change the cookie jar. Second, the schedule call ran
before dispatch, so a slow navigation or login redirect snapshotted the
pre-command jar; move it to after the command attempt (success and error
paths, plus the scoped-newtab early return) so the debounce captures the
post-mutation state.
Also wire cookie-picker import/remove: those flow through the /cookie-picker
route, not /command, so they never scheduled a save. Pass
scheduleAutoCookieCheckpoint as an onCookieMutation callback and fire it
after addCookies / removal.
Serialize checkpoints with an in-flight promise guard so an overlapping
periodic + debounced + shutdown checkpoint can't race two concurrent
saveAutoCookieState writes onto the same slot. The shutdown flush awaits
any in-flight checkpoint first, then runs its own, so no save is lost.
Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
reclaimIfStale treated any live pid matching the recorded owner as proof
the lock is still held. After a crash the OS can recycle that pid for an
unrelated process, so a liveness check alone would refuse to reclaim a
lock whose real owner is long gone, permanently disabling auto-persistence
for the workspace. Cross-check the owner's recorded lock-acquire time
against the live process start time via `ps -o lstart=`: if the live
process started after the lock was taken, the pid was reused and the lock
is stale. Fallback is conservative — if start-time can't be read (ps
fails, timeout, non-Unix, unparseable), we keep the lock rather than
risk stealing an active peer's slot.
Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
filterPersistableCookies trusted expires as a finite number and matched
internal-network domains case-sensitively. A cookie with expires=NaN or
Infinity slipped past the `=== -1` session check (NaN <= nowSec is false),
and an uppercase `LOCALHOST` / `Foo.INTERNAL` bypassed the internal-domain
block. Add a `Number.isFinite` guard and a shared `normalizeCookieDomain`
that lower-cases before the internal-domain comparison. Allowlist matching
already lower-cases internally, so behavior there is unchanged.
Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
- Unit: opt-in gate, persistent-only/expiry/internal-network/allowlist filter,
order-independent content hash, atomic-write helper, mkdir-atomic lock
(acquire / same-pid contend / dead-pid reclaim / release).
- Real Playwright round-trip: a persistent __Host- cookie saves and reloads via
newContext({ storageState }); session/expired cookies are excluded; an
unchanged set skips the second write.
- Real BrowserManager.launch() integration: a pre-seeded auto-state file is
restored into the launched context (simulates a daemon restart).
- Static wiring tripwires: save gated on the opt-in flag, shutdown flush runs
before the browser closes, and the crash path never checkpoints.
Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>