Commit Graph

47 Commits

Author SHA1 Message Date
pbednarcik 7febe00c86 lsp: env-tunable router idle timeout
One idle window cannot fit every server class: a Roslyn or jdtls
workspace can take longer to load than the 10-minute default, and a
provider reaped mid-load never becomes useful — each later pass pays
the load again and meets the reaper again. GORTEX_LSP_IDLE_TTL takes a
Go duration; zero or negative disables reaping (the router's existing
idleTimeout <= 0 no-op).
2026-08-17 15:27:57 +02:00
Andrey Kumanyaev 3fd504791d feat(indexer): isolate parser config and lifecycle 2026-08-14 11:06:23 +02:00
Andrey Kumanyaev 675039408d chore(daemon): drop the ignored backend buffer-pool knob
The cap was threaded through five files and discarded on arrival:
openSqliteBackend took it only to write `_ = bufferPoolMB`, since
SQLite sizes its page cache via a pragma. Removes the resolver, the
env read, the SharedServerConfig field and the OpenBackend parameter.
--backend-buffer-pool-mb stays registered as a hidden deprecated no-op
so existing start scripts and the detach re-exec keep working.
2026-08-14 09:49:32 +02:00
Andrey Kumanyaev b00782b764 chore(indexer): drop the vector snapshot import/export API
The vector-index serialization path existed only for the daemon warmup
snapshot, which no longer exists. Export/ImportVectorIndex on Indexer and
MultiIndexer, the skipVectorBuild flag that gated re-embedding during
warmup, and the VectorBackend Save/LoadFrom/SetCount frame codec had no
remaining callers. EmbedderDims on the shared server was write-only.
2026-08-14 09:45:43 +02:00
Andrey Kumanyaev 6f6d00acf0 refactor(serverstack): drop the repeated backend-name check
The store-path resolution added its own checkBackend call, but the
constructor already rejects an unusable backend name at the top — before
the store lock is taken and before any path is resolved, which is the
whole point of doing it there. The second call could only ever agree
with the first.
2026-08-09 09:37:10 +02:00
Andrey Kumanyaev c6f51d83ec Merge remote-tracking branch 'origin/main' into refactor/sqlite-only-graph-backend
* origin/main: (83 commits)
  State what the startup barrier actually guarantees
  Update CONTRIBUTING.md
  Report a repository the daemon is not actually watching
  Let the watcher observe its own startup handshake on macOS
  csharp: review round — claim self-typed-field recursion, stamp cross-repo exact-type origins
  csharp: stop member calls from binding to the calling method itself
  Write down where the user, not Gortex, is carrying the risk
  Describe the boundaries the code actually enforces
  Stop the HTTP surfaces from being reachable by anyone who can route to them
  Confine the generator tools' output paths, and keep the root set fixed
  Refuse a git revision that git would read as an option
  Stop diff handlers falling back to the daemon's own working directory
  Apply the overbroad-root refusal to ScopeForCWD's own containment arm
  Refuse to bind a session to a root too broad to be anyone's project
  Keep the embedded MCP fallback out of the directory it launched from
  Bind an MCP session opened above its repos to the repos it contains
  fix(indexer): announce a point patch whose graph mutation already landed
  Verify the user-state sandbox on the platform it exists for
  Fail cmd/gortex when a test writes to the real user state
  Route every home-isolating test through the shared sandbox
  ...

# Conflicts:
#	docs/onboarding.md
2026-08-09 09:35:54 +02:00
Andrey Kumanyaev 64d0c5fc38 fix(cli): read the backend path the daemon actually uses in repos
`gortex repos` reads index freshness straight out of the graph store,
but only ever looked at the platform default. A daemon started with
--backend-path writes its repo_index_state rows somewhere else, so every
repo that daemon had indexed came back reported as never indexed, and
there was no way to correct it — the command took no path of its own.

The daemon now records the choices an out-of-band CLI cannot discover in
a small runtime-state file beside its PID file: the PID that wrote it and
the resolved store path. It shares the PID file's lifetime and a reader
ignores a record whose process is gone, so a killed daemon cannot route
anyone at a store nothing has open.

