Files
PaperClipAI/server
DottaandClaude Opus 5.5 51c352ec2e fix(skills): release a runtime skill-cache lock whose holder provably exited (#19849)
## Thinking Path

> - Paperclip is the open source app people use to manage AI agents for
work
> - Before a run starts, the server builds a read-only cache of each
company skill under a cross-process file lock
> - The lock records only a pid and a hostname, so a crash leaves a lock
that nothing can release
> - If the holder pid is dead on the same host, the next run fails and
asks for manual cleanup. If the host name differs, the next run waits 60
s and then fails
> - This is on the turn path, so one crash during a skill build breaks
later runs that use that skill
> - This pull request records the shared controller process identity in
the lock and releases the lock at once when `controllerProcessExited`
proves the holder is gone
> - The benefit is that a crash during a skill build no longer blocks
later runs, and the release never relies on lock age or another host

## Linked Issues or Issue Description

**Bug description**
The Phase 0 ownership inventory (finding 4) lists the runtime
skill-cache lock (`runtime-skill-cache.ts`) as a record that nothing
releases automatically. When a publisher crashes while it holds
`<skillId>.lock`, every later preparation of that skill fails.

**Steps to reproduce**
1. Start a skill cache build, then kill the server process while it
holds the lock.
2. Restart and start a run that uses the same skill.
3. Preparation fails with "Runtime skill cache publisher exited; remove
its stale publication lock before retrying". For a holder whose host
name differs, it fails after 60 s with "publisher is busy".

**Expected behavior**
A lock whose holder has provably exited is released, and the build
continues without waiting. A holder that cannot be proven gone is still
waited for.

Refs #19846

## What Changed

- `controller-process-identity.ts`: add
`currentControllerProcessStamp()`. It returns this process's identity
with no I/O, so the turn path gets no new wait.
- `runtime-skill-cache.ts`: the lock owner file now also stores
`controller` (boot id, pid, PID-space hash, start time if already
known). `pid` and `host` stay so older readers still work.
- New `runtimeSkillCacheLockHolderExited`: true only when
`controllerProcessExited` proves the holder gone. Another host or PID
namespace, an unknown space, a legacy lock without `controller`, a live
pid, and this process all return false.
- On proof, waiters race for one exclusive claim named by the stale
holder's unique token (`<lock>.released-<token>`). Only the winner
unlinks the lock, and only while it still has the stale inode. The claim
is kept, so a late waiter does nothing. The waiter then retries the link
right away instead of after the 20 ms poll.
- A live holder is re-proved at most once a second.
`controllerProcessExited` skips the start-time probe (`ps` on macOS)
when no start time was recorded.
- The busy error now names the lock path. The same-host "publisher
exited" hard failure is removed.

## Verification

- `npx vitest run src/__tests__/runtime-skill-cache.test.ts
src/services/controller-process-identity.test.ts`: 28 passed, rebased on
master after #19848.
- New case "takes over a release claim whose claimer exited before
removing the stale lock" covers the second Greptile finding.
- New case "never removes a newer lock for a waiter that read a stale
one late" covers the Greptile P1.
- New case "releases a publication lock at once when its holder provably
exited". On the old code it fails with "Runtime skill cache publisher
exited; remove its stale publication lock before retrying". On the new
code it builds in well under 5 s and leaves no lock.
- New case "never treats age, another host, or a legacy holder as
proof": a different host hash, a legacy `{pid, host}` lock, a live pid,
this process, and malformed content all stay held.
- Server `tsc --noEmit` shows no errors in the changed files.

## Risks

- Previous-release story: a lock written by the previous release has no
`controller` field. It is unprovable. The waiter waits 60 s and fails as
before, and the error now names the path to remove. A new lock still
carries `pid` and `host`, so a previous-release reader behaves as
before.
- Lock release race: while a stale lock exists, only the single claim
winner can remove it, and the inode check cannot match a newer lock
because the stale holder's owner file stays and pins that inode. A claim
whose claimer crashed is itself released by the same proof (depth
bounded at 2).
- Each released lock leaves one small claim file and the dead holder's
owner file next to it.
- No new I/O before `turn.start`. The stamp is synchronous. The proof
runs only when the lock is contended.

## Model Used

- Claude Opus 5.5 (`claude-opus-5-5`), Anthropic, used with tool use and
code execution in Claude Code.

## Checklist

- [x] I have included a thinking path that traces from project context
to this change
- [x] I have specified the model used (with version and capability
details)
- [x] I have checked ROADMAP.md and confirmed this PR does not duplicate
planned core work
- [x] I have searched GitHub for duplicate or related PRs and linked
them above
- [x] I have either (a) linked existing issues with `Fixes: #` / `Closes
#` / `Refs #` OR (b) described the issue in-PR following the relevant
issue template
- [x] I have not referenced internal/instance-local Paperclip issues or
links (only public GitHub `#NNN` / `github.com/paperclipai/paperclip`
URLs)
- [x] My branch name describes the change (e.g. `docs/...`, `fix/...`)
and contains no internal Paperclip ticket id or instance-derived details
- [x] I have run tests locally and they pass
- [x] I have added or updated tests where applicable
- [x] I have updated relevant documentation to reflect my changes
- [x] I have considered and documented any risks above
- [ ] All Paperclip CI gates are green
- [ ] Greptile is 5/5 with no open P2s, recommendations, or follow-ups
- [x] I will address all Greptile and reviewer comments before
requesting merge

🤖 Generated with [Claude Code](https://claude.com/claude-code)

---------

Co-authored-by: Claude Opus 5.5 <noreply@anthropic.com>
2026-11-07 11:51:25 -06:00
..