Skip to content

fix: tolerate transient Windows file-lock errors and harden passive hooks - #6

Open
BoYanZh wants to merge 1 commit into
prevalentWare:mainfrom
BoYanZh:fix/windows-lock-eperm
Open

BoYanZh wants to merge 1 commit into
prevalentWare:mainfrom
BoYanZh:fix/windows-lock-eperm

Conversation

@BoYanZh

@BoYanZh BoYanZh commented Oct 17, 2026 •

Copy link
Copy Markdown

Problem

On Windows, normal dialogue occasionally fails with EPERM: operation not permitted, open '.../loops.json.lock'.

The cross-process lock in src/state.ts assumed POSIX semantics: any non-EEXIST error from link/readFile on the lock file was fatal. On Windows, benign handoffs (delete-pending handles, antivirus scans) surface as EPERM/EBUSY/EACCES/ENOTEMPTY on open, and every chat.message / system.transform / event plus the 30s lease heartbeat acquires the session lease — so the race is hit regularly. Only ENOENT was tolerated, so the transient error propagated into user-visible dialogue failures.

A second, latent bug: the V2 event consumer loop has no per-event handling, so a single failing event kills the whole consumer and silently disables all future event-driven scheduling until process restart.

Evidence from a real machine: orphaned loops.json.lock.owner.<pid>-* and loops.json.<pid>.ts.tmp files from dead processes, plus live multi-context contender files — confirming ongoing lock contention.

Approach

  • src/state.ts: retry transient lock errors briefly (5s budget) on owner-create/link/read/release instead of throwing; genuine contention still waits on the EEXIST path with the 30s deadline. Lock release() is now best-effort so a release failure can never turn a successful mutation into a user-visible error. Opportunistically sweep dead-pid lock.owner.* orphans on acquisition (live contenders are never touched).
  • src/server.ts: passive V1 hooks (chat.message, system.transform, compacting, event) and V2 hooks (context, event consumer) log and continue instead of breaking dialogue. Tool paths still surface errors so the agent can retry. The V2 consumer now catches per event.
  • dist/server.js rebuilt per CONTRIBUTING step 5.

Verification

  • bun test: 106 pass (102 baseline + 4 new), 0 fail.
  • New regression tests: isTransientLockError classification table; V1 passive hooks + V2 context hook resolve (not throw) when state is unusable; V2 consumer keeps processing after a failing event. Confirmed the new tests fail on unfixed code (4 fail + 1 import error) and pass with the fix.
  • bun run lint, bun run typecheck, bun run build, pack:dry-run all green.
  • Also verified on Windows with a 50-way concurrent-mutation stress run (correct counts, no lock leftovers) and the original APPDATA failure case (state file valid, orphans cleaned).

View with [code]smith Autofix with [code]smith
Need help on this PR? Tag @codesmith-bot with what you need.

…ooks

The cross-process lock in state.ts assumed POSIX semantics: any
non-EEXIST error from link/readFile on loops.json.lock was fatal.
On Windows, benign handoffs (delete-pending handles, antivirus
scans) surface as EPERM/EBUSY/EACCES/ENOTEMPTY on open, so normal
dialogue would occasionally fail with EPERM on loops.json.lock:
every chat.message / system.transform / event acquires the session
lease, and the lease heartbeat does it every 30s.

- Retry transient lock errors briefly (5s budget) instead of
  throwing; genuine contention still waits on the EEXIST path.
- Make lock release best-effort so a release failure can never turn
  a successful mutation into a user-visible error; sweep dead-pid
  lock.owner.* orphans on acquisition.
- Passive V1 hooks (chat.message, system.transform, compacting,
  event) and V2 hooks (context, event consumer) log and continue
  instead of breaking dialogue. The V2 consumer previously died
  permanently on a single bad event.

Verified with: bun test (106 pass, 4 new), lint, typecheck, build,
plus a concurrent-mutation stress run and the original Windows
APPDATA failure case.
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.

1 participant