`gortex repos` resolves the store as --backend-path, else the running
daemon's recorded store, else the platform default — and now registers
--backend-path, which existed only as an internal variable, with the
order documented in the command help. The server stack publishes the
store file it actually opened rather than each caller re-deriving it.
2026-08-08 20:18:05 +02:00
Andrey Kumanyaev e3978afe56 Join the analysis-generation prune before the backend store closes
RunAnalysis defers scheduleAnalysisGenerationPrune, which detaches a
goroutine of batched DELETEs against the backend sqlite store. Nothing
joined it: a test (or any teardown) could close the store and delete its
directory while the prune's already-acquired connection kept committing,
recreating WAL files mid-RemoveAll. On macOS CI this surfaced as
TestRunAnalysis_FeedsBundleFingerprintsToSQLiteBackend failing TempDir
cleanup with "directory not empty".

Track the goroutine on a Server WaitGroup and expose DrainBackground,
which waits for an in-flight prune and refuses to schedule new ones.
The Add is gated by a mutex + drained flag so it can never start at
counter zero concurrently with the drain's Wait, and a prune requested
after the drain is dropped instead of escaping it into a closing store.
The wiring test drains before its store closes, and the SharedServer
cleanup chain drains before the backend close during daemon teardown.
2026-08-08 08:47:33 +02:00
Andrey Kumanyaev 18ca30f9e3 refactor(serverstack)!: select the sqlite backend unconditionally
The server stack now opens the sqlite store for every lifecycle. An empty
backend name still resolves to it, so the daemon and the one-shot embedded
server keep working unchanged, but the in-memory names are refused with an
error that names the replacement: --backend sqlite with a throwaway
--backend-path reproduces the old ephemeral behaviour. Falling back silently
would write a caller who asked for a scratch store into the shared database.

With one backend left, the name check no longer varies by lifecycle: the
per-lifecycle default and the is-it-sqlite predicate collapse into a single
validation that runs before the store lock is taken, and the unused HTTP
lifecycle constant goes with them (the daemon's HTTP surface has always run
as the daemon lifecycle).

*graph.Graph is untouched and still implements the whole Store contract — it
remains the indexer's cold-index staging buffer and the fixture the rest of
the tree's tests build on.
2026-08-06 01:11:54 +02:00
Andrey Kumanyaev 65a446419f refactor(serverstack): give the one-shot server its own store
The embedded MCP server used to run on an in-process graph. It now
defaults to sqlite like every other lifecycle, backed by a per-process
temp file that `gortex mcp` creates and removes on shutdown.

That path has to be explicit. The one-shot server takes no store lock,
and an empty backend path resolves to the shared store under ~/.gortex —
so a second, unsynchronised writer would land on the daemon's database.
The constructor now refuses a one-shot with no backend path instead of
silently sharing, and tests pin both the refusal and the temp location.
2026-08-06 01:11:53 +02:00
Andrey Kumanyaev 4b048f321a refactor(cli): drop the one-shot graph snapshot and its cache flag
The daemonless `gortex mcp` path kept a second, independent graph
snapshot: a file store it replayed nodes and edges from on startup and
wrote back on shutdown, plus the --no-cache flag that only existed to
turn it off. It is gone, and so is the flag.

The accepted consequence, stated plainly: without the snapshot cache,
every daemonless one-shot invocation cold-indexes the repository from
scratch. Run the daemon if you want an index that survives the process.

`gortex repos` loses its snapshot-store fallback with it — that store was
only ever written by this path, so the repo_index_state rows the daemon
writes are now the sole freshness source and a repo without one reports
as never indexed. Its tests move to seeding those rows.
2026-08-06 01:11:53 +02:00
Andrey Kumanyaev c534750bd3 perf(embedding): halve the rerank model, give it an idle reaper and a config gate
The rerank semantic-cosine channel loads a bundled static code model on
the first search that reranks. Three things were wrong with how it was
held.

It was expanded to float32 at load. The tensor ships F16, and the loader
converted the whole 32 MB matrix into 62 MiB of float32 that the process
then kept forever — for a matrix that is read at most a few hundred rows
per call. Keep it packed and convert inside the gather loop: the same
arithmetic, half the resident cost.

It was a sync.Once with a no-op Close, so a daemon that stopped searching
held it for the life of the process. Track last use and drop it after 30
minutes of no rerank traffic; the next search reloads.

It had no supported way to decline it. The loader is independent of the
embedding: section — that section drives vector indexing, this drives
rerank scoring — so a user who configured provider: api, or disabled
embeddings entirely, paid for it identically. Add
search.rerank_embedder, honoured from the resolved config at server
bootstrap, and document the existing GORTEX_POTION=0 escape hatch, which
appeared nowhere outside a code comment.
2026-08-05 13:03:23 +02:00
Andrey Kumanyaev 55cb93e45a fix(lsp): root language servers at the tracked repo, not the daemon cwd
A daemon tracking several repos is normally launched from none of them.
Three paths let that launch directory leak into an LSP workspace, which
becomes the language server subprocess's working directory:

