Skip to content

🤖 tests: record the session lock calls of 18 more files - #7291

Merged
ThomasK33 merged 18 commits into
phase4/call-recordfrom
phase4/call-record-18
Oct 23, 2026
Merged

ThomasK33 merged 18 commits into
phase4/call-recordfrom
phase4/call-record-18

Conversation

@ThomasK33

@ThomasK33 ThomasK33 commented Oct 23, 2026 •

Copy link
Copy Markdown
Member

Summary

Extends the session lock call record of PR 2 to the 18 other files that phase 4 moves behind SessionLocks. The fixture grows to 56 rows. It is PR 3 of phase 4.

Background

Phase 4 moves the session-dir lock calls of 27 files. PR 2 records the calls of the first 9 files. This PR adds scenarios that reach the other 18: usage, goal and timing services, the subagent artifact stores, the task handle store, init state, session files, plan migration, workspace refine (full clear), pinned artifacts, the artifact index, workflow runs, refinement rollback and journal appends.

Implementation

  • Five new scenarios in sessionLocks.callOptions.scenarios.testHarness.ts:
    1. oneUsageAndGoal: one headless usage record, then the timing file clear and a goal read.
    2. oneChildReport: the 5 subagent artifact functions, then the task handle publication lock.
    3. oneInitAndPlan: backend A starts an init, backend B replays it while A holds the record lock, then A ends it. Then a remote plan migration mark runs.
    4. oneWorkspaceRefine: one full clear of the history through the workspace service.
    5. oneArtifactAndRun: pinned paths, an artifact version, a workflow run and its lease, the rollback lock and one journal append.
  • The test also spies on WorkspaceUseLeases.prototype.isHeld (the init record check), with its workspace id.
  • A scenario can name its current step (lockCallStep()), and the record adds it to each row. The child report scenario names the two calls of subagentReportArtifacts.ts, so they stay two rows. Function names from the stack were not usable: another test file in the same bun test src process changes the frame format.
  • The record of 🤖 tests: record the options of each session lock call #7290 keeps every argument of each call, so these rows also show createParentDirectory (workflow locks pass false, so a late operation cannot recreate a removed run dir) and the signal as <AbortSignal> (a canceled artifact publish wakes its wait through it). The artifact scenario passes a live signal. Each new scenario names its temp root and workspace id for the name masks of 🤖 tests: record the options of each session lock call #7290 (<root:artifact>, <hash:history:ws-record-init>), and the usage scenario names its session dir as a memory target. The fixture is recorded again in this format: the same 61 rows.
  • The workflow scenario calls createRunIfAbsent and releaseLease, which pass the time left of a deadline as the timeout. Date.now stands still during them, so the record holds the whole budget (120000) in every run.
  • The full-clear scenario sets XUM_ROOT to its temp root. A full clear deletes plan files under the local Xum home, and without it the test deleted a real ~/.xum plan of the same name (a probe confirmed it).
  • The init scenario does not poll. It queues one no-op withLock turn behind the init write.
  • One row has the site unknown: a withLock call of the full clear. JSC makes proper tail calls, so a non-async caller that returns the lock call leaves no stack frame. A comment in callSite() explains it. The row was the same in 26 runs.

