Skip to content

Commit dd6de1d

Browse files
dsarnoclaude
andcommitted
Fix script edit tools retrying non-idempotent commands during reload
When Unity enters domain reload after a script edit, the retry loop in send_command_with_retry would re-send the identical edit command up to 40 times, duplicating insert_method/anchor_insert edits. Pass retry_on_reload=False on all script-mutating send calls since the edit lands on disk before the reload triggers. Also fix _flip_async coroutine in apply_text_edits that was passed as a threading.Thread target — the coroutine was never awaited. Replace with asyncio.create_task so the sentinel reload flip actually executes. Fixes CoplayDev#790 Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
1 parent d2c2808 commit dd6de1d

File tree

2 files changed

+17
-4
lines changed

2 files changed

+17
-4
lines changed

Server/src/services/tools/manage_script.py

Lines changed: 12 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -11,6 +11,9 @@
1111
from transport.unity_transport import send_with_unity_instance
1212
import transport.legacy.unity_connection
1313

14+
# Strong references to fire-and-forget tasks to prevent GC before completion
15+
_background_tasks: set = set()
16+
1417

1518
def _split_uri(uri: str) -> tuple[str, str]:
1619
"""Split an incoming URI or path into (name, directory) suitable for Unity.
@@ -326,6 +329,7 @@ def _le(a: tuple[int, int], b: tuple[int, int]) -> bool:
326329
unity_instance,
327330
"manage_script",
328331
params,
332+
retry_on_reload=False,
329333
)
330334
if isinstance(resp, dict):
331335
data = resp.setdefault("data", {})
@@ -335,8 +339,7 @@ def _le(a: tuple[int, int], b: tuple[int, int]) -> bool:
335339
if resp.get("success") and (options or {}).get("force_sentinel_reload"):
336340
# Optional: flip sentinel via menu if explicitly requested
337341
try:
338-
import threading
339-
import time
342+
import asyncio
340343
import json
341344
import glob
342345
import os
@@ -354,7 +357,7 @@ def _latest_status() -> dict | None:
354357

355358
async def _flip_async():
356359
try:
357-
time.sleep(0.1)
360+
await asyncio.sleep(0.1)
358361
st = _latest_status()
359362
if st and st.get("reloading"):
360363
return
@@ -367,7 +370,9 @@ async def _flip_async():
367370
)
368371
except Exception:
369372
pass
370-
threading.Thread(target=_flip_async, daemon=True).start()
373+
task = asyncio.create_task(_flip_async())
374+
_background_tasks.add(task)
375+
task.add_done_callback(_background_tasks.discard)
371376
except Exception:
372377
pass
373378
return resp
@@ -423,6 +428,7 @@ async def create_script(
423428
unity_instance,
424429
"manage_script",
425430
params,
431+
retry_on_reload=False,
426432
)
427433
return resp if isinstance(resp, dict) else {"success": False, "message": str(resp)}
428434

@@ -452,6 +458,7 @@ async def delete_script(
452458
unity_instance,
453459
"manage_script",
454460
params,
461+
retry_on_reload=False,
455462
)
456463
return resp if isinstance(resp, dict) else {"success": False, "message": str(resp)}
457464

@@ -551,6 +558,7 @@ async def manage_script(
551558
unity_instance,
552559
"manage_script",
553560
params,
561+
retry_on_reload=(action == "read"),
554562
)
555563

556564
if isinstance(response, dict):

Server/src/services/tools/script_apply_edits.py

Lines changed: 5 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -970,6 +970,7 @@ def error_with_hint(message: str, expected: dict[str, Any], suggestion: dict[str
970970
unity_instance,
971971
"manage_script",
972972
params_struct,
973+
retry_on_reload=False,
973974
)
974975
if isinstance(resp_struct, dict) and resp_struct.get("success"):
975976
pass # Optional sentinel reload removed (deprecated)
@@ -1110,6 +1111,7 @@ def _expand_dollars(rep: str, _m=m) -> str:
11101111
unity_instance,
11111112
"manage_script",
11121113
params_text,
1114+
retry_on_reload=False,
11131115
)
11141116
if not (isinstance(resp_text, dict) and resp_text.get("success")):
11151117
return _with_norm(resp_text if isinstance(resp_text, dict) else {"success": False, "message": str(resp_text)}, normalized_for_echo, routing="mixed/text-first")
@@ -1135,6 +1137,7 @@ def _expand_dollars(rep: str, _m=m) -> str:
11351137
unity_instance,
11361138
"manage_script",
11371139
params_struct,
1140+
retry_on_reload=False,
11381141
)
11391142
if isinstance(resp_struct, dict) and resp_struct.get("success"):
11401143
pass # Optional sentinel reload removed (deprecated)
@@ -1267,6 +1270,7 @@ def _expand_dollars(rep: str, _m=m) -> str:
12671270
unity_instance,
12681271
"manage_script",
12691272
params,
1273+
retry_on_reload=False,
12701274
)
12711275
if isinstance(resp, dict) and resp.get("success"):
12721276
pass # Optional sentinel reload removed (deprecated)
@@ -1356,6 +1360,7 @@ def _expand_dollars(rep: str, _m=m) -> str:
13561360
unity_instance,
13571361
"manage_script",
13581362
params,
1363+
retry_on_reload=False,
13591364
)
13601365
if isinstance(write_resp, dict) and write_resp.get("success"):
13611366
pass # Optional sentinel reload removed (deprecated)

0 commit comments

Comments
 (0)