- Server.absolutePath fell back to filepath.Abs for a repo-relative
  graph path the prefix join could not claim, joining the fragment onto
  the daemon's cwd and minting a path that exists nowhere.
- Server.workspaceRootFor then returned filepath.Dir of that phantom
  path, and matched the indexer root with a plain string prefix, so
  /src/repo could claim a file under /src/repo-old.
- The router's default workspace was seeded from os.Getwd() whenever
  cfg.Index was empty — always, in daemon mode.

The spawn failed with a bare `chdir <phantom>: no such file or
directory`, and markSpawnFailed then disabled that spec for every other
tracked repo for the rest of the session, so one mis-anchored query
silently dropped a whole language's enrichment daemon-wide.

absolutePath now anchors an unclaimed relative path on a tracked repo
root, workspaceRootFor resolves the containing tracked repo on
component boundaries and refuses to invent a workspace from a
nonexistent directory, and the daemon's router default stays empty so a
caller that omits the per-repo root fails loudly. The router validates
the workspace before spawning, keeping the failure local to the one bad
workspace instead of poisoning the spec.

Fixes #297
2026-07-28 01:14:55 +02:00
Andrey Kumanyaev 32b103048f feat(mcp): ship compact surface across agent integrations 2026-07-15 00:44:09 +02:00
Andrey Kumanyaev 457eef9455 fix(llm): free the local model on daemon shutdown; config idle TTL
Service.Close — which unloads the in-process model and frees its Metal
buffers — had no runtime caller: the serverstack cleanup chain
registered lock, backend, overlay, telemetry, and savings teardowns but
never the LLM service, so only the idle reaper ever released the
multi-gigabyte model. A graceful daemon shutdown now closes the service
through the same cleanup chain as its peers.

The idle-unload TTL also gains a config surface: llm.local.idle_ttl
mirrors GORTEX_LLM_IDLE_TTL with env > config > default precedence and
the same 0/off/none-disables semantics, so operators pin it in
config.yaml instead of exporting an environment variable into launchd.
2026-07-11 04:01:17 +02:00
Andrey Kumanyaev 9b7fd895d9 semantic: make subprocess LSP enrichment lazy, off the cold path
The subprocess language servers (tsserver, rust-analyzer, pyright, jdtls,
clangd, ...) are the slowest part of a cold/warm index — a full sweep runs for
minutes to hours — and their net-new value over the in-process tiers is narrow:
a Go module is served by go-types, and every language has the tree-sitter
floor, which stamps real types (measured: rust-types 1,089 nodes on a crate
where eager rust-analyzer produced 0 in 782s). So running them synchronously
taxes every startup for little return.

Gate the manager's router-backed LSP dispatch behind a new EagerLSP config flag
(default false); go-types, SCIP, and the tstypes floor still run eagerly. The
LSP router stays wired, so a query can lazy-spawn a server on demand — LSP moves
off the synchronous path rather than being removed. GORTEX_LSP_EAGER=1 (or
semantic.eager_lsp) restores the old behaviour.

Measured on full PATH with the servers installed: cold-to-settled drops to 37s
(rust crate) and 41s (python repo) with zero lsp-* subprocess passes, while the
in-process floor keeps stamping types. Router-dispatch tests opt into EagerLSP.
2026-07-07 21:05:24 +02:00
Andrey Kumanyaev 42e039f984 serverstack: default Go enrichment to in-process go-types over gopls
The go-types provider (go/packages + go/types) type-checks a module once and
stamps every symbol's exact type plus resolves stdlib/dep calls to real ext::
nodes. Driving gopls over LSP instead re-checks per request — one hover / one
references / one call-hierarchy round-trip per symbol and edge — which on a
mid-size Go repo ran the enrichment pass for ~900s and, being a whole gopls
subprocess, left external calls as dead stubs. go-types does the same work in
tens of seconds and produces a richer graph.

Flip go-types on by default (env GORTEX_GO_TYPES and an explicit
semantic.go_types config still win). Because it is an eager provider it claims
the Go language slot, so the LSP router's gopls spec is skipped as a gap-filler
and never spawns. Registration is gated on the toolchain being present, so a
host without `go` keeps the slot open for gopls / the tree-sitter floor.

