TypeScript: make the tsserver progress-start grace configurable, raise the default

find_referencing_symbols/request_references could return incomplete results on
large TypeScript projects. _wait_for_indexing_start_or_completion waits only
_INDEXING_START_GRACE_S (hardcoded 2.0s) for tsserver to *start* emitting
$/progress after opening files; if nothing has appeared by then, it assumes no
indexing is needed and proceeds immediately. On a large project, tsserver can
take longer than that just to resolve the project graph before it even creates
the first progress token, so the first cross-file reference query can race a
project that is still loading.

Every caller of _wait_for_indexing_start_or_completion (the base TypeScript
path and the Svelte companion) always calls expect_indexing() first, so the
"no progress observed" branch is reached specifically when indexing is already
expected; it cannot distinguish "nothing to do" from "not started yet",
because the two look identical from the client for as long as tsserver stays
quiet. That ambiguity cannot be resolved from client-observable signals alone,
so this makes the grace an ls_specific_settings knob (`indexing_start_grace`),
the same way indexing_timeout and server_ready_timeout already are, and raises
the default from 2.0s to 5.0s.

Fixes #1586
This commit is contained in:
Amir Fathi authored and Dr. Dominik Jain committed 2026-07-24 00:27:36 +02:00
1 parent 05468d9685
commit 61ca9ba5de
4 files changed
+63 -7

No files matched your search

