Skip to content

🤖 refactor: the branch-summary pending marker takes its lock and a read-only probe from SessionLocks - #7427

Merged
ThomasK33 merged 2 commits into
mainfrom
phase4/branch-summary-marker
Oct 24, 2026
Merged

ThomasK33 merged 2 commits into
mainfrom
phase4/branch-summary-marker

Conversation

@ThomasK33

Copy link
Copy Markdown
Member

Summary

The branch-summary pending marker takes its lock from SessionLocks, and its reader finds a held marker through a new read-only probe, SessionLocks.processFileLockExists. It is PR 18 of phase 4 (landing note 15). In the app nothing changes: the Node layer's probe is the same single fs.stat of the lock file that the reader makes today, and the writer and reader get acquireProcessFileLock with today's arguments.

Why

PR 12 (#7404) first moved the marker's writer and reader to SessionLocks. Codex found that the reader found a held marker with fs.stat on the lock file, which an injected memory SessionLocks never creates. So the reader stopped seeing the writer in that layer. #7404 left the marker as it is on main, and this PR moves the writer and the reader together, with a probe that every layer answers. Tracked in #7326.

The existing API does not cover the probe. Both the Node and the memory acquireProcessFileLock assert timeoutMs > 0, so a try-acquire with timeout 0 is refused. A short try-acquire would also create the lock's folder and lock file on every send and contend with the writer, which changes what the writer sees.

Order

Implementation

  • sessionLocks/sessionLocks.ts: SessionLocks gains processFileLockExists(lockPath): true while any holder (this process or another, live or dead) has the lock. It never takes or reclaims the lock.
  • utils/concurrency/fileLock.ts: processFileLockExists, one fs.stat of the lock path. nodeSessionLocks.ts forwards to it.
  • sessionLocks/memorySessionLocks.testHarness.ts: the memory layer answers from its lock table. A dead holder stays until an acquire reclaims it, as its lock file would on the disk.
  • sessionLocks/foreignHolders.testHarness.ts: a fake foreign holder, live or dead, makes the probe true. Otherwise the inner layer answers.
  • sessionLocks/sessionLocks.contract.testHarness.ts: a contract case for all 4 layers (Node, memory, and each with foreign holders): own, live and dead holders, and the probe takes no lock.
  • branchSummary.ts: startAbandonedBranchSummaryInBackground takes locks in its input (Node default) for the writer. awaitPendingBranchSummary takes locks (Node default) for the probe and the wait.
  • agentSession.ts and workspaceService.ts: they pass this.historyService.locks to the reader and the writer.

Validation

On the stack head d24c5642c4 (PR 18 on PR 14's stack head c0831c924e), every gate passed:

  • make static-check and make lint-eslint pass. make crap-check: PASS, 0 violations, 9 warnings on touched hotspots (total CC of the changed files 13439 -> 13446).
  • bun test of sessionLocks/, branchSummary.test.ts, agentSession*.test.ts, workspaceService.*.test.ts and utils/concurrency/: 2668 pass, 0 fail.
  • The tripwire (scripts/perf/loopTestBaseline.ts --runs 3 --base c0831c924e): 0 trips.
  • make test: 27903 pass, 0 fail in the main run, and every other run passes.

The head 6a6948c03e is the same 2 commits rebased on main be78b8fed3 (after #7425). Its diff against main has the 10 files above, 103 insertions and 25 deletions. On this head, make static-check passes, and sessionLocks/, branchSummary.test.ts and utils/concurrency/ give 191 pass, 0 fail.

Tests

  • branchSummary.test.ts: the r48 cross-process wait runs on the Node layer and on the memory layer, through one helper. The memory case is new.
  • Revert proofs, each run once:
    • The reader goes back to fs.stat: the memory r48 case fails.
    • The writer goes back to nodeSessionLocks: the memory r48 case fails.
    • The foreign-holder probe drops its own table: the contract case fails on the 2 foreign-holder layers.

Mutation check

make mutate-check BASE=<PR 14 head>: 103 killed, 197 survived, 287 not run. Every survivor is in AgentSession.prepareMessage (CC 421), where the PR adds one argument, and the 300-mutant budget runs out there. So I ran a second, scoped check on a scratch commit that leaves agentSession.ts as on main: 38 killed, 81 survived, 1 equivalent, 0 without a test, 13 not run. No survivor of either run is on a line that this PR changes. I list them in #7326 for PR 17:

  • WorkspaceService.fork: 78. The PR adds one locks field.
  • awaitPendingBranchSummary: 2, and startAbandonedBranchSummaryInBackground: 1.
  • The equivalent is the writer's input.locks ?? nodeSessionLocks, with a mutation-equivalent marker.

Risks

  • None for behavior: in the app, historyService.locks is nodeSessionLocks. Its probe is the same fs.stat as before, and its acquire calls the same acquireProcessFileLock with the same arguments.

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

… file lock

Every layer answers it: the Node layer stats the lock file, the memory layer
reads its table, and the foreign holders see their own entries. A contract
case checks own, live and dead holders, and that the probe takes no lock.
… from SessionLocks

The writer and the reader move together: the reader probes the marker through
the same locks as the writer's acquire, since a memory lock leaves no file to
stat. agentSession and workspaceService pass the history service's locks.
@chatgpt-codex-connector

chatgpt-codex-connector Bot commented Oct 24, 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-24T16:33:52.226910Z 6a6948c PR opened
🔒 Security Review ✅ Completed 2026-10-24T16:35:01.275679Z 6a6948c PR opened
ℹ️ 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.

@github-actions

Copy link
Copy Markdown
Contributor

🟢 e2e stories (Haiku): 14 passed

27 agent steps · 7 replayed from cache · 31 model calls · 218.7k tokens (70% cached)
Spend: $0.10 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 24.0s
🟢 a chat survives a restart of the server 2 steps · 4 calls 24.0s
🟢 compaction.e2e.ts · 1 passed 3 steps · 7 calls 45.3s
🟢 /compact replaces the old chat with a summary 3 steps · 7 calls 45.3s
🟢 pluginTrustRestart.e2e.ts · 1 passed 4 steps · 2 calls 10.8s
🟢 a trusted plugin hook stays trusted after the server restarts 4 steps · 2 calls 10.8s
🟢 settingsRoundTrip.e2e.ts · 1 passed 3 steps · 5 calls 32.5s
🟢 a chat survives trips through Settings 3 steps · 5 calls 32.5s
🟢 sidebarPerWorkspace.e2e.ts · 1 passed 4 steps · 1 call 4.4s
🟢 a new workspace starts with its right sidebar collapsed while another keeps it open 4 steps · 1 call 4.4s
🟢 smoke.e2e.ts · 1 passed 2 steps · 1 call 2.7s
🟢 the agent opens the playground workspace 2 steps · 1 call 2.7s
🟢 streamErrors.e2e.ts · 1 passed 1 step · 1 call 7.5s
🟢 stream errors show in the chat, and the chat recovers 1 step · 1 call 7.5s
🟢 todoNudge.e2e.ts · 1 passed 2 steps · 4 calls 23.7s
🟢 the agent finishes open TODO items after the turn-end nudge 2 steps · 4 calls 23.7s
🟢 touchAlignment.e2e.ts · 6 passed 6 steps · 6 calls 21.0s
🟢 the Settings close button is level with the title on a touch screen 390px wide 1 step · 1 call 3.8s
🟢 the Settings close button is level with the title on a touch screen 768px wide 1 step · 1 call 3.9s
🟢 Backup settings checkboxes are small and level with their labels on a touch phone 1 step · 1 call 4.4s
🟢 the heartbeat checkbox is small and level with its label on a touch phone 1 step · 1 call 2.9s
🟢 a reply's model and time stay on one line on a touch phone 1 step · 1 call 3.1s
🟢 the workspace actions menu stays on the title row on a touch phone 1 step · 1 call 2.8s

e2e 0.20.0 · 2m 55s · web · run artifacts

🟢 e2e Electron stories (Haiku): 3 passed

3 agent steps · 3 model calls · 8.1k tokens (21% cached)
Spend: $0.02 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.6s
🟢 a deep link of a second launch starts a chat in the running app 1 step · 1 call 5.6s
🟢 electron/menuBar.e2e.ts · 1 passed 1 step · 1 call 6.1s
🟢 the workspace menu bar is the desktop title bar 1 step · 1 call 6.1s
🟢 electron/smoke.e2e.ts · 1 passed 1 step · 1 call 4.9s
🟢 Xum Electron opens the playground workspace 1 step · 1 call 4.9s

e2e 0.20.0 · 18.6s · 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 added this pull request to the merge queue Oct 24, 2026
Merged via the queue into main with commit 60170df Oct 24, 2026
33 of 34 checks passed
@ThomasK33
ThomasK33 deleted the phase4/branch-summary-marker branch October 24, 2026 17:13
yermakoffivan pushed a commit to yermakoffivan/mux that referenced this pull request Oct 26, 2026
…rker, subagent artifacts and tool configs (coder#7506)

## Summary

No-behavior-change refactor for the readiness-advisor follow-ups of
phase 4 PRs 13, 14 and 18 in coder#7326. The branch-summary marker, the four
subagent artifact writers, the patch-apply watermark and
`ToolConfiguration` now require the caller's `SessionLocks` instead of
falling back to the Node layer, and the two skill tools pass the tool
config's locks to the target mutation lock. Two production sites that
built a `ToolConfiguration` without locks now pass
`historyService.locks`. In the app every one of these resolves to
`nodeSessionLocks`, so the locks taken do not change.

Refs coder#7326

## Items

| coder#7326 item | Result |
|
----------------------------------------------------------------------------------------------------------------------------------
|
-----------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------
|
| PR 18 (coder#7427) advisor 1: `startAbandonedBranchSummaryInBackground` and
`awaitPendingBranchSummary` fall back to `nodeSessionLocks` | Done. Both
take an optional `marker: BranchSummaryMarker` (`{ sessionDir, locks }`)
instead of `sessionDir?` plus an optional or defaulted `locks`. The
locks are needed only for the marker, so a caller that names the marker
must name its locks, and the calls without a session dir stay as they
are. |
| PR 18 (coder#7427) advisor 2: the call-record branch-summary scenario uses
the default locks | Done. `oneFork` passes `historyService.locks` to the
writer and the reader. The recorded fixture does not change. |
| PR 14 (coder#7425) advisor 1: the 4 artifact functions keep an optional
`locks` | Done. `locks` is required in `subagentFailureArtifacts.ts`,
`subagentReportArtifacts.ts`, `subagentTranscriptArtifacts.ts` and
`subagentGitPatchArtifacts.ts`, and in `applyProjectPatch`
(`taskGitPatchEngine.ts`), which passes it through. Every production
call already passed locks. The test calls pass `nodeSessionLocks`. |
| PR 14 (coder#7425) advisor 2: `ToolConfiguration.sessionLocks` is optional
| Done. It is required. `createTestToolConfig` supplies
`nodeSessionLocks`. Two production configs left it out and now pass
`historyService.locks`: the skill-write config of `RefineService` and
the IPC bash tool of `WorkspaceService`. `todo.ts` and `review_pane.ts`
drop their `?? nodeSessionLocks`. |
| PR 13 (coder#7423) advisor: 9 callers of the target mutation lock take the
Node default | Partly done. `agent_skill_write.ts` and
`agent_skill_delete.ts` pass `config.sessionLocks` (6 sites), with a
test each. The other 7 (`memoryService.ts`, `reviewStateService.ts`,
`mcpAppResultStore.ts`, `devToolsService.ts`, `draftService.ts`,
`creationDraftService.ts`, `backup/adapters.ts`) have no `SessionLocks`
in scope: each needs a new constructor dependency and about 300 test
constructions, for no change in the app. Reasoned close in coder#7326. |
| PR 12 (coder#7404) advisor: the durable journal callers | Not in this PR.
They touch the files of coder#7485, which is still open. |

## Validation

- The two new tests ("takes the target lock from the config's session
locks", in `agent_skill_write.test.ts` and `agent_skill_delete.test.ts`)
fail when the tools drop `config.sessionLocks` and pass with it.
- The other changes are type-level: `tsc` fails on any caller that
leaves the locks out.
- `sessionLocks.callOptions.test.ts` passes with the fixture unchanged.
- `git merge-tree` of this head with each open `turn-engine/*` PR and
with coder#7491 gives no conflict that this PR causes. coder#7488 conflicts with
main in `streamManager.ts`, a file that this PR does not change.

- Mutation gate: the final `make mutate-check` run (`--max-mutants 900`,
base e803893) found 287 survivors, all in
`AgentSession.prepareMessage`. That is the hotspot that coder#7475 lists, and
this PR changes only one argument there. I did not chase them. The run
stopped at its time budget with 438 mutants not run, so it did not reach
the other changed functions. `MUTATION_MODE` is `warn`.

## Risks

Low. In the app the injected locks are `nodeSessionLocks` everywhere, so
no lock path or lock kind changes. Most of the diff is test call sites
that now name `nodeSessionLocks`.

---

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

<!-- mux-attribution: model=anthropic:claude-opus-5-5 thinking=high
costs=209.75 -->
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