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
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:
@@ -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
12
rust/Cargo.lock
generated
@@ -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"
|
||||
|
||||
@@ -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"
|
||||
|
||||
@@ -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)
|
||||
|
||||
|
||||
@@ -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 ---
|
||||
|
||||
|
||||
|
||||
Reference in New Issue
Block a user