+9
View File
@@ -16,6 +16,15 @@ Status of the `main` branch. Changes prior to the next official version change w
* JetBrains:
- `jet_brains_find_symbol`: Disallow wildcard-only search, delegating to overview tool if request is for file
* Language Servers:
- `typescript`: Fix: on large projects, the first `find_referencing_symbols`/`request_references` call
could silently race tsserver's project load and return incomplete results, because the fixed 2s
grace for tsserver to *start* reporting `$/progress` (distinct from the separate, already
configurable `indexing_timeout` used to wait for it to *drain*) was hardcoded and not large enough
for projects where the initial project-graph resolution itself takes longer than that. The grace
is now `indexing_start_grace` (default 5.0s), configurable the same way as `indexing_timeout` and
`server_ready_timeout` #1586
* Hooks:
- Add `serena-hooks --client=grok`, including Grok-native PreToolUse allow/deny output.
- PreToolUse remind hook: coerce non-string shell command values instead of failing, and recognize
+2 -1
View File
@@ -1118,8 +1118,9 @@ Supported settings:
| `typescript_version` | `5.9.3` | Override the bundled `typescript` npm package version Serena installs when `ls_path` is not set. |
| `typescript_language_server_version` | `5.1.3` | Override the bundled `typescript-language-server` npm package version Serena installs when `ls_path` is not set. |
| `npm_registry` | `null` | Override the npm registry Serena uses for the managed install. |
| `indexing_timeout` | `30.0` | Timeout in seconds for waiting on tsserver's `$/progress` project-indexing signal (both at startup and before the first cross-file reference query). If indexing does not complete within this window, Serena logs a warning and proceeds anyway. Increase it for very large projects. |
| `indexing_timeout` | `30.0` | Timeout in seconds for waiting on tsserver's `$/progress` project-indexing signal to *drain* once it has started (both at startup and before the first cross-file reference query). If indexing does not complete within this window, Serena logs a warning and proceeds anyway. Increase it for very large projects. |
| `server_ready_timeout` | `10.0` | Timeout in seconds for waiting on the server-ready signal after initialization. If the signal does not arrive within this window, Serena logs a message and proceeds anyway. |
| `indexing_start_grace` | `5.0` | Timeout in seconds to wait for tsserver to *start* reporting `$/progress` before the first cross-file reference query. tsserver must resolve the project graph before it can emit the first progress token, and that can take longer than the default on a very large project; if it takes longer than this window, Serena assumes no indexing was needed and may return incomplete cross-file references. Raising `indexing_timeout` alone does not help here, since this grace elapses first. Increase this for very large projects if `find_referencing_symbols`/`request_references` returns incomplete results shortly after project load. |
#### Svelte
@@ -74,6 +74,8 @@ class TypeScriptLanguageServer(SolidLanguageServer):
- typescript_language_server_version: Version of typescript-language-server to install (default: "5.1.3")
- indexing_timeout: float, timeout in seconds for project indexing (default: 30.0)
- server_ready_timeout: float, timeout in seconds for the server-ready signal (default: 10.0)
- indexing_start_grace: float, timeout in seconds to wait for tsserver to *start*
reporting indexing progress before the first cross-file reference query (default: 5.0)
"""
@classmethod
@@ -84,6 +86,11 @@ class TypeScriptLanguageServer(SolidLanguageServer):
# well within this window; the timeout is only hit if the server never sends progress.
INDEXING_PROGRESS_TIMEOUT = 30.0
SERVER_READY_TIMEOUT = 10.0
# How long to wait for tsserver to *start* emitting $/progress after opening files, before
# assuming no indexing is needed. tsserver has to resolve the project graph before it can even
# create the first progress token, and that resolution is proportional to project size, so a
# large project can genuinely take longer than a small one just to begin reporting.
INDEXING_START_GRACE = 5.0
def __init__(self, config: LanguageServerConfig, repository_root_path: str, solidlsp_settings: SolidLSPSettings):
"""
@@ -123,7 +130,7 @@ class TypeScriptLanguageServer(SolidLanguageServer):
:param start_grace: Maximum seconds to wait for progress to begin after opening files.
:return: True if indexing completed or no progress began within the grace period, False on timeout.
"""
grace = self._INDEXING_START_GRACE_S if start_grace is None else start_grace
grace = self.INDEXING_START_GRACE if start_grace is None else start_grace
# wait for progress to begin
progress_deadline = time.monotonic() + grace
@@ -171,6 +178,10 @@ class TypeScriptLanguageServer(SolidLanguageServer):
""":return: maximum seconds to wait for TypeScript project indexing."""
return float(self._custom_settings.get("indexing_timeout", self.INDEXING_PROGRESS_TIMEOUT))
def _get_indexing_start_grace(self) -> float:
""":return: maximum seconds to wait for tsserver to start reporting indexing progress."""
return float(self._custom_settings.get("indexing_start_grace", self.INDEXING_START_GRACE))
def _handle_server_ready_timeout(self, timeout: float) -> None:
"""Handle a TypeScript server-ready timeout.
@@ -531,17 +542,14 @@ class TypeScriptLanguageServer(SolidLanguageServer):
if not self._has_waited_for_cross_file_references:
self.expect_indexing()
#: how long to wait for tsserver to *start* reporting indexing progress after didOpen;
#: matches the fixed wait the base implementation previously used
_INDEXING_START_GRACE_S = 2.0
@override
def _wait_for_cross_file_references_if_needed(self) -> None:
if self._has_waited_for_cross_file_references:
return
timeout = self._get_indexing_timeout()
if self._wait_for_indexing_start_or_completion(timeout=timeout):
start_grace = self._get_indexing_start_grace()
if self._wait_for_indexing_start_or_completion(timeout=timeout, start_grace=start_grace):
log.info("TypeScript cross-file indexing complete")
else:
log.warning(
@@ -63,6 +63,14 @@ class TestBaseTypeScriptTimeoutPolicy:
assert server._get_server_ready_timeout() == 1.5
assert server._get_indexing_timeout() == 2.5
def test_indexing_start_grace_default(self) -> None:
server = _bare_ts_server(TypeScriptLanguageServer)
assert server._get_indexing_start_grace() == 5.0
def test_indexing_start_grace_configurable_via_ls_specific_settings(self) -> None:
server = _bare_ts_server(TypeScriptLanguageServer, {"indexing_start_grace": 12.0})
assert server._get_indexing_start_grace() == 12.0
class TestSvelteCompanionTimeoutPolicy:
"""The Svelte companion must be strict: raise instead of serving from a cold/partial program."""
@@ -141,6 +149,36 @@ class TestWaitForIndexingStartOrCompletion:
timer.cancel()
class TestWaitForCrossFileReferencesUsesConfiguredGrace:
"""The actual find-references call path (not just the helper in isolation) must honor
indexing_start_grace: this is the mechanism behind oraios/serena#1586, where a large
project's tsserver takes longer than the (previously hardcoded, unconfigurable) grace to
even start reporting progress, so the first cross-file reference query silently races a
still-loading project and returns incomplete results.
"""
def test_configured_grace_bounds_the_real_call_path(self) -> None:
server = _bare_ts_server(TypeScriptLanguageServer, {"indexing_start_grace": 0.05})
server._has_waited_for_cross_file_references = False
server.expect_indexing() # mirrors _pre_open_for_cross_file_references having already run
start = time.monotonic()
server._wait_for_cross_file_references_if_needed()
elapsed = time.monotonic() - start
assert server._has_waited_for_cross_file_references is True
# bounded by the configured 0.05s grace, not the 5.0s default (let alone the old 2.0s one)
assert elapsed < 1.0
def test_second_call_is_a_noop_regardless_of_grace(self) -> None:
server = _bare_ts_server(TypeScriptLanguageServer, {"indexing_start_grace": 0.05})
server._has_waited_for_cross_file_references = True
start = time.monotonic()
server._wait_for_cross_file_references_if_needed()
assert time.monotonic() - start < 0.1
class _FakeSolidLSPSettings:
"""Minimal settings stand-in: returns a fixed TypeScript indexing_timeout so the guard tests can
prove the svelte-key value takes precedence over the typescript-key value.