Skip to content

fix(code): serialize dcode self-upgrades across processes - #5252

Merged
Mason Daugherty (mdrxy) merged 3 commits into
mainfrom
mdrxy/code/update-cross-process-lock
Aug 11, 2026
Merged

fix(code): serialize dcode self-upgrades across processes#5252
Mason Daugherty (mdrxy) merged 3 commits into
mainfrom
mdrxy/code/update-cross-process-lock

Conversation

@mdrxy

@mdrxy Mason Daugherty (mdrxy) commented Aug 3, 2026

Copy link
Copy Markdown
Member

Closes #5250

When several dcode terminals are open at once, only one of them installs an update now — the others keep running the version they launched with and pick up the new one next launch.


Nothing coordinated the startup auto-update across processes. Every launching terminal independently decided an update was available and ran its own uv tool install -U against the same tool environment. The only lock in the area, _environment_mutation_lock, is an in-process asyncio.Lock.

Two things partly hid this, neither of which is a lock. A terminal opened after another finished upgrading self-skips, because is_installed_version_at_least reads the on-disk distribution metadata — that covers "open a new tab later" but not "open four tabs at once". And uv takes its own lock on the tool directory, so concurrent installs queue rather than corrupt; but they wait, and a queued loser can exceed the upgrade timeout, be reported to the user as Auto-update failed, and write a 24-hour cooldown suppressing its next legitimate attempts. Upgrading in place is also actively hostile to running sessions: _prewarm_deferred_imports already carries a defensive guard because a concurrent upgrade can make a not-yet-imported module transiently absent and crash the TUI.

update_install_lock is a non-blocking context manager pairing a process-local threading.Lock with a FileLock(thread_local=False) on a dedicated lock file in the state directory — the same shape as _approval_state_lock and _trust_store_lock, so no new dependency. It yields whether the caller may install; losers return immediately rather than stalling startup behind an install they do not need, and record no failure cooldown, since nothing failed. It is taken by all three paths that replace the installed package: startup auto-update, /update, and the update notification's install action.

Test plan
  • update_install_lock exclusion is tested against a real second OS process holding the same lock file, not a patched filelock.
  • Covers release on normal exit, release after an exception, refusal of a second in-process holder, the fail-open path, the startup skip recording no cooldown, lock release before the re-exec, and the two app-level install paths deferring and releasing.
  • The unit-test state-dir isolation fixture now redirects the lock file, so no test can contend with a genuinely running dcode.

Made by Open SWE

Concurrently launched terminals each ran their own `uv tool install -U`
against one tool environment, multiplying startup latency and turning a
sibling's successful upgrade into a bogus "Auto-update failed" plus a 24h
cooldown. Take a non-blocking cross-process file lock so exactly one
process installs and the rest launch on the version they already have.

Co-authored-by: open-swe[bot] <open-swe@users.noreply.github.com>
@github-actions github-actions Bot added dcode Related to `deepagents-code` fix A bug fix (PATCH) internal User is a member of the `langchain-ai` GitHub organization size: L 500-999 LOC labels Aug 3, 2026
@mdrxy
Mason Daugherty (mdrxy) marked this pull request as ready for review August 11, 2026 05:44
Extend the cross-process update lock to the two remaining paths that
replace the installed package, and make its failure modes diagnosable.

`dcode update` and `/update --deps` both run `uv tool install` against the
same tool environment as an upgrade, so neither could be left unlocked
without reopening the race the lock exists to close.

Observability: both refusal branches logged at DEBUG, and the package
logger sits at INFO unless DEEPAGENTS_CODE_DEBUG is set, so a skipped
install left no evidence at all. A failed release was swallowed outright
— and it is not harmless, since `UnixFileLock._release` clears its fd
handle before unlocking, leaking an fd that still holds the lock and
making every later attempt in the session report a phantom concurrent
install.

Also:

- Replace the notification path's `ExitStack`, which was never closed on
  the refusal branch, with a plain `with`. Release had been delegated to
  refcount finalization of the suspended generator; a retained traceback
  would have pinned the process-global thread lock indefinitely.
- Split the `mkdir`/`chmod` guard. A `chmod` refusal (CIFS/exFAT) is a
  hardening failure, not a locking failure, and abandoning the lock for
  it disabled the protection on every launch.