Verified on two repos with default config and gopls installed: gopls did not
run, go-types stamped more symbols than the gopls hover pass (7,065 vs 6,851)
and added 8,779 external-node edges gopls never produced, cutting a repo's
index-to-settled from 942s to 157s.
2026-07-07 19:07:57 +02:00
Andrey Kumanyaev 70223fbfa7 serverstack: GORTEX_GO_TYPES_TESTS opts go-types into test files
go-types is constructed with includeTest=false, so go/packages never
type-checks _test.go files and their symbols go unstamped — the whole
remaining stamp gap versus the gopls hover pass, which opens every file.
Loading test variants roughly doubles the go/packages work per module (114s
to 524s on a mid-size repo), so keep it off by default and add
GORTEX_GO_TYPES_TESTS=1 to opt in — with it, go-types stamps 16,644 symbols,
exceeding the LSP pass everywhere while still finishing faster end to end.
2026-07-07 18:58:39 +02:00
Andrey Kumanyaev a58d8f9aea feat(mcp): learn a per-workspace tool surface that survives restarts
Generalise promote-on-discovery into a persisted, per-workspace learned
surface. When a deferred tool is promoted — via tools_search or a direct
call by name — the promotion is recorded in the sidecar, keyed per repo
with a session epoch. On startup the daemon re-promotes the survivors into
the cold tools/list and demotes any promotion unused past a hysteresis
window (continued use resets its clock; floor tools never demote). On the
lean agent surface a learned tool is kept visible even though it is outside
the strict roster, so a tool the team reaches for is one hop cheaper next
session without bloating everyone else's cold list.

The active preset + learned-promotion count are surfaced on index_health
and `gortex daemon status` so the state is inspectable. Mid-session
promotion still fires notifications/tools/list_changed via the existing
AddTool path.

Claude-Session: https://claude.ai/code/session_01SXwzimUB1ZVcBaGsC9qfZ5
2026-07-05 14:30:30 +02:00
Andrey Kumanyaev 2d5763627f perf(lsp): gate the per-file enrichment sweep behind a sweep-mode knob
The whole-repo hover / call-hierarchy sweep re-opens and re-hovers every
file to confirm zero new edges on an already-resolved graph. Add a knob
that gates it: "demand" (default) sweeps only files whose declarations
still carry unresolved same-name call candidates, "full" sweeps every
file (the prior behaviour), and "off" skips the per-file sweep. The
tier-deciding confirm / add / interface passes are never gated.

The mode threads config.SemanticConfig -> semantic.Config -> the router's
WithEnrichSweepMode -> the provider's sweepMode field; the GORTEX_LSP_SWEEP
env override wins over the configured value.
2026-07-04 04:20:45 +02:00
Andrey Kumanyaev 4bdf14ac14 fix(embedding): quiet not-compiled backends and firm up config/error diagnostics
Refinements from a self-review of the reliability work:
- A backend that is simply not compiled into the build (the onnx/gomlx stubs)
  now wraps a sentinel error, so the degradation-to-static warning fires only
  for backends that could really have worked — no more warning that onnx/gomlx
  are 'not compiled in' on every default build. The warning also covers the
  case where even the static fallback fails to construct.
- The unknown-global-key diagnostic now inspects the config file that was
  actually loaded (which may be an overridden path), and a malformed global
  config warns instead of being silently ignored — exactly when its embedding
  block would have taken effect.
- The indexer's last-vector-build-error is reset at the top of the build so a
  benign skip (no embedder / snapshot restore) can't report a stale failure.
2026-07-03 23:45:55 +02:00
Andrey Kumanyaev 1ff47fc9b2 feat(config): honor an embedding block in the global config and warn on unknown keys
An 'embedding:' block in ~/.gortex/config.yaml was silently ignored: GlobalConfig
had no Embedding field, and an unknown top-level key vanished with no warning —
so a user who put their embedding config in the global file (rather than a
repo-local .gortex.yaml) got the static default with no explanation.

Add an Embedding field to GlobalConfig and MergeEmbeddingInto (repo-local
non-zero fields win, the global block fills the rest; the tri-state Enabled
pointer inherits when unset), merged just before the embedder is resolved so
flag/env precedence is untouched. Add UnknownGlobalKeys plus a daemon-startup
warning so a misplaced or misspelled top-level key is surfaced rather than
dropped — never failing the load, to keep forward compatibility.
2026-07-03 23:05:59 +02:00
Andrey Kumanyaev 527c20ef69 fix(embedding): report the constructed backend instead of the configured one
A local config that silently fell back to static GloVe still logged
'provider: local ... dim: 50' because the startup line echoed the configured
name, and every backend that failed along the way was swallowed with no signal.