Validation

  • Tripwire (loopTestBaseline.ts --runs 3 --base origin/phase4/call-record) on the round-5 head: 0 trips. The round-5 fix changes only names in the test file and the fixture, and the phase lead counts this tripwire for the final head.
  • bun test ./src/node/services/sessionLocks/sessionLocks.callOptions.test.ts: 10 more runs passed, about 1 s each.
  • Scratch edits: createParentDirectory: true in WorkflowRunStore.ts, and no signal in artifactVersionStore.ts -> each fails the test. Reverted.
  • Probe: a marker file at the real ~/.xum/plans/ws-record-clear.md was deleted by the old scenario and survives the new one.
  • Scratch edit: acquireTimeoutMs: 0 of the init record lock in initStateManager.ts changed to 1 -> the test fails with a diff on exactly that row. Reverted.
  • make test and its second half (tests/ui/storybook/, tests/ui/domIsolation.test.ts, vscode/src/, scripts/) on the 🤖 tests: record the session lock calls of 18 more files #7291 head 069b820 (which contains this PR) pass except WorkspaceMenuBar.test.tsx and scripts/perf/pageTimingCore.test.ts, known failures on this older base (fixed on main by 🤖 tests: make test-unit runs CI's isolated files alone and ends a leaked metadata loop #7305), and the known load flake backgroundProcessManager.test.ts, which passed alone (163 of 163).
  • Mutation run (scripts/mutate/check.ts, base = this PR's base): PASS with 0 mutants. The PR changes only test files, so no survivor needs a disposition.

Deferred review findings

#7326 tracks the findings that #7290 and this PR declined. This PR declined none of its own.

Risks

Test-only. The unknown row changes if a refactor changes how that call returns, although the options stay the same. The PR that does so records the fixture again.


Generated with xum • Model: anthropic:claude-opus-5-5 • Thinking: high • Cost: $70.87

@chatgpt-codex-connector

chatgpt-codex-connector Bot commented Oct 23, 2026 •

Copy link
Copy Markdown

Codex Review Summary

This comment shows the latest Codex review activity on this pull request.

Review Status Commit Review trigger
📝 Code Review ✅ Completed 2026-10-23T09:43:45.387233Z 069b820 New commits
🔒 Security Review ✅ Completed 2026-10-23T09:43:44.625618Z 069b820 New commits
ℹ️ About Codex in GitHub

Your team has set up Codex to review pull requests in this repo. Reviews are triggered when you

  • Open a pull request for review
  • Mark a draft as ready
  • Comment "@codex review" or "@codex security review".

Codex reacts with 👀 while any review is running, comments if it has suggestions, and reacts with 👍 once all reviews finish with no findings.

@ThomasK33
ThomasK33 added this pull request to stack #7292 October 23, 2026 03:04
@github-actions

github-actions Bot commented Oct 23, 2026 •

Copy link
Copy Markdown
Contributor

🟢 e2e stories (Haiku): 14 passed

27 agent steps · 7 replayed from cache · 32 model calls · 233.2k tokens (75% cached)
Spend: $0.09 of the $1.00 cap (proxy ledger)

All 14 tests in 9 files
Test Agent Time
🟢 chatRestart.e2e.ts · 1 passed 2 steps · 4 calls 27.7s
🟢 a chat survives a restart of the server 2 steps · 4 calls 27.7s
🟢 compaction.e2e.ts · 1 passed 3 steps · 7 calls 43.1s
🟢 /compact replaces the old chat with a summary 3 steps · 7 calls 43.1s
🟢 pluginTrustRestart.e2e.ts · 1 passed 4 steps · 2 calls 10.1s
🟢 a trusted plugin hook stays trusted after the server restarts 4 steps · 2 calls 10.1s
🟢 settingsRoundTrip.e2e.ts · 1 passed 3 steps · 5 calls 28.6s
🟢 a chat survives trips through Settings 3 steps · 5 calls 28.6s
🟢 sidebarPerWorkspace.e2e.ts · 1 passed 4 steps · 1 call 4.3s
🟢 a new workspace starts with its right sidebar collapsed while another keeps it open 4 steps · 1 call 4.3s
🟢 smoke.e2e.ts · 1 passed 2 steps · 1 call 4.7s
🟢 the agent opens the playground workspace 2 steps · 1 call 4.7s
🟢 streamErrors.e2e.ts · 1 passed 1 step · 1 call 6.9s
🟢 stream errors show in the chat, and the chat recovers 1 step · 1 call 6.9s
🟢 todoNudge.e2e.ts · 1 passed 2 steps · 5 calls 24.6s
🟢 the agent finishes open TODO items after the turn-end nudge 2 steps · 5 calls 24.6s
🟢 touchAlignment.e2e.ts · 6 passed 6 steps · 6 calls 31.6s
🟢 the Settings close button is level with the title on a touch screen 390px wide 1 step · 1 call 3.7s
🟢 the Settings close button is level with the title on a touch screen 768px wide 1 step · 1 call 12.9s
🟢 Backup settings checkboxes are small and level with their labels on a touch phone 1 step · 1 call 4.8s
🟢 the heartbeat checkbox is small and level with its label on a touch phone 1 step · 1 call 3.6s
🟢 a reply's model and time stay on one line on a touch phone 1 step · 1 call 3.4s
🟢 the workspace actions menu stays on the title row on a touch phone 1 step · 1 call 3.2s

e2e 0.20.0 · 3m 5s · web · run artifacts

🟢 e2e Electron stories (Haiku): 3 passed

3 agent steps · 3 model calls · 8.0k tokens (32% cached)
Spend: $0.01 of the $1.00 cap (proxy ledger)

All 3 tests in 3 files
Test Agent Time
🟢 electron/deepLink.e2e.ts · 1 passed 1 step · 1 call 5.2s
🟢 a deep link of a second launch starts a chat in the running app 1 step · 1 call 5.2s
🟢 electron/menuBar.e2e.ts · 1 passed 1 step · 1 call 5.6s
🟢 the workspace menu bar is the desktop title bar 1 step · 1 call 5.6s
🟢 electron/smoke.e2e.ts · 1 passed 1 step · 1 call 4.8s
🟢 Xum Electron opens the playground workspace 1 step · 1 call 4.8s

e2e 0.20.0 · 17.7s · electron · run artifacts

🟢 e2e agent repros (Haiku): no tests selected

Spend: $0.00 of the $1.00 cap (proxy ledger)

e2e 0.20.0 · 5.3s · 2 targets (web, phone) · run artifacts

@ThomasK33
ThomasK33 force-pushed the phase4/call-record-18 branch from f4e279b to 20f014a Compare October 23, 2026 04:09

@chatgpt-codex-connector chatgpt-codex-connector Bot left a comment

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

💡 Codex Review

Here are some automated review suggestions for this pull request.

Reviewed commit: 20f014a217

ℹ️ About Codex in GitHub

Your team has set up Codex to review pull requests in this repo. Reviews are triggered when you

  • Open a pull request for review
  • Mark a draft as ready
  • Comment "@codex review".

If Codex has suggestions, it will comment; otherwise it will react with 👍.

Codex can also answer questions or update the PR. Try commenting "@codex address that feedback".

Comment thread src/node/services/sessionLocks/__fixtures__/callOptions.json Outdated
@ThomasK33

Copy link
Copy Markdown
Member Author

@codex review

…e4/call-record-18

# Conflicts:
#	src/node/services/sessionLocks/__fixtures__/callOptions.json
#	src/node/services/sessionLocks/sessionLocks.callOptions.test.ts
# Conflicts:
#	src/node/services/sessionLocks/__fixtures__/callOptions.json
@ThomasK33

Copy link
Copy Markdown
Member Author

@codex review

@chatgpt-codex-connector

Copy link
Copy Markdown

🛡️ Codex Security Review · Automatically triggered

Security review completed. No security issues were found in this pull request.

Reviewed commit: c95e6e619a

View security finding report

Only the user who started this review can view the report in Codex.

ℹ️ About Codex security reviews in GitHub

This is an experimental Codex feature. Security reviews are triggered when:

  • You comment "@codex security review"
  • A regular code review gets triggered (for example, "@codex review" or when a PR is opened), and you’re opted in so security review runs alongside code review

Once complete, Codex will leave suggestions, or a comment if no findings are found.

@chatgpt-codex-connector chatgpt-codex-connector Bot left a comment

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

💡 Codex Review

Here are some automated review suggestions for this pull request.

Reviewed commit: c95e6e619a

ℹ️ About Codex in GitHub

Your team has set up Codex to review pull requests in this repo. Reviews are triggered when you

  • Open a pull request for review
  • Mark a draft as ready
  • Comment "@codex review".

If Codex has suggestions, it will comment; otherwise it will react with 👍.

Codex can also answer questions or update the PR. Try commenting "@codex address that feedback".

Comment thread src/node/services/sessionLocks/sessionLocks.callOptions.scenarios.testHarness.ts Outdated
Comment thread src/node/services/sessionLocks/sessionLocks.callOptions.test.ts Outdated
Comment thread src/node/services/sessionLocks/sessionLocks.callOptions.scenarios.testHarness.ts Outdated
@ThomasK33

Copy link
Copy Markdown
Member Author

@codex review

@chatgpt-codex-connector

Copy link
Copy Markdown

🛡️ Codex Security Review · Automatically triggered

Security review completed. No security issues were found in this pull request.

Reviewed commit: ce10f7311f

View security finding report

Only the user who started this review can view the report in Codex.

ℹ️ About Codex security reviews in GitHub

This is an experimental Codex feature. Security reviews are triggered when:

  • You comment "@codex security review"
  • A regular code review gets triggered (for example, "@codex review" or when a PR is opened), and you’re opted in so security review runs alongside code review

Once complete, Codex will leave suggestions, or a comment if no findings are found.

@chatgpt-codex-connector

Copy link
Copy Markdown

Codex Review: Didn't find any major issues. Swish!

Reviewed commit: ce10f7311f

ℹ️ About Codex in GitHub

Your team has set up Codex to review pull requests in this repo. Reviews are triggered when you

  • Open a pull request for review
  • Mark a draft as ready
  • Comment "@codex review".

If Codex has suggestions, it will comment; otherwise it will react with 👍.

Codex can also answer questions or update the PR. Try commenting "@codex address that feedback".

# Conflicts:
#	src/node/services/sessionLocks/__fixtures__/callOptions.json
@ThomasK33

Copy link
Copy Markdown
Member Author

@codex review

@chatgpt-codex-connector

Copy link
Copy Markdown

🛡️ Codex Security Review · Automatically triggered

Security review completed. No security issues were found in this pull request.

Reviewed commit: 483f47a330

View security finding report

Only the user who started this review can view the report in Codex.

ℹ️ About Codex security reviews in GitHub

This is an experimental Codex feature. Security reviews are triggered when:

  • You comment "@codex security review"
  • A regular code review gets triggered (for example, "@codex review" or when a PR is opened), and you’re opted in so security review runs alongside code review

Once complete, Codex will leave suggestions, or a comment if no findings are found.

@chatgpt-codex-connector

Copy link
Copy Markdown

Codex Review: Didn't find any major issues. Can't wait for the next one!

Reviewed commit: 483f47a330

ℹ️ About Codex in GitHub

Your team has set up Codex to review pull requests in this repo. Reviews are triggered when you

  • Open a pull request for review
  • Mark a draft as ready
  • Comment "@codex review".

If Codex has suggestions, it will comment; otherwise it will react with 👍.

Codex can also answer questions or update the PR. Try commenting "@codex address that feedback".

# Conflicts:
#	src/node/services/sessionLocks/__fixtures__/callOptions.json
#	src/node/services/sessionLocks/sessionLocks.callOptions.test.ts
@ThomasK33

Copy link
Copy Markdown
Member Author

@codex review

@chatgpt-codex-connector

Copy link
Copy Markdown

Codex Review: Didn't find any major issues. Chef's kiss.

Reviewed commit: f16d08a2a6

ℹ️ About Codex in GitHub

Your team has set up Codex to review pull requests in this repo. Reviews are triggered when you

  • Open a pull request for review
  • Mark a draft as ready
  • Comment "@codex review".

If Codex has suggestions, it will comment; otherwise it will react with 👍.

Codex can also answer questions or update the PR. Try commenting "@codex address that feedback".

@chatgpt-codex-connector

Copy link
Copy Markdown

🛡️ Codex Security Review · Automatically triggered

Security review completed. No security issues were found in this pull request.

Reviewed commit: f16d08a2a6

View security finding report

Only the user who started this review can view the report in Codex.

ℹ️ About Codex security reviews in GitHub

This is an experimental Codex feature. Security reviews are triggered when:

  • You comment "@codex security review"
  • A regular code review gets triggered (for example, "@codex review" or when a PR is opened), and you’re opted in so security review runs alongside code review

Once complete, Codex will leave suggestions, or a comment if no findings are found.

@ThomasK33
ThomasK33 added this pull request to the merge queue Oct 23, 2026
Merged via the queue into main with commit ac9946f Oct 23, 2026
41 of 63 checks passed
@ThomasK33
ThomasK33 deleted the phase4/call-record-18 branch October 23, 2026 11:05
azaj01 pushed a commit to azaj01/mux that referenced this pull request Oct 25, 2026
## Summary

Adds a test that records each session lock call that a few real
scenarios make: the source file of the call, the lock function and all
its arguments (every option). A fixture pins today's calls. It is PR 2
of phase 4 (session locks behind one `SessionLocks` service). Each later
move PR must keep this fixture byte for byte, so a move that changes a
lock path, a timeout, a label or any other option fails.

## Background

Phase 4 moves the lock calls of 27 files behind `SessionLocks`, with no
change of behavior. The moved calls keep their options by hand. A
changed timeout or path changes behavior only under contention, which
normal tests do not reach. This record makes such a change visible in
review. This PR covers the 9 files of the first group (history, agent
session, turn manager, removal, branch summary, refine, durable journal,
todo and review pane tools). PR 3 adds the other 18 files.

## Implementation

- `sessionLocks.callOptions.test.ts` spies on the lock modules: the
`fileLock` and `crossProcessLock` namespaces,
`WorkspaceUseLeases.prototype.hold` and the shared
`workspaceFileLocks.withLock`. In Bun, a spy on a module namespace also
reaches the named imports of the callers, so the record sees each call
before and after a move. `callSite()` takes the first source file on the
stack and skips the `SessionLocks` layers, which only forward a call.
Calls from test files are left out.
- `sessionLocks.callOptions.scenarios.testHarness.ts` runs the scenarios
through public entry points: one turn, one refusal of a mutation in a
turn, one removal, one refine apply and one fork.
- Each row records every argument of the call, serialized by kind:
strings are masked, functions and `AbortSignal`s become fixed tokens,
and any other kind fails the test with an assertion. So no option can
drop out of the record (`createParentDirectory`, for example). This
replaced a chosen list of keys in review round 4, because rounds 1, 2
and 4 each found one more key that the list left out.
- The `workspaceFiles.withLock` key and the `WorkspaceUseLeases.hold`
workspace id are arguments, so the record has them too. A move that
passes the wrong id fails.
- `callSite()` reads the stack-frame path up to `.ts:line:col`, so a
checkout path with spaces (or a drive letter) still parses.
- Paths are masked by name, not by order of use. Each scenario names the
temp roots and workspace ids it creates (`<root:turn>`,
`ws-record-turn`), and the removal names its memory target keys. The
test turns each name into digests with the code's own path functions
(`historyWriteLockPath`, `refineApplyLockPath`,
`workspaceMutationLockPath`, `workspaceUseLockDir`,
`targetMutationLockFilePath`), so a digest shows as
`<hash:history:ws-record-turn>`. Each digest scheme has its own name:
`history` (SHA-256 of UTF-8, cut to 32 characters: the history and
refine-apply locks), `workspace-use` (SHA-256 of UTF-16LE: the mutation
gate and the use leases) and `target` (memory target keys). The test
checks that the paths of one scheme share one digest, and that a digest
keeps one name. So a lock built with the other scheme's digest shows the
other name (review round 5). A digest or temp path with no name fails
the test with a clear message (`lock calls with no name: ...`). So a
lock built from the wrong id or root fails even when it occurs once.
This replaced first-use numbering in review round 4, because rounds 2, 3
and 4 each found one more case that the numbering hid. Uuids (lease
tokens) are masked. Rows are sorted and deduplicated, so the fixture is
stable.
- The scenarios build their `HistoryService` with
`createTestHistoryService()`.
- The rows are sorted, so they cannot show lock order. One explicit
check keeps the order of removal: refine, then target, then history.
Another order can deadlock against an apply that holds refine and waits
for history.
- Record again after an intended change: `UPDATE_FIXTURE=1 bun test
./src/node/services/sessionLocks/sessionLocks.callOptions.test.ts`, then
`make fmt`.

## Validation

- Tripwire (`loopTestBaseline.ts --runs 3 --base origin/main`) on the
round-5 head: 0 trips. The round-5 fix changes only names in the test
file and the fixture, and the phase lead counts this tripwire for the
final head.
- `bun test
./src/node/services/sessionLocks/sessionLocks.callOptions.test.ts`:
passes, about 1 s, and 10 more runs passed, also 3 repeats in one
process.
- Scratch edit: the `withLock` key in `tools/todo.ts` changed -> the
test fails with a diff on that row's key. Reverted.
- Scratch edit: `AgentSession` holds the turn lease for a wrong
workspace id -> the test fails on the `hold` row and on the history lock
token. Reverted.
- Scratch edit: `historyService.ts` builds the history write lock from
`${workspaceId}-other` -> the test fails with `lock calls with no name:
<the three new digests>`. Reverted.
- Scratch edit: `historyService.ts` builds the history write lock path
from the `workspaceUseLockDir` digest (the UTF-16LE scheme) -> the test
fails with a diff from `history-<hash:history:...>` to
`history-<hash:workspace-use:...>` on each history row. Reverted.
- Scratch edit: `workspaceUseLeases.ts` passes `createParentDirectory:
false` to the turn lease's `acquireCrossProcessLock` -> the test fails
with a diff that adds `"createParentDirectory": false` to both lease
rows. Reverted. (No call in the scenarios passes `false` today, only
`WorkflowRunStore`, so the scratch edit adds the option where the review
finding said a move could add it. At the turn owner lock in
`workspaceTurnManager.ts` the same edit makes the scenario itself fail
before the record compare, because that lock directory does not exist
yet.)
- Scratch edit: the 10 000 ms timeout of the removal lock in
`workspaceRemoval.ts` changed -> the test fails with a diff on that row.
Reverted.
- `make test` and its second half (`tests/ui/storybook/`,
`tests/ui/domIsolation.test.ts`, `vscode/src/`, `scripts/`) on the coder#7291
head 069b820 (which contains this PR) pass except
`WorkspaceMenuBar.test.tsx` and `scripts/perf/pageTimingCore.test.ts`,
known failures on this older base (fixed on main by coder#7305), and the
known load flake `backgroundProcessManager.test.ts`, which passed alone
(163 of 163).
- Mutation run (`scripts/mutate/check.ts`, base = this PR's base): PASS
with 0 mutants. The PR changes only test files, so no survivor needs a
disposition.

## Known limits of the record

1. The rows are sorted, so they show no lock order except the removal
order that the test checks. The record cannot see if
`WorkspaceUseLeases.hold()` inspects the mutation gate before it
acquires the use-lease file. Phase 4 PR 4 adds an explicit order check
for that (review round 6).
2. The digest names come from the production hash builders. A move that
changes a hash scheme changes the path and its name together, so the
fixture still matches. Phase 4 PR 4 pins fixed digests for the fixed
test IDs and keys, before PRs 9, 12 and 13 touch the files of those
builders (review round 6).

## Deferred review findings

coder#7326 tracks the findings that this PR declined, each with the PR that
must close it: the two limits above, seeding HistoryService in
`beforeEach` (round 4) and Windows path separators (round 2).

## Risks

Test-only. The record depends on stack traces: if a refactor changes
which source file makes a call, the site of a row changes although the
options do not. The PR that does so records the fixture again and
explains the row change.

---

_Generated with `xum` • Model: `anthropic:claude-opus-5-5` • Thinking:
`high` • Cost: `$70.87`_

<!-- mux-attribution: model=anthropic:claude-opus-5-5 thinking=high
costs=70.87 -->
azaj01 pushed a commit to azaj01/mux that referenced this pull request Oct 25, 2026
…as its Node layer (coder#7345)

## Summary

Adds the `SessionLocks` service: one interface over today's session lock
functions, with a Node layer that calls those functions. No caller moves
in this PR. It is PR 4 of phase 4.

## Background

Phase 4 moves the session-dir lock calls of 27 files behind one Effect
service, so tests and the in-memory turn can swap the lock medium with a
layer. PRs 2 and 3 (coder#7290, coder#7291) record the options of every lock call.
This PR adds the service and its contract. Later PRs move the callers.

## Implementation

The first commit adds two checks to `sessionLocks.callOptions.test.ts`,
on the code before the move. They close the two round-6 findings of
coder#7290 that coder#7326 tracks:
1. `hold()` takes the use lease before the mutation lease:
`["workspace-use", "workspace-mutation"]`.
2. The lock digests of the record are pinned: `PINNED_DIGESTS` for 11
workspace ids and `PINNED_TARGET` for `/record/target`. A changed hash
scheme now fails the record, even when the digest names follow it.

The second commit adds the service:
- `sessionLocks.ts`: the `SessionLocks` interface. Each member has the
signature of today's function: `workspaceFiles.withLock`,
`acquireProcessFileLock`, `acquireCrossProcessLock`,
`inspectCrossProcessLock` and `useLeasesFor(config)`.
- `nodeSessionLocks.ts`: the Node layer. Its members call today's
functions at call time, so spies on the lock modules still reach the
call. `workspaceFiles` is the shared `workspaceFileLocks` map: a second
map would let two writers of one workspace run at once.
- `di/tags.ts`, `di/layers/stores.ts`: `SessionLocksTag`, and
`StoresLive` provides `SessionLocksNode`, so the app always gets the
Node layer.
- `utils/concurrency/lockErrors.ts`: `CrossProcessLockTimeoutError` and
`WorkspaceMutationInProgressError` move to a pure module.
`crossProcessLock.ts` and `workspaceUseLeases.ts` re-export them, so
each stays one class for `instanceof`.
- The message functions `fileLockTimeoutMessage` and
`workspaceMutationInProgressMessage` are extracted with the same text,
and `crossProcessLock.ts` exports its holder and blocker types and
`describeHolder`. The test layers of PR 6 refuse with today's text
through them.
- `sessionLocks.contract.testHarness.ts`,
`sessionLocks.contract.test.ts`, `sessionLocks.testChild.ts`: the
contract, run on the Node layer. A child process is a real foreign
holder, live or killed with SIGKILL.

## Validation

Checks before and after the move (`bun test
src/node/services/sessionLocks/`, bun 1.4.3):
- First commit e217b89 (before the move): `moved lock calls keep
their options` passes, with both new checks. 2 pass, 0 fail across 2
files.
- Service commit 2715877 (after the move): `moved lock calls keep
their options` passes. 10 pass, 0 fail across 3 files.

Gates on the PR head:
- `bun test ./src/node/services/sessionLocks/
./src/node/utils/concurrency/fileLock.test.ts
./src/node/utils/main/crossProcessLock.test.ts
./src/node/services/workspaceUseLeases.test.ts`: 79 pass, 0 fail on
907f6b0. The three lock test files pass with no edit
(`fileLock.test.ts` asserts the timeout text).
- `git diff --name-only` shows no lock caller: the changed non-test
files are the three lock modules, `lockErrors.ts`, the new service
files, `tags.ts` and `stores.ts`.
- `make typecheck` and `make static-check` on 907f6b0: pass.
- `make test` on 907f6b0: pass, 0 failures. Its second half
(`tests/ui/storybook/`, `tests/ui/domIsolation.test.ts`, `vscode/src/`,
`scripts/`): pass, 0 failures. An earlier `make test` on 722dc08
(before the last main merge) failed only on the known load flake
`backgroundProcessManager.test.ts`, which passed alone (163 of 163).
- Tripwire (`loopTestBaseline.ts --runs 3 --base origin/main`): 0 trips
on 907f6b0, and 0 trips on 722dc08.
- 91dc55d (review round 1, the fresh advisor's nit): it moves the
`WorkspaceUseLeases` doc comment back onto its class. `git diff
907f6b0 91dc55d` changes one comment line and no code, so the
gates above count for it. `make static-check` passes on it.

## Mutation check

Survivors get one of three dispositions: "killed by <test>" (a test in
this PR), "equivalent: <reason>" (no input can tell the mutant from the
original), or "pre-existing gap: PR 17" (a survivor on a line that this
PR does not change). Each gap is listed in coder#7326. A later PR that
changes or moves one of those lines must first kill its survivor, in a
test commit at its start that passes on the code before the change.

- The default `make mutate-check` caps each source file at its 10
nearest test files. That cap leaves out
`sessionLocks.callOptions.test.ts` and `sessionLocks.contract.test.ts`.
The default run on 722dc08 had 94 mutants, 56 killed and 38 survived.
- The table below comes from one scratch run of
`scripts/mutate/mutate_report.py` on the PR head 907f6b0, with no
cap. Its lane is the nearest tests of the default runs plus the two
sessionLocks tests: 94 mutants, 55 killed, 39 survived, 0 uncovered. The
call-record test is in the lane of `workspaceUseLeases.ts`,
`fileLock.ts` and `crossProcessLock.ts`, and kills `acquireTimeoutMs: 0`
-> 1 in `hold`.
- Some kills are timing-dependent: the default run killed `parseHolder`
:144 `||` and :145 `||`, and this run did not. The table lists them as
gaps.
- Every mutant in the new code (the message functions, `lockErrors.ts`,
the service files) is killed. All 39 survivors are on lines that this PR
does not change. The check scores whole functions, and the PR edits one
line in each of `hold`, `acquireProcessFileLock`, `parseHolder`,
`describeHolder` and `acquireCrossProcessLock` (an extracted message
function or a renamed type).
- Line numbers are on the PR head.

| # | Site | Mutant | Disposition |
|---|------|--------|-------------|
| 1 | workspaceUseLeases.ts:331 `hold` | `existing.count > 0` -> `>=` |
equivalent: release deletes an entry at 0, so no call sequence stores a
count of 0 |
| 2 | workspaceUseLeases.ts:362 `hold` | `?? new Map` -> `\|\|` |
equivalent: a Map is always truthy |
| 3 | workspaceUseLeases.ts:373 `release` | `files?.get` -> `files.get`
| equivalent: `files` is missing only on an over-release, and no call
sequence makes one (each lease releases once, `releasing ??=`, and each
hold that returns a lease counts it) |
| 4-5 | workspaceUseLeases.ts:375 `release` | `&&` -> `\|\|` (2 sites) |
equivalent: the assert fails only on an over-release, which no call
sequence makes |
| 6 | workspaceUseLeases.ts:375 `release` | `file.count > 0` -> `>=` |
equivalent: the same invariant as coder#1 |
| 7 | crossProcessLock.ts:515 | `interval.unref?.()` ->
`interval.unref()` | equivalent: Bun and Node timers always have `unref`
|
| 8-10 | fileLock.ts:260, :261 | `lockPath.length > 0` -> `> 1` / `>=`,
`timeoutMs > 0` -> `>=` | pre-existing gap: PR 17 (no test passes `""`,
a 1-character path or a timeout of 0) |
| 11-13 | fileLock.ts:278, :279 | `?? FILE_LOCK_RENEW_INTERVAL_MS` ->
`\|\|`, `renewIntervalMs > 0` -> `> 1` / `>=` | pre-existing gap: PR 17
(no test passes an interval of 0 or 1) |
| 14 | fileLock.ts:265 | `acquired = false` -> `true` | pre-existing
gap: PR 17 (a failed acquire keeps its token live; a lock file with that
token then reads as live) |
| 15 | fileLock.ts:296 | dispose `renewInFlight !== null` -> `===` |
pre-existing gap: PR 17 (release does not join a renewal in flight) |
| 16 | fileLock.ts:306 | `reclaimStaleFileLock(..., 0)` -> 1 |
pre-existing gap: PR 17 (no test reaches the reclaim depth limit) |
| 17 | fileLock.ts:307 | `>= deadline` -> `>` | pre-existing gap: PR 17
(one more retry at the deadline's millisecond) |
| 18-19 | fileLock.ts:311 | retry sleep `*` -> `/`, `+` -> `-` |
pre-existing gap: PR 17 (no test checks the retry interval) |
| 20-30 | crossProcessLock.ts:130, :140-145 `parseHolder` | 11 mutants
of the record shape check | pre-existing gap: PR 17 (no test reads a
record that is null, an array, or has a bad pid, token or acquiredAt; a
backend can be pid 1 in a container) |
| 31 | crossProcessLock.ts:226 `describeHolder` | `birth ?? ...` ->
`\|\|` | pre-existing gap: PR 17 (no test reads a record with an empty
birth) |
| 32 | crossProcessLock.ts:499 | renewal guard `\|\|` -> `&&` |
pre-existing gap: PR 17 (overlapping renewal ticks) |
| 33 | crossProcessLock.ts:504 | renew only our own record `&&` ->
`\|\|` | pre-existing gap: PR 17 (a tick that finds another holder's
record) |
| 34 | crossProcessLock.ts:517 | `stopped = true` -> `false` |
pre-existing gap: PR 17 (a tick past its check after release) |
| 35-36 | crossProcessLock.ts:528 | release attempts `attempt = 0` -> 1,
`< 40` -> `<=` | pre-existing gap: PR 17 (no test makes `rm` fail on
every attempt) |
| 37 | crossProcessLock.ts:543 | `rm({ force: true })` -> `false` |
pre-existing gap: PR 17 (a record that vanishes between observe and
`rm`) |
| 38 | crossProcessLock.ts:569 | `owned = false` -> `true` |
pre-existing gap: PR 17 (`tryTake` throws and the token stays live) |
| 39 | crossProcessLock.ts:585 | `>= deadline` -> `>` | pre-existing
gap: PR 17 (a zero-timeout try-lock that fails within the deadline's
millisecond sleeps once) |

## Deferred review findings

coder#7326 tracks the findings that coder#7290 declined. This PR closes items 1
and 2 (the first commit). coder#7326 also lists the 32 pre-existing mutation
gaps above, with the rule for later PRs.

## Risks

No lock caller changes, and the Node layer calls the same functions. The
error classes move module, so a second copy of a class would break
`instanceof`. Both lock modules re-export the one class from
`lockErrors.ts`. The contract checks `CrossProcessLockTimeoutError` with
`toBeInstanceOf` on the Node layer.

---

_Generated with `xum` • Model: `anthropic:claude-opus-5-5` • Thinking:
`high` • Cost: `$81.85`_

<!-- mux-attribution: model=anthropic:claude-opus-5-5 thinking=high
costs=81.85 -->

---------

Signed-off-by: Thomas Kosiewski <tk@coder.com>
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