mirror of
https://github.com/paperclipai/paperclip.git
synced 2026-10-06 10:48:12 +02:00
## Thinking Path > - Paperclip is the open source app people use to manage AI agents for work > - A workspace can run a shared local service, such as a dev server, on an automatic port > - Paperclip adopts a live service again after it loses the runtime registry state > - Paperclip must first prove that the port owner runs inside the workspace > - Linux reads the process working directory from `/proc/<pid>/cwd` > - macOS has no `/proc`, so the check returned `null` and adoption always failed > - This pull request reads the process working directory with `lsof` on macOS > - The benefit is that macOS keeps a healthy live service after startup reconciliation, instead of recording it as stopped ## Linked Issues or Issue Description Closes #9911. That pull request reports the same defect and was opened first, on 2026-07-20. Its checks have been red since that day, because its inline issue description does not use the label format the gate parses. It has had no author activity since. This pull request keeps that author's test-fixture commit, with the author unchanged, and adds NUL-delimited parsing, adoption-boundary tests, and fail-closed Darwin registry handling. Maintainers may prefer to land #9911 instead. I will close this one again if they do. This pull request replaces #11600, which I closed earlier as a duplicate. It carries the same work, rebased onto current `master`, with the review feedback from that pull request applied. No public issue exists. The problem follows. **What happened?** On macOS, `readLocalServiceProcessCwd` returned `null`. Startup reconciliation found a live port owner, but it could not verify the working directory. It rejected the candidate and recorded the live service as stopped. **Expected behavior** Paperclip adopts a healthy port owner when the working directory is inside the requested workspace. Paperclip rejects the process when the working directory is outside the workspace, or when it cannot be read. **Steps to reproduce** 1. Build Paperclip from source on macOS. 2. Start a shared workspace runtime service on an automatic port. 3. Remove the runtime registry state while the service stays alive. 4. Run startup reconciliation. 5. Read the result. Unpatched `master` reports `adopted: 0` and `stopped: 1`. **Paperclip version or commit** This branch is based on `master` at `7c8064da1b35527865c1d523c9f0016e304ae46d`. **Deployment mode** Local development from source. **Installation method** Built from source with pnpm. **Operating system** macOS 26.4, Darwin 25.4.0, arm64. **Node.js version** Node.js 22.22.2 on macOS. Node.js 24.19.0 on Linux. pnpm 9.15.4. ## Darwin Registry Adoption Now Fails Closed This pull request changes one existing Darwin registry-adoption behavior in addition to enabling port-owner adoption. Before this change, `readLocalServiceProcessCwd` always returned `null` on Darwin. `isLocalServiceRegistryCwdCompatible` treated a null cwd as compatible on every non-Linux platform, so a service with an existing registry record could still be adopted when its port owner, process group, and command matched, even though Paperclip had not verified the process's real working directory. Darwin can now inspect the process cwd through `lsof`. If that inspection returns `null` — including a missing `lsof`, a command failure, or missing cwd output — registry-backed adoption now fails closed and the stale registry record is removed. This is a deliberate behavior change. It prevents a failed Darwin cwd probe from silently falling back to trusting stored registry metadata. The no-registry port-owner path already rejected a null cwd before this pull request, so its failure behavior has not changed. ## What Changed - Add a Darwin branch to `readLocalServiceProcessCwd`. - Run `lsof -a -d cwd -p <pid> -F0n` to read the process working directory. - Parse the NUL-delimited field output. - Do not trim the path. Do not split it on newlines. A directory name can contain a trailing space or a newline, and a changed path would name a different directory. - Keep the Linux `/proc/<pid>/cwd` path unchanged. - Return `null` for an invalid pid, a missing `lsof`, a command error, or missing output. - Reject a Darwin registry record when the working directory cannot be read. Darwin can now read it, so a failed read means the check failed. It no longer means the platform has no way to check. - Keep the registry fallback only on platforms that cannot read a process working directory. - Run the existing foreign-workspace rejection test on macOS. - Add a test: Paperclip adopts a port owner inside the workspace when no registry record exists. - Add a test: Paperclip rejects a listener in a sibling directory that differs only by a trailing space. - Add helper tests for newline and whitespace parsing, an invalid pid, and a missing `lsof` binary. - Resolve the branch-containment temporary repository root before the path comparison. This test-only commit comes from #9911 and keeps its author. ## Verification Head of this branch: `2be1b74746d8a0db4b680062f0c57995a6ff3912`. **Linux, on this head** ```sh pnpm --filter @paperclipai/server exec vitest run \ src/__tests__/workspace-runtime.test.ts \ src/__tests__/heartbeat-workspace-branch-containment.test.ts ``` Result: 138/138 pass. `workspace-runtime.test.ts` is 132/132. `heartbeat-workspace-branch-containment.test.ts` is 6/6. **macOS, on this head** macOS 26.4, Darwin 25.4.0, arm64, Node.js 22.22.2, pnpm 9.15.4. - Controlled baseline: `workspace runtime startup reconciliation > adopts a live auto-port shared service after runtime state is reset` fails on the rebase base `7c8064da1b35527865c1d523c9f0016e304ae46d` and reports `adopted: 0`, `stopped: 1`. The same test passes on this head. That test uses the normal managed start path, which starts the service detached. - Focused working-directory, registry, adoption, and boundary tests: 8/8 pass. - `heartbeat-workspace-branch-containment.test.ts`: 6/6 pass. Two assertions failed before the fixture change, because `/var/...` and `/private/var/...` name the same macOS directory. - Server typecheck: pass. - Full `workspace-runtime.test.ts`: 131/132 pass. The one failure is `realizeExecutionWorkspace > records teardown and cleanup operations when a recorder is provided`: ```text expected: /var/folders/... received: /private/var/folders/... ``` I ran that same test alone on the rebase base `7c8064da`, with no patch applied, and got the identical failure. It is a pre-existing macOS fixture that builds a path from `os.tmpdir()` and compares it against a realpath. It does not run the changed adoption path. This description does not claim the whole file is green on macOS. **macOS listener evidence** In the `adopts a port owner running inside the workspace when the registry record is gone` scenario, the auto-port listener bound port `54360`: ```text COMMAND PID USER FD TYPE DEVICE SIZE/OFF NODE NAME node 6808 <local-user> 12u IPv4 0xee4e36b2c8c094cf 0t0 TCP 127.0.0.1:54360 (LISTEN) ``` To hold the listener open long enough to capture this, that one diagnostic run added a temporary pause, which exceeded the Vitest timeout. The pause was reverted, the unmodified test was run again on this head, and it passed 1/1. The process and the port were then released. Note for maintainers: an existing test already covered this defect. That test never runs on macOS, because CI runs on Linux. A macOS job would have caught it in July. ## Risks Low risk. - Linux keeps the existing procfs implementation. - Other platforms keep the existing registry fallback. - macOS makes one extra `lsof` call, and only when it must read a process working directory. - A probe failure returns `null`. - Darwin port-owner adoption and Darwin registry adoption both fail closed. - The parser keeps significant whitespace and embedded newlines. - There is no database migration and no API change. ## Model Used Claude Opus 5 (`claude-opus-5`), with extended thinking, tool use, and code execution. It wrote the original implementation and the adoption tests, reviewed the branch, ran the Linux test suite, rebased onto current `master`, and prepared this text. OpenAI GPT-5.6-sol, through Hermes Agent, added the failure-mode coverage and ran the macOS checks. A human reviewed the change and controls publication. ## 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 (see Verification for the one disclosed macOS baseline failure) - [x] I have added or updated tests where applicable - [x] I have updated relevant documentation to reflect my changes (this change affects an internal helper and tests only) - [x] I have considered and documented any risks above - [x] All Paperclip CI gates are green - [x] Greptile is 5/5 with no open P2s, recommendations, or follow-ups - [x] I will address all Greptile and reviewer comments before requesting merge --------- Co-authored-by: tim <tf00185077@i-mps.com> Co-authored-by: Claude Opus 5 <noreply@anthropic.com> Co-authored-by: scbailey-build <scott@bequall.com>