Add a SelectionReport that records each backend the local chain tried and the
one it chose, thread it through ResolveEmbedder, and log the truth: the startup
line now reads the constructed backend ('local (hugot)', or 'local → static
fallback' when it degraded) with its real dimension. A genuine degradation to
static warns per failed backend; the benign misses behind a successful choice
(the onnx/gomlx stubs in a default build) stay at debug level.
2026-07-03 23:02:06 +02:00
Alexander Yazvetsky 468f494d3b feat(scope): intent-based default scoping for multi-repo workspaces
In a multi-repo workspace, graph queries had no principled default
scope:
every tool read the full index, so results leaked across project and
workspace boundaries unless the caller manually scoped each call. This
PR
makes scope a first-class, intent-derived default while keeping explicit
overrides uniform and workspace a hard isolation boundary.

## Behavior change (default ON)

Ships enabled by default (`scope.intent_defaults: true`, override with
`GORTEX_SCOPE_INTENT_DEFAULTS=0`). In a multi-repo workspace, results
that
previously spanned every indexed repo now default to the session's repo
(lookups) or workspace (reachability / analysis). Set the flag to false
to
restore the prior whole-index behavior. Single-repo setups are
unaffected.

## Two-layer model

- Layer A — uniform overrides: an explicit `repo:` / `project:` /
`scope:`
  (workspace) argument overrides the default identically across query,
MCP,
  and analyze tools. `repo:"*"` is a sentinel escape hatch meaning "all
  repos", not a post-filter.
- Layer B — intent-based defaults: with no explicit scope, the default
is
  chosen by the tool's intent —
    - Locate (symbol search / lookup) ............... repo (narrowest)
    - Reach  (callers / usages / dependents / walk) . workspace
    - Analyze ....................................... workspace
  Defaults only ever narrow; they never widen past the session
workspace.

## Analyze made scope-aware

The analyze dispatcher now scopes its kinds in tiers: kinds built on the
scoped-node accessors inherit scoping automatically; dead_code,
hotspots,
cycles, and releases are per-row filtered via analyzeNodeVisible; the
edge / algorithm / framework and file / AST-scan kinds were taught to
honor
the resolved scope. Kinds that read the full graph directly (community /
git-mining / per-id / synthesizer) are not narrowed in v1 and now stamp
a
`scope_note` saying their results may span the whole index.

## Tests & docs

- New tests: scope_resolve_test.go, analyze_scope_test.go,
  workspace_isolation_test.go, scope_allows_test.go, plus search_text
and
  field_query cases. `go build ./...` and `go test -race
./internal/mcp/...`
  pass.
- docs/multi-repo.md documents the defaults and the narrow-only
invariant;
  docs/proposals/scope-intent-defaults.md records the design and the
  default-ON decision.
2026-06-30 00:19:40 -04:00
Andrey Kumanyaev 0807de76ba perf(semantic): make the heavyweight go-types provider opt-in
The go/packages go-types provider runs a full `go list ./...` + go/types
pass over the module (and its deps) on every index — tens of seconds per Go
module per warmup — yet on an already-resolved graph it routinely confirms
zero new edges, because the always-on go-ast-types tree-sitter floor already
covers Go receiver/cross-file resolution. Demote it to opt-in: keep it as the
contracts binding resolver, but only register it for enrichment when
semantic.go_types (or GORTEX_GO_TYPES=1) is set. Default OFF.
2026-06-29 01:07:55 +02:00
Chris 85109f3bec fix(lsp): enable resolve-time pyright helpers 2026-06-21 18:35:44 +01:00
Andrew Kumanyaev 8cc18f66de Merge pull request #109 from avfirsov/pr/embedder-robustness
feat(embedding): authenticated, size-robust API embedder + vector persistence + warm-restart fix
2026-06-20 19:01:27 +02:00
Andrey Kumanyaev 0684d3e35c Skip enrichment of generated and vendored files
Indexing a fresh multi-repo workspace, the slowest remaining work by far
was a language server background-indexing machine-generated sources — in
this workspace clangd spent ~5 minutes indexing tree-sitter's generated
src/parser.c across the grammar repos, for essentially no enrichment value.

Add a generated/vendored heuristic (IsLowValueForEnrichment): vendored
dependency dirs, the tree-sitter runtime and generated parser.c/scanner.c,
and common generated suffixes, plus a configurable exclude_globs list.
Apply it both at the presence gate — so a repo whose only files of a
language are generated never spawns that language's server — and in the
LSP provider's work set, so a mixed repo's server never opens those files.
2026-06-20 14:24:23 +02:00
Andrey Kumanyaev bf2d6643c1 Wire opt-in telemetry record sites for index, daemon session, and install events plus client name folding 2026-06-19 22:00:42 +02:00
Andrey Kumanyaev a2d3ab273f Gate the graph-store rebuild-wipe behind an explicit lock-holder opt-in
The destructive drop-and-recreate in store_sqlite.Open was unconditional: its
cross-process safety relied on the daemon's exclusive store flock, but that
lock is acquired in a separate place under a different condition
(Lifecycle.Writable() && sqlite). A future caller that opened an on-disk sqlite
store without that lock could reach the wipe and unlink a database another
process had open — silent split-brain rather than a clean failure. Latent today
(the only oneshot path resolves to the memory backend), but the invariant was
convention-only.

Make it intrinsic. Open now refuses to wipe by default, returning
ErrSchemaRebuildRequired and leaving the file intact; the destructive rebuild
happens only when the caller passes WithRebuild. NewSharedServer passes it
through OpenBackend solely in the branch where it actually acquired the
exclusive flock, so the permission to wipe and the lock that makes wiping safe
are now derived from the same fact. No behaviour change for the daemon (it holds
the lock and still rebuilds); a non-locked caller fails safe.
2026-06-18 22:20:47 +02:00
Andrey Kumanyaev 47e7bdacc2 wire opt-in telemetry recording into the daemon and a consent step into gortex install 2026-06-18 09:34:40 +02:00
avfirsov c4481a3ef5 fix(embedding): probe API embedder dims at startup + tolerate /v1 base
An APIProvider reported Dimensions()==0 until its first embed, so the
daemon logged dim:0 and the snapshot-vector reload gate
(daemon_state.go: vec.Dims == EmbedderDims) rejected a correctly-sized
cached index, re-embedding the whole graph on every restart.

Add APIProvider.ProbeDimensions(ctx): one tiny embed call that caches the
true width up front — idempotent, best-effort (a failure only warns and
the lazy path still fills it in), and doubles as an early key/URL
connectivity check. NewSharedServer probes any API-backed provider before
logging "embeddings enabled", so the width is truthful from the start.

Also fix a double-/v1 bug: NewAPIProvider("…/v1") + embedOpenAI appending
"/v1/embeddings" produced "…/v1/v1/embeddings" → 404 → silent fallback to
BM25. OpenAI-compatible bases are conventionally given with /v1 (OpenAI,
OpenRouter), so append it only when absent.

Tests: probe unit/error/URL-variant tests + a live OpenAI integration test
(skipped without a key) asserting a 1536-d width and token accounting.
Verified live: daemon now logs "embedding dimension probed dim:1536".

Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
(cherry picked from commit d4b6a41f7bfd6110fb38cab53994378a130cee58)
2026-06-18 10:32:20 +03:00
Andrey Kumanyaev 4c378721ec mcp: configurable tool-surface presets for a minimal harness
Adds named presets (full/readonly/edit/nav) plus per-tool allow/deny
deltas to restrict the published MCP tool surface, so an agent on a
trusted box can drive a remote daemon through a small, fixed tool set
instead of the full ~170-tool catalogue.

Selected via mcp.tools in config, the GORTEX_TOOLS / GORTEX_TOOLS_MODE
env vars, or --tools / --tools-mode on 'gortex mcp' and 'gortex daemon
start' (precedence: env > flag > config > default full). Two modes:
hide removes non-allowed tools from tools/list and hard-blocks calls;
defer parks them behind tools_search via the lazy registry's eager
predicate. tool_profile reports the active preset and reflects the
narrowed surface. Detached daemons propagate the selection to the
re-exec'd child via env.
2026-06-16 12:56:11 +02:00
AVFirsov 7a38b73221 fix(lsp): Java LSP enrichment — три корневых бага
1. extractTypeFromHover (provider.go) — только Go-префиксы.
   Java hover типа "public class Foo" или "void bar()" не проходил.
   Добавлены: public/private/protected/abstract/static/final/class/
   interface/enum/void/@ и markdown fence ```java.

2. jdtls без classpath — "invisible project" (только JRE).
   Добавлены InitializationOptions в InitializeParams (protocol.go),
   ServerSpec (registry.go) и провайдер (provider.go).
   Для jdtls: Maven+Gradle import enabled, autobuild on.

3. MultiIndexer не передаёт semanticMgr — enrich:0 в daemon.
   Добавлен semanticMgr в MultiIndexer, SetSemanticManager метод,
   propagation в newPerRepoIndexer и вызов из shared_server.go.
   Без этого патча демон (multi-repo режим) вообще не запускал
   semantic enrichment.
2026-06-15 14:43:24 +03:00
Andrey Kumanyaev db648c70a9 feat(analysis): config-driven event-boundary rule family
A declarative events: block in .gortex.yaml constrains the pub/sub graph —
which paths may produce or consume a topic, whether a produced topic must have
a consumer, and which paths are forbidden from it. EventBoundaryFamily walks
the emit / produces_topic / listens_on / consumes_topic edges of each changed
symbol and plugs into change_contract as a RuleFamily like guards and
architecture.
2026-06-15 01:04:09 +02:00
Andrey Kumanyaev 68edf88dbf feat(semantic): in-process tree-sitter type resolvers for six languages
New internal/semantic/tstypes package: per-language type resolvers for Java,
Python, Ruby, Rust, TypeScript/JavaScript, and C# that run fully in-process
over the shared tree-sitter AST — no external language server. A table-driven
engine builds per-file scope graphs, binds declared and constructor types,
propagates them through local assignments, resolves receivers against the
graph's method sets via import-aware cross-file lookup, and synthesizes
implements/extends edges per language.

Resolutions are stamped at the ast_resolved tier with semantic_source
<lang>-types, never downgrading a stronger edge; ambiguous receivers are
skipped. Enrichment is scoped to the repo being enriched, runs its graph-apply
phase under the resolve mutex, persists full edge provenance on disk backends,
and wires single-file incremental enrichment. Providers register as
supplemental in the semantic manager and coexist with LSP providers.
2026-06-13 09:11:20 +02:00
Andrey Kumanyaev f38a6c768a savings: one machine-global ledger for every entry point; non-destructive reads
Three wiring holes from the review. The embedded server's --cache-dir
still relocated its ledger away from the dashboard's default read path
— the exact writer/reader split this branch exists to fix; the flag now
moves only the graph cache, and ledger isolation comes from
XDG_DATA_HOME / XDG_CACHE_HOME, which both ledger paths honour.

The savings/gain CLIs run the one-shot legacy import only against the
default locations: pointing a dashboard at a directory with --cache-dir
must never rename files there as a side effect of looking.

And the serverstack constructor test pinned its SavingsPath +
SavingsLegacyJSON to temp paths — with both empty it opened the REAL
machine-global sidecar and imported (renaming!) the developer's live
flat-file ledger on every 'go test ./internal/serverstack'. The config
doc now matches the machine-global behavior and carries the warning.

Also states the percentage semantics in the dashboard help: bars cover
ALL recorded source fetches, including uncompressed read_file calls
that saved nothing.
2026-06-12 01:24:46 +02:00
Andrey Kumanyaev 51b4cfc34d serverstack: the savings ledger always defaults machine-global
Deriving the ledger from the side-store dir split it by entry point:
the embedded server's side stores default to the cache dir, so its
ledger landed in <cache>/sidecar.sqlite while the savings CLI reads
~/.gortex/sidecar.sqlite — the writer/reader divergence the flat files
had. Every entry point now defaults to the machine-global sidecar; an
explicit --cache-dir still relocates both the ledger and the legacy
files for isolation.
2026-06-12 00:28:39 +02:00
Andrey Kumanyaev f05aa9a907 savings: move the ledger into the sidecar database
The flat-file ledger (savings.json cumulative totals + savings.jsonl
event log under the cache dir) was only durable on a lucky schedule:
the cumulative file flushed every 20 observations, on a 5-minute ticker
gated on pending work, or on graceful shutdown — and MCP clients
SIGKILL their stdio servers, so light sessions never reached disk at
all. Every write error was silently discarded on top.

The ledger now lives in the machine-global SQLite sidecar
(~/.gortex/sidecar.sqlite, shared with notes/memories/scopes):
savings_events one row per call (now carrying the session id),
savings_totals running aggregates per bucket, savings_meta the
first/last stamps. Each observation is a single transaction — durable
at the call, safe across concurrent writer processes via WAL, no flush
machinery left to miss (the periodic flusher and its wiring are gone;
FlushSavings stays as a no-op for the shutdown chains).

Flat files import once on open — totals, per-repo/per-language buckets,
and the event history (a lone .jsonl without its cumulative file
rebuilds totals from events) — then rename to *.bak behind a migration
mark. The savings/gain CLIs and serverstack derive the ledger from the
same sidecar path the side-stores use, --cache-dir now relocating both
the ledger and the legacy files it imports. A fresh ledger reports a
zero FirstSeen instead of seeding time.Now(), so nothing claims to have
been tracking before anything was recorded.
2026-06-12 00:20:58 +02:00
Andrey Kumanyaev 9432314822 daemon: start the overlay idle-TTL janitor
SweepIdle existed but nothing ever called it — the documented 30m
idle expiry was inert, so an overlay session whose proxy vanished
without an observed disconnect pinned its pushed buffers for the
daemon's lifetime. Add OverlayManager.StartJanitor (ticker-driven
sweep, TTL/4 default interval, idempotent stop, no-op when expiry is
disabled) and wire it into NewSharedServer's cleanup chain.
2026-06-11 08:58:50 +02:00
Andrey Kumanyaev fb8b1f2c13 persistence+review: durable FP suppression store 2026-06-11 00:27:32 +02:00
Andrey Kumanyaev d29a99cd74 comments: drop internal planning-doc references from federation code
Strips internal planning-document citations -- spec filenames,
requirement/decision identifiers, and the enumerated strategy labels --
from code comments and a few log strings across the cross-daemon
federation work. Comment- and string-only: no behaviour change, and every
rewording preserves the original technical content.
2026-06-07 19:07:00 +02:00
Andrey Kumanyaev 65d0a7c34a serverstack: parameterize semantic mode, active project, savings, single-repo LSP
Adds the knobs the embedded entry point needs that the daemon doesn't:
SemanticMode (typecheck/callgraph), ActiveProject, SavingsPath/SavingsRepo,
WatchDebounceMs passthrough, and a single-repo "" resolver-LSP helper
registered when Index is set (the embedded path) — the daemon leaves
Index empty and registers per-repo helpers via the track hook. All
additive with daemon-compatible zero defaults; daemon tests unchanged.
2026-06-07 12:56:52 +02:00
Andrey Kumanyaev 4a1e2237d9 serverstack: parameterize side-store keying per entry point
The agent-knowledge stores (notes/memories/feedback/notebook/combo/
frecency) key differently per entry point: the daemon partitions
workspace-globally under a fixed key in DataDir, while the embedded path
partitions per-repo with a git-tracked notebook. NewSharedServer now
takes a SideStores config (NotesDir/NotesRepo, FeedbackDir/FeedbackRepo,
NotebookPath) instead of a single CacheDir/SideStoreKey, so each entry
point supplies its own layout. The daemon delegation reproduces its
prior keying verbatim.
2026-06-07 12:51:56 +02:00
Andrey Kumanyaev 6e55c6f8dc serverstack: cross-process store lock for writable lifecycles
A writable, on-disk lifecycle (daemon / http) now acquires an advisory
flock on store.sqlite.lock before opening the backend and fails fast
with an actionable hint when another gortex process already owns the
store. SQLite's in-process write serialisation does nothing to stop a
second OS process from opening the same file; the lock closes that gap.
Released via the teardown chain after the backend handle closes.
2026-06-07 12:33:57 +02:00
Andrey Kumanyaev 5947ff111d daemon: delegate construction to serverstack.NewSharedServer
buildDaemonState collapses from ~360 lines of bespoke wiring to a
SharedServerConfig + NewSharedServer call plus the daemon-specific
snapshot warm-start (memory backend) and the long-lived daemonState
return. The shared stack's Close() (savings flush + backend close, which
checkpoints the sqlite WAL) is now run at daemon shutdown. EmbedderDims
is surfaced so the snapshot vector-skip check survives. All daemon
construction + control tests pass.
2026-06-07 12:31:08 +02:00
Andrey Kumanyaev f7845a40b8 serverstack: implement NewSharedServer, the single construction path
NewSharedServer wires graph.Store -> parser.Registry -> indexer ->
query.Engine -> mcp.Server plus the semantic/LSP block, embedder,
MultiIndexer, overlay manager, side-stores, and LLM — the one place that
construction lives. SharedServerConfig carries the few varying knobs
(lifecycle, backend, index root, side-store keying); snapshot warm-start
and the per-repo warmup stay with the entry point, which orchestrates
around the returned graph/indexer. A smoke test builds the stack over a
tmp repo and confirms it indexes.
2026-06-07 12:24:35 +02:00