Skip to content

🤖 refactor: the target mutation and rollback locks take their file lock from SessionLocks - #7423

Merged
ThomasK33 merged 3 commits into
mainfrom
phase4/target-locks
Oct 24, 2026
Merged

ThomasK33 merged 3 commits into
mainfrom
phase4/target-locks

Conversation

@ThomasK33

@ThomasK33 ThomasK33 commented Oct 24, 2026 •

Copy link
Copy Markdown
Member

Summary

The target mutation lock and the refinement rollback lock take their file lock from SessionLocks. It is PR 13 of phase 4. Lock paths, the 2 s waits, order and messages do not change: the Node layer gives today's acquireProcessFileLock. The in-process mutexes stay in their modules.

Order

Implementation

  • refinement/targetMutationLocks.ts: withTargetMutationLock and withTargetMutationLocks take an optional last locks argument (Node default) for the file lock.
  • refinement/refinementRollback.ts: rollbackRefinement takes locks in its options for the rollback lock and the target locks. acquireRollbackFileLock(sessionDir, locks) defaults to the Node layer.
  • workspaceRemoval.ts: the removal target lock and rollbackRemovalTombstoneIfOwned use the caller's locks.
  • workspaceService.ts: the 2 tombstone rollbacks pass this.historyService.locks. The plan also lists the refine serialization lock here (workspaceService.ts:4478). PR 12 (🤖 refactor: removal, refine and the durable journal take their locks from SessionLocks #7404) moved it in its round 5, because Codex found it split from its partner lock.
  • sessionUsageService.ts: the headless usage target lock passes this.historyService.locks.

The 9 other callers of the target lock (plan claim 2.1.1) keep the default with no edit: reviewStateService.ts, mcpAppResultStore.ts, memoryService.ts, tools/agent_skill_write.ts, tools/agent_skill_delete.ts, devToolsService.ts, creationDraftService.ts, draftService.ts and backup/adapters.ts. The debug CLI cli/debug/refinements.ts has no service, so its rollback keeps the default too.

Files beyond the plan's list, each a pass-through of locks that a caller already has (landing note 14):

  • tools/refinement_rollback.ts, toolAssembly.ts, turnRequestBuilder.ts: the refinement_rollback tool passes locks to rollbackRefinement. turnRequestBuilder puts historyService.locks in the tool assembly's sandbox context. Why: the tool is a production caller of the new option, and its turn has a history service.
  • tools/refinement_rollback.test.ts: a new case that gives the tool a context whose locks record each acquire, and checks that the rollback lock comes from them. Revert proof: without locks: ctx.locks in the tool, the case fails.

Validation

On the stack head 2456cafda9 (PR 13 on PR 12's final head 4eabff0f7b), every gate passed:

  • make static-check and make lint-eslint pass. make crap-check: PASS, 0 violations, 5 warnings on touched hotspots with unchanged CC (total CC of the changed files 5117 -> 5118).
  • bun test of refinement/, workspaceRemoval.test.ts, backup/, sessionUsageService*.test.ts, toolAssembly.test.ts, turnRequestBuilder.test.ts and tools/refinement_rollback.test.ts: 686 pass, 0 fail.
  • The tripwire (scripts/perf/loopTestBaseline.ts --runs 3 --base 4eabff0f7b): 0 trips.
  • make test: 27851 pass, 0 fail in the main run, and every other run passes.

The head 41aab3e5b4 is the same 3 commits rebased on main 6887934d00 (after #7404 and #7417). Its diff against main has the 9 files above. On this head, make static-check passes and the plan tests give 686 pass, 0 fail.

Mutation check

make mutate-check (base 214ec69d6a, so PR 12's lines are in scope too): 187 killed, 113 survived, 0 without a test, 466 not run. No survivor is on a line that this PR changes. I list them in #7326 for PR 17:

  • prepareOrThrow (turnRequestBuilder.ts): 45. The PR adds one locks field.
  • RefineService.applyLocked: 22, RefineService.runLocked: 12, and cacheAuditCommand: 20. These are PR 12's, already listed.
  • rollbackRefinement: 9, and acquireRollbackFileLock: 1 (refinementRollback.ts).
  • withTargetMutationLocks: 2, and createRefinementRollbackTool: 2.
  • Not run (466): past the default budget of 300 mutants per run.

Risks

  • None for behavior: in the app, historyService.locks is nodeSessionLocks, which calls the same acquireProcessFileLock with the same arguments.

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

… from SessionLocks

withTargetMutationLock(s), acquireRollbackFileLock and rollbackRefinement take an optional
SessionLocks (default: the Node layer). Removal, its tombstone rollback, WorkspaceService's refine
serialization lock and SessionUsageService's headless usage write pass their history service's
locks. The 9 other target-lock callers, the rollback tool and the CLI keep the default.
…ocks to the rollback

turnRequestBuilder puts its history service's locks in the tool assembly's sandbox context, and
refinement_rollback passes them to rollbackRefinement for the rollback and target 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-24T14:47:30.160042Z 41aab3e PR opened
🔒 Security Review ✅ Completed 2026-10-24T14:47:33.791420Z 41aab3e 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 · 222.0k tokens (74% 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 24.1s
🟢 a chat survives a restart of the server 2 steps · 4 calls 24.1s
🟢 compaction.e2e.ts · 1 passed 3 steps · 7 calls 43.2s
🟢 /compact replaces the old chat with a summary 3 steps · 7 calls 43.2s
🟢 pluginTrustRestart.e2e.ts · 1 passed 4 steps · 2 calls 9.4s
🟢 a trusted plugin hook stays trusted after the server restarts 4 steps · 2 calls 9.4s
🟢 settingsRoundTrip.e2e.ts · 1 passed 3 steps · 4 calls 29.5s
🟢 a chat survives trips through Settings 3 steps · 4 calls 29.5s
🟢 sidebarPerWorkspace.e2e.ts · 1 passed 4 steps · 1 call 7.4s
🟢 a new workspace starts with its right sidebar collapsed while another keeps it open 4 steps · 1 call 7.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 6.8s
🟢 stream errors show in the chat, and the chat recovers 1 step · 1 call 6.8s
🟢 todoNudge.e2e.ts · 1 passed 2 steps · 5 calls 24.2s
🟢 the agent finishes open TODO items after the turn-end nudge 2 steps · 5 calls 24.2s
🟢 touchAlignment.e2e.ts · 6 passed 6 steps · 6 calls 24.4s
🟢 the Settings close button is level with the title on a touch screen 390px wide 1 step · 1 call 6.2s
🟢 the Settings close button is level with the title on a touch screen 768px wide 1 step · 1 call 4.0s
🟢 Backup settings checkboxes are small and level with their labels on a touch phone 1 step · 1 call 4.3s
🟢 the heartbeat checkbox is small and level with its label on a touch phone 1 step · 1 call 3.4s
🟢 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 3.3s

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

🟢 e2e Electron stories (Haiku): 3 passed

3 agent steps · 3 model calls · 8.1k 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.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 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 · 18.0s · 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
@github-merge-queue
github-merge-queue Bot removed this pull request from the merge queue due to failed status checks Oct 24, 2026
@ThomasK33
ThomasK33 added this pull request to the merge queue Oct 24, 2026
Merged via the queue into main with commit 8a723f7 Oct 24, 2026
33 of 34 checks passed
@ThomasK33
ThomasK33 deleted the phase4/target-locks branch October 24, 2026 15:32
azaj01 pushed a commit to azaj01/mux that referenced this pull request Oct 25, 2026
…O from SessionLocks (coder#7425)

## Summary

The report, failure, transcript and git patch artifact index writes take
the workspace FIFO from `SessionLocks`. It is PR 14 of phase 4. Lock
keys and order do not change: the Node layer gives today's
`workspaceFileLocks`.

## Order

- PR 13 (coder#7423), which the plan puts below it, has merged, so the base
is main. PR 18 (the branch-summary marker, landing note 15) stacks on it
in the native stack.
- Phase 5b PR 3 waits for it.

## Implementation

- `subagentFailureArtifacts.ts`, `subagentReportArtifacts.ts`,
`subagentTranscriptArtifacts.ts`, `subagentGitPatchArtifacts.ts`: each
index write takes an optional `locks` (Node default). In
`subagentReportArtifacts.ts`, the helper `workspaceFileLocksOf` holds
the fallback, so the CC of `upsertSubagentReportArtifact` does not grow.
- `workspaceService.ts`: the child-artifact rollup passes
`this.historyService.locks` for the parent's index writes.

Files beyond the plan's list (landing note 14). The plan said that
`taskService.ts`, `gitPatchArtifactService.ts` and
`taskGitPatchEngine.ts` do not change. These changes close the gap of a
production caller that has a service but keeps the Node default:

- `taskService.ts`: the failure and report upserts pass
`this.historyService.locks`, and so does `new
GitPatchArtifactService(config, locks)`.
- `gitPatchArtifactService.ts`: the constructor requires `locks`, so the
type checker proves that no caller is left. Its 4 upserts pass them.
`gitPatchArtifactService.test.ts` passes `nodeSessionLocks` at its 4
call sites. One test now records the locks that the service takes.
- `taskGitPatchEngine.ts`: `TaskGitPatchApplyConfig` picks
`sessionLocks`, and the applied watermark write passes it.
- `common/utils/tools/tools.ts`: the `sessionLocks` comment names the
watermark write. The `task_apply_git_patch` tool gets the locks through
its tool config, which `turnRequestBuilder` already sets.
- `turnRequestBuilder.ts`, `workflows/WorkflowService.ts`,
`cli/workflow.ts`: each workflow patch config sets `sessionLocks` from
its history service. `WorkflowServiceContext` gains `historyService:
Pick<HistoryService, "locks">`. The ORPC context already has it.
`WorkflowService.context.test.ts` adds it to its test context.

The 4 artifact functions keep an optional `locks`: a required one breaks
about 25 test files.

## Validation

On the stack head `c0831c924e` (PR 14 on PR 13's final head
`41aab3e5b4`), every gate passed:

- `make static-check` and `make lint-eslint` pass. `make crap-check`:
PASS, 0 violations, 7 warnings on touched hotspots (total CC of the
changed files 10107 -> 10112: the 4 artifact functions each gain one
`locks ?? nodeSessionLocks` fallback, and `workspaceFileLocksOf` holds
the report one).
- `bun test` of the 4 `subagent*Artifacts.test.ts`,
`historyService.test.ts`, `taskService.agentReports.test.ts`,
`taskService.handoffAndCleanup.test.ts`,
`gitPatchArtifactService.test.ts`, `taskGitPatchEngine.test.ts`,
`workflows/WorkflowService.context.test.ts`,
`workflows/WorkflowTaskServiceAdapter.test.ts` and
`tools/task_apply_git_patch.test.ts`: 589 pass, 0 fail.
- The tripwire (`scripts/perf/loopTestBaseline.ts --runs 3 --base
41aab3e`): 0 trips.
- `make test`: 0 fail in every run.

The head `0ae88d7ef7` is the same 5 commits rebased on main `d1f9d61489`
(after coder#7423). Its diff against main has the 14 files above. On this
head, `make static-check` passes and the plan tests give 589 pass, 0
fail.

Test: `gitPatchArtifactService.test.ts` gives the service a
`SessionLocks` whose workspace FIFO records each key, and checks that
the parent's index writes take it. Revert proof: with the 4 upserts back
on the Node default, that test fails.

## Mutation check

`make mutate-check` (base `6887934d00`, so PR 13's lines are in scope
too): 167 killed, 128 survived, 4 equivalent, 1 without a test, 682 not
run. No survivor is on a line that this PR changes. I list them in coder#7326
for PR 17.

- The 4 equivalents are the `locks ?? nodeSessionLocks` fallbacks, each
with a `mutation-equivalent` marker: `locks` is a `SessionLocks` object
or undefined, so `??` and `||` agree.
- The 1 without a test is `cli/workflow.ts`, and the local baseline
reports it as failed. That is a local limit: mutate's worker copies hold
only tracked files, and `cli/workflow.test.ts` runs the built
`dist/cli/index.js`, which they lack. The test passes in the checkout
(10 pass, 0 fail). The PR adds one `sessionLocks` field there, with no
operator to mutate. CI's Quality / Mutation is the check of record.
- Survivors: `GitPatchArtifactService.generate` 29,
`maybeStartGenerationUnlocked` 20,
`taskGitPatchEngine.applyProjectPatch` 31 and `applyPatch` 21,
`upsertSubagentReportArtifact` 7, `upsertSubagentFailureArtifact` 3, and
5 more in the other artifact functions. PR 13's survivors
(`rollbackRefinement` 9, `withTargetMutationLocks` 2,
`acquireRollbackFileLock` 1) repeat and are already listed.

## Risks

- None for behavior: in the app, `historyService.locks` is
`nodeSessionLocks`, which gives the same `workspaceFileLocks` FIFO.

---

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

<!-- mux-attribution: model=anthropic:claude-opus-5-5 thinking=high
costs=163.55 -->
azaj01 pushed a commit to azaj01/mux that referenced this pull request Oct 25, 2026
…ugh the HistoryService's SessionFiles (coder#7428)

## Summary

5b PR 5 of phase 5 (Loop host I/O) of the agent-loop refactor.
`SessionUsageService` reads and writes its files through the
`SessionFiles` of its HistoryService (phase 2) instead of `fs/promises`
and `writeFileAtomic`:

- the settled ledger `session-usage.json`,
- the in-flight files of running streams
(`session-usage.in-flight/<key>.json`),
- the headless sidecar `headless-usage.jsonl`.

All three live in the session dir, so they are session files. The
service already takes its locks from the same HistoryService (phase 4).
Now a memory `SessionFiles` layer keeps these files off the disk too.

Behavior does not change: the default is the Node layer
(`nodeSessionFiles`), which calls the same `fs/promises` functions and
`writeFileAtomic`. Its members read their bindings at call time, so the
existing tests that spy on `fs/promises` still intercept these calls.

## Order

Plan order: stack 5b is PR 1 to PR 9. This PR waits for phase 4 PR 13
(coder#7423, merged), which also changed `sessionUsageService.ts`. It uses no
code of 5b PRs 3 and 4, so it opens on its own, with base main.

## Implementation

- `services/sessionUsageService.ts`: a private `files` getter returns
`historyService.files`. Like the `fileLocks` getter beside it, it reads
at each use, not at construction. The 11 `fs/promises` calls and the 2
`writeFileAtomic` calls go through it, and the module-level
`readInFlightUsageFile` takes the files as its first argument. The file
has no `fs` import now.
- `recordHeadlessUsage` checks the removal tombstone through the same
files (`isWorkspaceRemovalTombstoned(rootDir, workspaceId,
this.files)`), like HistoryService's own check. The files argument of
that function came with 5b PR 4 (coder#7426, merged), so a later commit of
this PR adds it after a merge of main.

The plan estimated about 420 lines. This PR adds 115 and removes 18,
because the service reaches the files through the HistoryService it
already has, so no caller changes.

## Validation

- `sessionUsageService.test.ts`: a second new case puts a tombstone only
in the memory volume, and `recordHeadlessUsage` refuses the write
(undefined) and creates no session dir. Without the files argument of
the check, the write succeeds and the case fails.
- `sessionUsageService.test.ts`: a new case builds HistoryService on
memory `SessionFiles`. It records a ledger entry, an in-flight step and
a headless sidecar line. The memory session dir holds all three, and a
restarted service folds the in-flight file into the ledger from memory
(120 input tokens) and deletes it there. The session dir on disk does
not exist. With main's `sessionUsageService.ts`, the case fails (ENOENT
on the memory readdir).
- The plan's tests (`sessionUsageService.test.ts`,
`sessionUsageService.inFlight.test.ts`, `streamManager*.test.ts`) plus
`agentStatusService.test.ts` and `tokenizerService.test.ts`: 513 pass, 0
fail. The 8 other test files that use `SessionUsageService`: 520 pass, 0
fail. The on-disk tests read `session-usage.json` and the in-flight
files from disk, and they pass unchanged.
- `make static-check`, `make crap-check` and `make dry-check` pass.
`make lint-eslint` first flagged an any-typed matcher
(`expect.arrayContaining`) in the new case, and a test-only commit
replaced it with `toContain` checks.
- `make test` (host suite lock) on head 6ce1a31: `bun test src` had
27905 pass, 7 skip, 0 fail. The shared scripts/storybook/vscode run had
729 pass, 1 skip, 0 fail, and the isolated files had 0 fail. Loop-test
tripwire, 3 runs against main: 0 trips.
- After the lint fix and a merge of main be78b8f: `make static-check`
and `make lint-eslint` pass, and the plan's tests have 416 pass, 0 fail.
- After the tombstone commit and a merge of main 9c841fa (coder#7426):
typecheck, `make static-check`, `make lint-eslint`, `make crap-check`
and `make dry-check` pass, and the plan's tests plus
`workspaceRemoval.test.ts` have 428 pass, 0 fail.

---

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

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