fix(sync): mark hydrate / on-demand fetch / format-refresh writes as self-save (v0.7.41)
All checks were successful
boundary-lint / PR boundary-claim (Lint (push) Has been skipped
boundary-lint / duplication-deadline (Layer 1/2) (push) Successful in 19s
boundary-lint / ban-list lint (Lint (push) Successful in 19s
ci / test-health gate (push) Successful in 16s
ci / mutation test (broker) (push) Has been skipped
Release Publish (Gitea session_helper) / verify-release-tag (push) Successful in 17s
ci / rust debug (push) Successful in 2m40s
ci / rust release (push) Successful in 2m45s
ci / python (push) Successful in 1m37s
Release Publish (Gitea session_helper) / publish-linux-x86_64 (push) Successful in 4m21s

User trace (v0.7.40 session) revealed a write echo loop that fires
every time a Sessions cache file is opened or its formatter runs:

  23:05:36.236  hydrate file/read writes remote bytes to local cache
  23:05:36.284  local_watcher.change_detected (= our own write)
  23:05:41.619  watcher → _save_remote_file_for_workspace → file/write
                pushes the same bytes back to the remote
  23:05:41.629  ... and re-enqueues sessions_format_then_pipeline_after_save
  23:05:41.646  exec/once for the new formatter run starts
  23:05:41.905  Sublime aborts (formatter in flight)

Root cause: ``_RECENT_SELF_SAVE_REMOTE_PATHS`` (the cooldown the
local watcher uses to filter our own pushes) was only marked in
``_force_overwrite_remote`` and the regular ``file/write`` path —
not in any of the three places where Sessions writes remote bytes
into the local cache. So every cache materialisation looked like a
genuine user edit to the watcher.

Fix: call ``_mark_recent_self_save(remote_path)`` immediately after
each successful ``open_remote_file_into_local_cache`` call:

* ``_apply_hydrate_result`` — sidebar-placeholder hydrate finish path.
* ``_open_remote_file_for_workspace`` worker — Open Remote File +
  on-demand fetch.
* ``_refresh_local_cache_after_format`` — re-download after a
  successful remote formatter run.

The 5-second cooldown is the same one save echoes already use, so no
new tunable. New test
``test_refresh_local_cache_after_format_marks_self_save_for_watcher``
pins the format-refresh leg; the hydrate / on-demand legs share the
identical one-line pattern.

This is not the root cause of the intermittent macOS abort itself
(the same ``pointer being freed was not allocated`` signature
predates the watcher per the user report), but it removes a steady
stream of spurious file/write + format/lint round-trips that was
clearly increasing the FFI traffic surface area on every file open.

A planned follow-up will replace the four overlapping mechanisms
(``_RECENT_SELF_SAVE_REMOTE_PATHS``, ``_track_g_remote_ref_fingerprints``,
``_track_g_local_branch_baseline``, the ad-hoc
``_ON_DEMAND_FETCH_BYPASS`` flag) with a single origin-tagged
last-write-wins log so this whole class of "we wrote it ourselves"
filtering is unified.

1,368 tests pass; coverage 80.69% (gate=80%).

Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
This commit is contained in:
2026-05-03 23:30:14 +09:00
parent 0dc93212de
commit 836d7e4a73
6 changed files with 50 additions and 9 deletions

View File

@@ -1,6 +1,6 @@
[project]
name = "sessions-sublime"
version = "0.7.40"
version = "0.7.41"
description = "Sublime-facing Python code for Sessions."
requires-python = ">=3.8"
license = {text = "MIT"}

12
rust/Cargo.lock generated
View File

@@ -221,7 +221,7 @@ checksum = "32a66949e030da00e8c7d4434b251670a91556f4144941d37452769c25d58a53"
[[package]]
name = "local_bridge"
version = "0.7.40"
version = "0.7.41"
dependencies = [
"base64",
"glob",
@@ -432,7 +432,7 @@ dependencies = [
[[package]]
name = "session_helper"
version = "0.7.40"
version = "0.7.41"
dependencies = [
"base64",
"notify",
@@ -443,7 +443,7 @@ dependencies = [
[[package]]
name = "session_protocol"
version = "0.7.40"
version = "0.7.41"
dependencies = [
"base64",
"serde",
@@ -452,14 +452,14 @@ dependencies = [
[[package]]
name = "sessions_askpass"
version = "0.7.40"
version = "0.7.41"
dependencies = [
"tempfile",
]
[[package]]
name = "sessions_native"
version = "0.7.40"
version = "0.7.41"
dependencies = [
"base64",
"notify",
@@ -773,7 +773,7 @@ dependencies = [
[[package]]
name = "workspace_identity"
version = "0.7.40"
version = "0.7.41"
[[package]]
name = "zmij"

View File

@@ -12,7 +12,7 @@ resolver = "2"
[workspace.package]
edition = "2024"
license = "MIT"
version = "0.7.40"
version = "0.7.41"
authors = ["Myeongseon Choi <key262yek@gmail.com>"]
repository = "https://git.teahaven.kr/sublime-rs/sessions"
homepage = "https://git.teahaven.kr/sublime-rs/sessions"

View File

@@ -1659,6 +1659,12 @@ def _apply_hydrate_result(
if opened.outcome is OpenOutcome.OK:
if opened.remote_metadata is not None:
_write_remote_metadata_sidecar(path, opened.remote_metadata)
# Hydrate just wrote remote bytes into the local cache. The local
# filesystem watcher is about to fire ``change_detected`` for that
# write — without this mark the watcher's poller treats it as a
# genuine user edit and pushes the same bytes back to the remote
# (= self-save loop, also re-fires the format/lint pipeline).
_mark_recent_self_save(remote)
_HYDRATE_REVERT_COOLDOWN[path_str] = time.monotonic()
# ``revert`` re-loads the buffer from disk and that wipes the
# caret + selection. When LSP goto-definition opens a fresh
@@ -2078,6 +2084,9 @@ class SessionsOnDemandFetchListener(sublime_plugin.EventListener):
_write_remote_metadata_sidecar(
local_path, opened.remote_metadata
)
# Suppress the local-watcher echo for the bytes we
# just downloaded — same reason as ``_apply_hydrate_result``.
_mark_recent_self_save(remote_path)
_ON_DEMAND_FETCH_BYPASS.active = True
try:
open_args: Dict[str, object] = {"file": local_str}
@@ -5473,6 +5482,11 @@ def _refresh_local_cache_after_format(
return
if opened.outcome is OpenOutcome.OK and opened.remote_metadata is not None:
_write_remote_metadata_sidecar(local_cache_path, opened.remote_metadata)
# Format-after-save just re-downloaded the formatted bytes
# into the local cache. Mark so the local watcher does not
# treat that write as a fresh user edit and trigger a
# second push + format pass.
_mark_recent_self_save(remote_path)
_set_timeout(finish, 0)

View File

@@ -947,6 +947,33 @@ def test_refresh_local_cache_after_format_remote_not_found_warns(
assert any("no longer exists" in m for m in status_messages)
def test_refresh_local_cache_after_format_marks_self_save_for_watcher(
tmp_path: Path, monkeypatch
) -> None:
"""Refresh-after-format must mark the path as self-save so the local
watcher does not echo the just-downloaded bytes back to the remote
(= the v0.7.40 trace's save loop: hydrate writes → watcher fires →
spurious file/write → format pipeline → repeat)."""
window, ctx = _present_tool_result_ctx(tmp_path, monkeypatch)
expected_local = ctx.local_cache_root / "a.py"
expected_local.parent.mkdir(parents=True, exist_ok=True)
expected_local.write_text("x\n", encoding="utf-8")
metadata = RemoteFileMetadata(
mtime_ns=1, size_bytes=2, kind=RemoteFileKind.REGULAR_FILE, unix_mode=33188
)
_stub_refresh_cache_lower_layers(
monkeypatch,
opened=OpenFileResult(
outcome=OpenOutcome.OK,
local_cache_path=expected_local,
remote_metadata=metadata,
),
)
commands._RECENT_SELF_SAVE_REMOTE_PATHS.clear()
commands._refresh_local_cache_after_format(window, ctx, "/srv/ws/a.py")
assert commands._is_recent_self_save("/srv/ws/a.py")
# --- _apply_inline_diagnostics early-return paths ---

2
uv.lock generated
View File

@@ -854,7 +854,7 @@ wheels = [
[[package]]
name = "sessions-sublime"
version = "0.7.40"
version = "0.7.41"
source = { virtual = "." }
[package.dev-dependencies]