SerenaConfig.project_names / project_paths were cached_property values that were
never invalidated after projects were added or removed mid-session, so
user-facing project lists and error messages stayed stale. They are cheap to
derive, so drop the caching (plain properties) instead of invalidating.
CSharpLanguageServer._open_solution_and_projects scanned the whole
repository root and opened every .csproj it found, without consulting
the project's ignore settings.
On repositories that vendor third-party or sample C# projects this
opens projects the language server cannot restore. The cost is paid on
every server start, and the resulting restore failures bury the
diagnostics of the projects the user actually works on. Measured on an
Unreal Engine source tree: 245 projects opened, 53 of them under
Engine/Source/ThirdParty, which Roslyn cannot build; each restart
emitted thousands of NuGet advisory lines and ended in
`The "Csc" task could not be initialized`.
SolidLanguageServer.is_ignored_path already implements exactly this
check, and CSharpLanguageServer already overrides is_ignored_dirname,
so the ignore settings were being honoured everywhere except here.
This applies the existing check at project discovery.
ignore_unsupported_files=False is required because a .csproj is not
itself a C# source file, and would otherwise be excluded on file type
rather than by the ignore patterns.
Co-authored-by: Claude Opus 5 <noreply@anthropic.com>
Co-authored-by: Dr. Dominik Jain <dominik.jain@oraios-ai.de>
* fix(csharp): stop tuple-typed properties losing their name to the method branch
_extract_base_name_and_type split Roslyn's "Name : Type" property names by
checking for a literal '(' anywhere in the raw string. A C# tuple type is
written with parentheses ("(int X, string Y)"), so a tuple-typed property
tripped that guard and fell into the method branch instead, which kept the
trailing " :" as part of the reported name (e.g. "Position :"). Since
find_symbol defaults to exact name-path matching, such a property becomes
unfindable by its real name.
The guard now only checks for '(' in the identifier segment before the
first " : ", not the whole string, so a parenthesis inside the type
annotation no longer affects branch selection.
* fix(csharp): bump the high-level symbol cache fingerprint
_normalize_symbol_name's output changed in this PR, and
_document_symbols_cache_fingerprint gates the cache that stores its
result; leaving the version at 1 would keep serving the old, corrupted
names to anyone with an existing on-disk cache.
The previous default, 262.9593.0, is a JetBrains EAP-style build that has
expired: intellij-server exits at startup with "This build of intellij-server
has expired", so every Kotlin symbolic tool fails. 263.4702.0 is the newest
release on Kotlin/kotlin-lsp and starts normally.
Uses the same archive layout and CDN path as 262.9593.0. Hashes regenerated
with scripts/update_downloaded_dependency_hashes.py.
Fixes#2008
Co-Authored-By: Claude Sonnet 5 <noreply@anthropic.com>
Project.gather_source_files walks the tree with os.walk, which hands back
directories and files separately, and then calls is_ignored_path on each
one. That method re-derived file-ness from the filesystem: an
os.path.exists and an os.path.isfile in _is_ignored_relative_path, plus
an os.path.isdir in match_path. Three syscalls per path, for an answer
the caller already had.
is_ignored_path, _is_ignored_relative_path and match_path now accept an
optional hint, and gather_source_files supplies it from the os.walk
split. The parameter defaults to None, which determines file-ness from
the filesystem exactly as before, so no existing caller changes
behaviour.
Measured on a repository with 97,549 tracked source files out of 707,889
total (Unreal Engine source plus three game projects; Windows 11,
Python 3.13):
gather_source_files() 67.8s -> 11.6s
The returned file list is byte-identical before and after (sha256 over
the sorted relative paths, 10,390,077 bytes).
For context on where the time went: a bare os.walk of the whole 708k-file
tree takes 10.7s, so this was never I/O-bound. cProfile over 15,000 real
is_ignored_path calls attributed 3.41s to nt._path_exists,
nt._path_isfile and nt._path_isdir - about 82% of the per-call cost.
The new test asserts equivalence rather than specific verdicts: for every
path in a fixture tree, across several ignore configurations, the hinted
call must agree with the unhinted one. It covers the two cases where
guessing file-ness from the name would go wrong - a directory with a
suffix, and an extensionless file - and was checked against a deliberate
inversion of the hint, which it catches.
Refs #2077
Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
Co-Authored-By: Dominik Jain <dominik.jain@oraios-ai.de>
Previously, Tool.apply_ex derived a session ID from the MCP session object (or
"global" with no context) and injected it into any apply() method that declared
a session_id parameter. This is being changed because:
* the new MCP SDK v2 no longer provides session identifiers
* handling it internally is more robust anyway, since clients did not
consistently use sessions
Session handling
- Remove implicit session ID injection from Tool.apply_ex, along with the
supporting _is_session_aware property and SESSION_ID_PARAM_NAME skip logic
- SerenaAgent.create_system_prompt() now creates the session itself and reports
its id, instead of receiving session_id as an argument
Tool signatures
- InitialInstructionsTool.apply() and ActivateProjectTool.apply() now declare
session_id explicitly and rely on the LLM to pass it, rather than having it
injected
- Rename SerenaReplTool.apply()'s session parameter to session_id for consistency
with the other tools
Resolves#2061
* Many case differentiations in the agent code were replaced by
method calls in the newly introduced LanguageBackend abstraction
* The registry allows new backends to be added dynamically
(via Python packages that implement a specific entrypoint)
- Require a valid Bearer token on heartbeat and project-query requests.
- Send the configured auth_secret from clients and the query-project tool.
- Test accepted and rejected credentials, including real HTTP client requests.
- Set configuration permissions to 0600 before reading and log adjustments.
- Log chmod failures and continue loading; leave Windows permissions unchanged.
- Generate a random UUID when auth_secret is missing, null, or empty.
- Persist generated secrets and preserve configured values across reloads.
- Document the shared secret and test generation, persistence, and direct construction.
- Rename the read-only context to read_project_context and add writable project_context.
- Dispatch LSP-backed edits remotely and enforce read-only access in the calling context.
- Allow project-server facade calls independently of the target project's API restrictions.
- Update dispatch coverage for both backends and access modes; remove obsolete rejection assertions.
SerenaDashboardTrayManager._update_menu() called pystray's Icon.update_menu()
on whatever thread reached it. Four call sites are off the main thread: the
Flask handlers for /register, /update_project and /unregister, and
_alive_check_loop.
pystray does no marshalling. Icon.update_menu() calls the backend directly and
pystray/_darwin.py goes straight to NSStatusItem.setMenu_(), which AppKit
requires on the main thread. On macOS versions that enforce it the tray manager
traps with SIGTRAP inside the request handler, so the agent logs "Failed to
register with tray manager: Remote end closed connection without response" and
the tray icon never becomes usable.
Menu refreshes now go through PyObjCTools.AppHelper.callAfter on Darwin, which
is asynchronous so no Flask handler blocks on the main run loop. Other
platforms call through unchanged. The helper is separate from _update_menu
because _open_dashboard and _run_viewer run on the same Flask threads and will
need it too.
No new dependency: PyObjCTools comes from pyobjc-core, already required on
macOS via pystray -> pyobjc-framework-Quartz -> pyobjc-core.
The crash itself could not be reproduced locally (macOS 26.6.2; the reporter is
on 27.0, everything else matching). Verified instead that the AppKit call moves
from a Flask worker thread to the main thread: NSThread.isMainThread() across
the three HTTP routes reads [False, False] before and [True, True, True] after.
A language server's cache directory was determined by the language_id rather than
the language server identifier's key. The two identifiers coincided in most cases.
Facade availability:
* facades can be optional (`Facade.from_api(..., is_optional=True)`), mirroring optional tools
* `Facade.is_enabled()` is derived: a facade is available iff it has at least one enabled method
* `ApiScope.is_facade_enabled` is thereby obsolete and removed
Opt-in rule (uniform for optional facades, excluded facades and optional methods):
* methods of a facade which is not included and optional methods require explicit inclusion
* all other methods are enabled unless explicitly excluded
Application:
* the `ext` facade is optional; the query-projects mode includes it (`included_apis: [ext]`)
Godot's GDScript parser can report a symbol's end column one column past the
line-end convention every other language server follows (closing a node's range from
the next lookahead token instead of the last consumed one, when that lookahead is a
synthesized newline); `replace_symbol_body` on the last function in a file silently
consumed the separating blank line as a result. `GodotLanguageServer` now corrects this
specific, measured overshoot when building its high-level document symbols (#1974)
---------
Signed-off-by: Amir Fathi <amirfathi.me@gmail.com>
The project registry in serena_config.yml can be added to from the CLI
('project create', 'project index') and from the MCP side
('activate_project'), but it could only be removed from on the MCP side,
via the 'remove_project' tool. From a terminal, hand-editing the file was
the only way to drop an entry.
'serena project remove <PROJECT_NAME|PROJECT_PATH>' takes the same
argument type as 'project index', so a project can be addressed by name
or by path. Only the registry entry is removed; the project's own files,
including its project configuration, are left on disk.
Adds SerenaConfig.remove_registered_project, which mirrors
add_registered_project and removes the given entry rather than the first
entry whose name matches. Resolving a path to an entry and then deleting
by name would remove the wrong project when two registered directories
carry the same project_name.
Refs #2029
`serena.analytics` imported `anthropic.types` at module level for two type
annotations, so every `import serena.cli` / `import serena.agent` paid for
loading the whole anthropic package (2-4 s on the reporter's Windows machine)
even with the default CHAR_COUNT token estimator. That delay can push MCP
startup past a client's initial tool-discovery window.
The import now lives behind `TYPE_CHECKING`; `AnthropicTokenCount` already
imported `anthropic` lazily in `__init__`, and the request body is a plain
dict (what `MessageParam` produces at runtime anyway).
Tests: importing serena.cli / serena.agent / serena.analytics in a fresh
interpreter leaves `anthropic` out of `sys.modules`; the Anthropic estimator
still sends the expected count_tokens request.
Fixes#2012
* fix(tools): save edited source files atomically
Issue #1958 lists three direct open(path, "w") writes that can destroy a
file if the process stops partway through: two in MemoryManager and
CodeEditor._save_edited_file. The memory ones were fixed in #1969, which
is also where write_file_atomic comes from; this is the source-file half
the issue proposes as a follow-up.
A plain open(path, "w") truncates the file before the new content is
complete, so a crash, an OOM kill or a full disk during the write leaves
the file holding neither the old content nor the new one.
The issue asks for symlink and metadata behaviour to be confirmed before
source files use the helper. Measured: the executable bit already
survived, but a bare os.replace onto a symlink path swapped the link out
for a regular file and left its target holding stale content, which
open(path, "w") does not do. write_file_atomic now resolves the path
first, so it follows symlinks the way the call it replaces did, and the
temporary file lands in the destination's real directory.
Refs #1958
* test(tools): make the atomic-write tests platform-independent
Two of the new tests failed on the Windows CI job. Both were the tests,
not the change under test.
- The executable-bit test asserted 0o755 and read back 0o666: Windows
does not model POSIX permission bits, and chmod there only toggles the
read-only flag. Skipped on win32.
- The encoding test left newline unset, which is LineEnding.NATIVE and
therefore newline=None, under which Python translates "\n" to
os.linesep on write; it got b"caf\xe9\r\n". The line ending is now
pinned so the test covers the encoding alone, matching the sibling
test that already pins "\r\n" and passes on Windows.
Also covers three paths the first round missed, all of which pass both
with and without the change and are there to show it does not break
them: a file in a subdirectory, a symlinked parent directory (the path
is resolved in full, so the temp file lands in the destination's real
directory), and a non-ASCII filename. The last two skip where the
platform cannot create symlinks or represent the name.
Refs #1958
trusted_project_path_patterns is matched against the project's root path itself
(SerenaConfig.is_trusted_project_path, called from Project.is_trusted with project_root).
A '<project root>/**' entry therefore matches only paths below the root and trusts no
project at all, while reading like the natural way to name one project. The template's
example showed only parent-directory globs, so there was nothing to copy for the
single-project case.
Adds the bare-root form to the example, states which path a pattern is matched against,
and pins all three cases in tests so the documented example cannot silently go stale.
Requested by a maintainer in #2001.
Configuration endpoint:
* ResponseConfigOverview: agent_interface, language_backend and facades (None in tool mode)
* facades report each facade and its methods with their enabled state (SerenaReplEntrypoint.get_facade_availability_info)
Dashboard UI:
* new rows "Interface" and "Backend"; "Languages" is shown for the LSP backend only
* new collapsible section "Active Functions" listing the facades and their methods, dimming disabled ones
* _activate_project: a project's backend override switches the backend in REPL mode (background
modes, facades, prompt parameters and backend initialisation are recomputed upon activation)
* the tool interface keeps rejecting the mismatch (fixed toolset); the error now mentions the REPL
as a workaround
* single-project sessions: in the REPL tool's description (fixed at registration)
* otherwise: in the project activation message (facades depend on the activated project)
* REPL tool description wording adjusted (listing location, notebook semantics)
Peripheral changes:
* is_tool_function_available: in REPL mode, active tools without facade counterparts count as available
* subject line must cover the entire change (abstract if necessary)
* details as concise bullet items, grouped by topic where the change spans several topics
Availability check:
* SerenaAgent.is_tool_function_available(tool_class): whether a tool's functionality is available
to the LLM (tool mode: tool active; REPL mode: an enabled facade method corresponds to the tool)
* replaces the name-based tool_is_active/tool_is_exposed (activation message, dashboard, onboarding)
Prompts:
* activation message names functions via PromptParams.get_function_name (interface-specific names)