diff --git a/docs/ISSUES.md b/docs/ISSUES.md
index 068448a7..a7b1e78a 100644
--- a/docs/ISSUES.md
+++ b/docs/ISSUES.md
@@ -46,9 +46,24 @@ Copy this block when adding a new issue:
---
-## #174 — Door Use dies after the first attempt: close-range deferred uses silently swallowed (player MoveToManager never completes)
+## #174 — Door Use dies after the first jump: the RemoveLinkAnimations seam stripped animations without retail's queue drain
-**Status:** OPEN — root under investigation (wire + log evidence in hand; needs one `ACDREAM_PROBE_AUTOWALK=1` round to pick between the two candidate mechanisms)
+**Status:** 🟡 FIX SHIPPED 2026-07-05 (same session) — pending user gate (jump around, then use the Facility Hub door from close AND from ~3 m).
+**FIX:** the `MotionInterpreter.RemoveLinkAnimations` seam is retail
+`CPhysicsObj::RemoveLinkAnimations` 0x0050fe20 — a tailcall to
+`CPartArray::HandleEnterWorld` 0x00517d70 → `MotionTableManager::
+HandleEnterWorld` 0x0051bdd0: strip the sequence's link animations AND drain
+`pending_animations` completely (each pop relays MotionDone → the interp pops
+its `pending_motions` node in lockstep). acdream bound the seam to the BARE
+sequence strip (`RemoveAllLinkAnimations`), so every jump's LeaveGround
+removed the animations that queued manager nodes were counting down on —
+orphaning them and permanently damming BOTH queues; `MotionsPending()` then
+starved every armed moveto (the far-range walk-to-door and the close-range
+use turn — both door faces below). Rebound at both production sites
+(GameWindow remote bindings + the player's EnterPlayerModeNow block) to
+`Manager.HandleEnterWorld()`; harness mirrors updated; pins
+`Issue174LinkStripDrainTests` (seam drains both queues; new motions queue +
+complete after). The `UseDone` (0x01C7) display gap stays open below.
**Severity:** HIGH (can't open doors reliably → blocks the #137 door acceptance + normal play)
**Filed:** 2026-07-05
**Component:** interaction — B.4b Use pipeline / AP-23 speculative moveto deferral (R5-V5 facade)
diff --git a/src/AcDream.App/Rendering/GameWindow.cs b/src/AcDream.App/Rendering/GameWindow.cs
index d470d95a..cd90737a 100644
--- a/src/AcDream.App/Rendering/GameWindow.cs
+++ b/src/AcDream.App/Rendering/GameWindow.cs
@@ -4288,7 +4288,20 @@ public sealed class GameWindow : IDisposable
rmForSink.ObservedOmega = System.Numerics.Vector3.Zero,
};
rm.Motion.DefaultSink = rm.Sink;
- rm.Motion.RemoveLinkAnimations = ae.Sequencer.RemoveAllLinkAnimations;
+ // #174 (2026-07-05): the RemoveLinkAnimations seam is retail
+ // CPhysicsObj::RemoveLinkAnimations 0x0050fe20 — a TAILCALL to
+ // CPartArray::HandleEnterWorld 0x00517d70 →
+ // MotionTableManager::HandleEnterWorld 0x0051bdd0: strip the
+ // sequence's link animations AND drain pending_animations
+ // completely (each pop fires MotionDone → CMotionInterp pops its
+ // pending_motions node in lockstep). The previous binding was the
+ // bare sequence strip — every LeaveGround (jump) stripped the
+ // animations that queued manager nodes were counting down on,
+ // orphaning them (NumAnims>0, anims gone) and permanently damming
+ // BOTH queues: MotionsPending() never drained again, so every
+ // armed moveto (ACE's walk-to-door mt-6, the close-range use turn)
+ // starved — the "door only works on a fresh session" bug.
+ rm.Motion.RemoveLinkAnimations = () => ae.Sequencer.Manager.HandleEnterWorld();
rm.Motion.InitializeMotionTables = () => ae.Sequencer.Manager.InitializeState();
// R3-W5: the per-op zero-tick flush (CPhysicsObj::CheckForCompletedMotions
// 0x0050fe30 -> MotionTableManager.CheckForCompletedMotions).
@@ -13697,7 +13710,14 @@ public sealed class GameWindow : IDisposable
// R3-W4: bind the player interp's retail seams to the player
// sequencer — LeaveGround/HitGround strip transition links here
// (the K-fix18 replacement).
- _playerController.Motion.RemoveLinkAnimations = playerSeq.RemoveAllLinkAnimations;
+ // #174 (2026-07-05): the seam is HandleEnterWorld (strip + FULL
+ // queue drain), not the bare sequence strip — see the remote
+ // bind site's comment (retail 0x0050fe20 → 0x00517d70 →
+ // 0x0051bdd0). The bare strip orphaned every pending manager
+ // node on each jump's LeaveGround; the local player's
+ // pending_motions then never drained and every armed moveto
+ // starved (#174: doors unusable after the first jump/run).
+ _playerController.Motion.RemoveLinkAnimations = () => playerSeq.Manager.HandleEnterWorld();
_playerController.Motion.InitializeMotionTables =
() => playerSeq.Manager.InitializeState();
_playerController.Motion.CheckForCompletedMotions =
diff --git a/tests/AcDream.Core.Tests/Input/PlayerMoveToCutoverTests.cs b/tests/AcDream.Core.Tests/Input/PlayerMoveToCutoverTests.cs
index e5146d20..1a32b33d 100644
--- a/tests/AcDream.Core.Tests/Input/PlayerMoveToCutoverTests.cs
+++ b/tests/AcDream.Core.Tests/Input/PlayerMoveToCutoverTests.cs
@@ -122,7 +122,9 @@ public class PlayerMoveToCutoverTests
// the A9 pending_motions node (a null sink orphans it and the
// MoveToManager wait-for-anims gate never opens — the live stall).
controller.Motion.DefaultSink = new MotionTableDispatchSink(seq);
- controller.Motion.RemoveLinkAnimations = seq.RemoveAllLinkAnimations;
+ // #174: production wiring — HandleEnterWorld (strip + drain), not
+ // the bare sequence strip (which orphaned pending manager nodes).
+ controller.Motion.RemoveLinkAnimations = () => seq.Manager.HandleEnterWorld();
controller.Motion.InitializeMotionTables = () => seq.Manager.InitializeState();
controller.Motion.CheckForCompletedMotions = seq.Manager.CheckForCompletedMotions;
controller.SetPosition(new Vector3(96f, 96f, 50f), 0x0001);
diff --git a/tests/AcDream.Core.Tests/Input/W6EdgeDrivenMovementTests.cs b/tests/AcDream.Core.Tests/Input/W6EdgeDrivenMovementTests.cs
index 3198da4a..aa80104e 100644
--- a/tests/AcDream.Core.Tests/Input/W6EdgeDrivenMovementTests.cs
+++ b/tests/AcDream.Core.Tests/Input/W6EdgeDrivenMovementTests.cs
@@ -103,7 +103,9 @@ public class W6EdgeDrivenMovementTests
// dispatch, else its A9 pending_motions node is orphaned).
controller.Motion.DefaultSink = new MotionTableDispatchSink(seq);
var s = seq;
- controller.Motion.RemoveLinkAnimations = s.RemoveAllLinkAnimations;
+ // #174: production wiring — HandleEnterWorld (strip + drain), not
+ // the bare sequence strip (which orphaned pending manager nodes).
+ controller.Motion.RemoveLinkAnimations = () => s.Manager.HandleEnterWorld();
controller.Motion.InitializeMotionTables = () => s.Manager.InitializeState();
controller.Motion.CheckForCompletedMotions = s.Manager.CheckForCompletedMotions;
controller.SetPosition(new Vector3(96f, 96f, 50f), 0x0001);
diff --git a/tests/AcDream.Core.Tests/Physics/Motion/Issue174LinkStripDrainTests.cs b/tests/AcDream.Core.Tests/Physics/Motion/Issue174LinkStripDrainTests.cs
new file mode 100644
index 00000000..dadba9a3
--- /dev/null
+++ b/tests/AcDream.Core.Tests/Physics/Motion/Issue174LinkStripDrainTests.cs
@@ -0,0 +1,97 @@
+using AcDream.Core.Physics;
+using AcDream.Core.Physics.Motion;
+using Xunit;
+using Xunit.Abstractions;
+
+namespace AcDream.Core.Tests.Physics.Motion;
+
+// ─────────────────────────────────────────────────────────────────────────────
+// #174 pin (2026-07-05): the RemoveLinkAnimations seam must be retail
+// CPhysicsObj::RemoveLinkAnimations 0x0050fe20 — a TAILCALL to
+// CPartArray::HandleEnterWorld 0x00517d70 → MotionTableManager::
+// HandleEnterWorld 0x0051bdd0: CSequence::remove_all_link_animations PLUS a
+// full pending_animations drain (`while (head) AnimationDone(0)`), each pop
+// relaying MotionDone → CMotionInterp pops its pending_motions node in
+// lockstep.
+//
+// The pre-fix binding was the bare sequence strip: every LeaveGround (jump)
+// removed the link animations that queued MotionTableManager nodes were
+// counting down on, orphaning them (NumAnims > 0, animations gone). Both
+// queues then dammed permanently — MotionsPending() never drained at rest —
+// and BeginTurnToHeading/BeginMoveForward (retail 0x00529b90 motions_pending
+// gate) starved every armed moveto: ACE's walk-to-door mt-6 armed but the
+// body never walked; the close-range Use turn never completed so the
+// deferred action was silently eaten. Live evidence: launch-174-autowalk.log
+// (last player pending=False at the first MovementJump press; old jump
+// motions still completing at rest minutes later).
+// ─────────────────────────────────────────────────────────────────────────────
+public class Issue174LinkStripDrainTests
+{
+ private readonly ITestOutputHelper _out;
+ public Issue174LinkStripDrainTests(ITestOutputHelper output) => _out = output;
+
+ ///
+ /// The jam repro: queue motions (link + cycle nodes land in BOTH the
+ /// interp's pending_motions and the manager's pending_animations), then
+ /// fire the LeaveGround-side seam. With the retail HandleEnterWorld
+ /// binding both queues drain to empty; the pre-fix bare-strip binding
+ /// left both non-empty forever.
+ ///
+ [Fact]
+ public void RemoveLinkAnimationsSeam_DrainsBothQueues()
+ {
+ var h = new RemoteChaseHarness(_out);
+
+ // Drive a motion burst — walk, run, stop — the shape a player's
+ // pre-jump input produces. Each successful dispatch pairs an interp
+ // node with a manager node.
+ var p = new MovementParameters();
+ h.Interp.DoMotion(MotionCommand.WalkForward, p);
+ h.Interp.set_hold_run(true, interrupt: false);
+ h.Interp.StopMotion(MotionCommand.WalkForward, p);
+
+ Assert.True(h.Interp.MotionsPending(),
+ "precondition: the burst must leave pending interp nodes");
+ Assert.NotEmpty(h.Seq.Manager.PendingAnimations);
+
+ // The LeaveGround seam (retail CMotionInterp::LeaveGround 0x00528b00
+ // fires CPhysicsObj::RemoveLinkAnimations).
+ h.Interp.RemoveLinkAnimations!.Invoke();
+
+ Assert.False(h.Interp.MotionsPending(),
+ "HandleEnterWorld's drain must pop every pending interp node " +
+ "(retail: each AnimationDone(0) relays MotionDone)");
+ Assert.Empty(h.Seq.Manager.PendingAnimations);
+ }
+
+ ///
+ /// The post-jump livability pin: after the seam fires mid-activity, a
+ /// NEW moveto-style dispatch must be able to queue and complete — the
+ /// #174 symptom was that BeginTurnToHeading's motions_pending gate never
+ /// re-opened after a jump, permanently starving armed movetos.
+ ///
+ [Fact]
+ public void AfterSeamDrain_NewMotionsQueueAndComplete()
+ {
+ var h = new RemoteChaseHarness(_out);
+ var p = new MovementParameters();
+
+ // Pre-jump activity, then the jump's LeaveGround strip+drain.
+ h.Interp.DoMotion(MotionCommand.WalkForward, p);
+ h.Interp.RemoveLinkAnimations!.Invoke();
+ Assert.False(h.Interp.MotionsPending());
+
+ // A fresh dispatch (the armed moveto's turn) queues...
+ h.Interp.DoMotion(MotionCommand.TurnRight, p);
+ Assert.True(h.Interp.MotionsPending());
+
+ // ...and the normal completion path (the manager's queue feeding
+ // MotionDone) drains it — the gate re-opens.
+ while (h.Seq.Manager.PendingAnimations.GetEnumerator() is var e && e.MoveNext())
+ h.Seq.Manager.AnimationDone(success: true);
+
+ h.Seq.Manager.CheckForCompletedMotions();
+ Assert.False(h.Interp.MotionsPending(),
+ "the normal AnimationDone → MotionDone chain must drain the new node");
+ }
+}
diff --git a/tests/AcDream.Core.Tests/Physics/Motion/RemoteChaseEndToEndHarnessTests.cs b/tests/AcDream.Core.Tests/Physics/Motion/RemoteChaseEndToEndHarnessTests.cs
index 1c113938..a1480435 100644
--- a/tests/AcDream.Core.Tests/Physics/Motion/RemoteChaseEndToEndHarnessTests.cs
+++ b/tests/AcDream.Core.Tests/Physics/Motion/RemoteChaseEndToEndHarnessTests.cs
@@ -174,7 +174,10 @@ internal sealed class RemoteChaseHarness
TurnStopped = () => ObservedOmega = Vector3.Zero,
};
Interp.DefaultSink = Sink;
- Interp.RemoveLinkAnimations = Seq.RemoveAllLinkAnimations;
+ // #174: production binds the seam to Manager.HandleEnterWorld
+ // (strip + full queue drain, retail 0x0050fe20 → 0x0051bdd0) —
+ // the bare sequence strip orphaned pending manager nodes.
+ Interp.RemoveLinkAnimations = () => Seq.Manager.HandleEnterWorld();
Interp.InitializeMotionTables = () => Seq.Manager.InitializeState();
Interp.CheckForCompletedMotions = Seq.Manager.CheckForCompletedMotions;