- Document that `except Timeout` must precede `except OSError`: `Timeout`
  subclasses `TimeoutError`, hence `OSError`, so reordering turns every
  contended case into a fail-open.
- Fail open when `filelock` cannot be imported, which a half-written
  `site-packages` mid-upgrade can cause.
- Correct the `os.execv` rationale. Exec drops the lock on its own, since
  filelock's fd is non-inheritable under PEP 446. Releasing explicitly is
  still right — correctness should not rest on a dependency's fd
  behavior, and the restart can raise — but the stated mechanism was
  wrong, and the test docstring repeated it.
- Stop blaming another session for in-process refusals in the TUI message.

Tests: assert the lock is held *during* the install at all four call
sites, not merely checked beforehand — shrinking any `with` to the
boolean check alone previously left the whole suite green. Cover the
acquire-failure fail-open, release failure, missing filelock, chmod
refusal, real cross-thread exclusion, the production lock path, and its
directory mode. The subprocess helper is now a context manager, so it no
longer leaks a pipe.
@github-actions github-actions Bot added size: XL 1000+ LOC and removed size: L 500-999 LOC labels Aug 11, 2026
@mdrxy
Mason Daugherty (mdrxy) merged commit e12acba into main Aug 11, 2026
71 checks passed
@mdrxy
Mason Daugherty (mdrxy) deleted the mdrxy/code/update-cross-process-lock branch August 11, 2026 06:29
Mason Daugherty (mdrxy) pushed a commit that referenced this pull request Aug 12, 2026
> [!CAUTION]
> Merging this PR will automatically publish to **PyPI** and create a
**GitHub release**.

For the full release process, see
[`.github/RELEASING.md`](https://github.com/langchain-ai/deepagents/blob/main/.github/RELEASING.md).

---

_Release notes preview: keep this section in sync with the package
`CHANGELOG.md`. Publish reads the merged CHANGELOG via `release.yml`,
not this PR description — keep them aligned anyway so the PR stays an
accurate historical record for reviewers and anyone returning later._

---


##
[0.1.55](deepagents-code==0.1.54...deepagents-code==0.1.55)
(2026-08-12)

### Features

- Added a `/context` usage report for inspecting context consumption
([#5407](#5407)).
- Added a cache and context status row for at-a-glance session state
([#5408](#5408)).
- Added configurable warnings when a session exceeds the configured cost
threshold
([#5405](#5405)).
- Added support for persisting and reconfiguring ACP sessions
([#5366](#5366)).
- Added automatic updates for installed plugins
([#5368](#5368)).
- Added a toggle for diff line numbers
([#5427](#5427)).
- `Ctrl+S` in `/auto model` now stores `[models].auto_classifier`
([#5313](#5313)).

### Fixes

- Restored edit diffs in resumed threads
([#5391](#5391)).
- Added a resume hint after crashes
([#5412](#5412)).
- Clarified the project hooks trust prompt and stopped prompting for
user hooks
([#5426](#5426)).
- Cleared dynamic subagents on the next turn
([#5437](#5437)).
- Improved grouped tool summaries by counting distinct targets
([#5409](#5409)).
- Improved ask-user choice wrapping and selection styling
([#5442](#5442)).
- Serialized `dcode` self-upgrades across processes
([#5252](#5252)).
- Added warnings for stale dependencies in editable installs
([#5386](#5386)).
- Hid incomplete extras from version output
([#5352](#5352)).
- Removed the optional-provider startup tip
([#5421](#5421)).
- Removed the “Message restored to input” toast
([#5253](#5253)).

_End release notes preview._

---

> [!NOTE]
> A **New Contributors** section is appended to the GitHub release notes
automatically at publish time (see [Release
Pipeline](https://github.com/langchain-ai/deepagents/blob/main/.github/RELEASING.md#release-pipeline),
step 2).

---------

Co-authored-by: github-actions[bot] <41898282+github-actions[bot]@users.noreply.github.com>
Co-authored-by: langchain-oss-automated-triage[bot] <248757908+langchain-oss-automated-triage[bot]@users.noreply.github.com>
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

dcode Related to `deepagents-code` fix A bug fix (PATCH) internal User is a member of the `langchain-ai` GitHub organization size: XL 1000+ LOC

Projects

None yet

Development

Successfully merging this pull request may close these issues.

Startup auto-update has no cross-process lock; concurrent dcode launches all upgrade

1 participant