Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
30 changes: 30 additions & 0 deletions CLAUDE.md
Original file line number Diff line number Diff line change
Expand Up @@ -229,6 +229,36 @@ The `find_clones` MCP tool surfaces near-duplicate ("clone") function/method clu
| Scoping a query to a project | Pass `project` param to any query tool |
| Filtering by reference tag | Pass `ref` param to any query tool |

### Live Editor Buffers (Shadow-Graph Overlay Sessions)

Editor extensions push in-flight (unsaved) buffers as **overlays**. Gortex composes a per-request **shadow view** (`graph.OverlaidView`) on top of the immutable base graph and threads it through the tool dispatch context. Every subsequent `tools/call` from the same MCP session reads through the shadow view — graph-walking tools (`find_usages`, `get_call_chain`, `get_file_summary`, `analyze`, …) and source-reading tools (`get_symbol_source`, `get_editing_context`, …) all see the editor-buffer state.

**Load-bearing invariant: the base graph is never mutated by overlay flow.** The overlay layer is built per request, parsed once per (session, content-hash) tuple, and discarded with the request. Consequences:

- **Multi-tenant safe.** Sessions A1, A2 (same user) and B (different user) can run concurrently against the same daemon; each sees its own view. The file watcher's reindex passes mutate base but the in-flight shadow view captured a stable base snapshot at request start.
- **Non-destructive.** Cross-file edges from non-overlaid files INTO overlaid file symbols (`Caller→Target`) survive overlay processing because base's edges are intact. The in-place-mutation approach (which Gortex briefly experimented with internally) lost those edges.
- **Diffable.** `compare_with_overlay` runs a query against both base and overlay and returns the delta — proving the two views are simultaneously available.

| Instead of... | You MUST use... |
|---------------------------------------|------------------------------------------|
| Asking the user to save before a query | `overlay_register` then `overlay_push` — pushes one editor buffer; subsequent tool calls see the overlay layered on top of base |
| Listing what an extension has staged | `overlay_list` — every path / size / deleted flag / base SHA for the current session |
| Cancelling a single overlay | `overlay_delete` with `path` — saved-buffer view returns for that path on the next tool call |
| Tearing down an overlay session | `overlay_drop` — discards every overlay attached to the session, in one call |
| Previewing impact of an unsaved edit | `compare_with_overlay` with `kind: find_usages / get_callers / get_call_chain / get_dependencies / get_dependents` — runs the query against base and overlay simultaneously, returns added / removed / common ID sets |

**Drift detection.** Pass an editor-captured git blob SHA as `base_sha` on `overlay_push`. When the next tool call needs that path, Gortex compares it to the on-disk hash; if they disagree (a sibling tool, a git operation, or another editor saved over the file) the tool call returns a structured `overlay base SHA mismatch` error so the client knows to re-read the file and resubmit a fresh overlay.

**Deletion overlays.** Push with `deleted: true` to model "this file is going away" — the symbols inside it vanish from the shadow view (but are untouched in base), so the user can preview the impact of a delete without staging it.

**HTTP transport.** `gortex server` exposes the same surface at `POST /v1/overlay/sessions` (optional `session_id` binds an overlay to a known MCP session), `PUT /v1/overlay/sessions/{id}/files`, `DELETE /v1/overlay/sessions/{id}/files`, `GET /v1/overlay/sessions/{id}/files`, `DELETE /v1/overlay/sessions/{id}`. The `/v1/tools/<name>` HTTP entry point reads the active session from `Mcp-Session-Id` (preferred), `X-Gortex-Overlay-Session`, or `?session_id=` (test fallback).

**Lifecycle and lease.** The overlay is **bound to the MCP session that registered it.** When the MCP session ends — for any reason (clean disconnect, dropped TCP, daemon proxy teardown) — the overlay is dropped synchronously. That closes the "abandoned buffer pinned in the daemon, reachable by anyone who learns the session ID" attack surface that a pure-TTL lifecycle would expose.

The idle TTL is a fail-safe for the case where the daemon never observes the disconnect (e.g. the process was killed -9 mid-stream). Default **30 minutes**, configurable via `GORTEX_OVERLAY_IDLE_TTL` (`30m` / `1h` / `45s` / `0` to disable for tests). Every tool call against a live overlay session refreshes the idle timer (`SnapshotFor` bumps `LastUsed`), so an editor that's actively querying never trips the TTL. The MCP `overlay_keepalive` tool exists for genuine idle gaps (debugger pause, IDE wizard) — it bumps the timer without re-pushing content. `overlay_list` returns `expires_at` / `idle_seconds` / `idle_ttl_seconds` so the extension can schedule keepalives proactively.

If a tools/call references a stale (reaped) session ID, the call falls through to base — no error, no corruption, no content leak. The next `overlay_push` self-heals (auto-registers the session); `overlay_keepalive` / `overlay_delete` surface explicit "session has been dropped" errors so the editor can take corrective action.

### MCP Resources

Bootstrap-state tools are also exposed as MCP resources (read-only, URI-addressable, no args). Clients that speak resources can `resources/subscribe` once and receive `notifications/resources/updated` after each graph re-warm — no polling. The tool form stays for back-compat with clients that don't speak resources.
Expand Down
19 changes: 18 additions & 1 deletion README.md
Original file line number Diff line number Diff line change
Expand Up @@ -323,7 +323,7 @@ gortex query stats Show graph statistics

All query commands support `--format text|json|dot` (DOT output for Graphviz visualization).

## MCP Tools (64)
## MCP Tools (69)

### Core Navigation
| Tool | Description |
Expand Down Expand Up @@ -439,6 +439,23 @@ Wired across every running language server (gopls, tsserver, pyright, rust-analy
| `list_repos` | List every project/repo in the active workspace |
| `workspace_info` | Workspace identity — bind mode, root directory, marker contents, discovered member set |

### Live Editor Buffers (Shadow-Graph Overlay Sessions)
Editor extensions push in-flight (unsaved) buffers as **overlays**. Gortex composes a per-request **shadow view** on top of the immutable base graph and threads it through the tool dispatch context — every subsequent `tools/call` from the same MCP session reads through the shadow. Graph-walking tools (`find_usages`, `get_call_chain`, `analyze`, …) and source-reading tools (`get_symbol_source`, `get_editing_context`, …) all see the editor-buffer state without per-tool changes.

**Base is never mutated by overlay flow.** Concurrent sessions (multiple users, multiple windows of the same user) each see their own view; the file watcher's reindex passes don't race with overlay queries; cross-file edges from non-overlaid files into overlaid symbols (`Caller → Target`) are preserved.

| Tool | Description |
|------|-------------|
| `overlay_register` | Bind an overlay session to the current MCP session ID (idempotent) |
| `overlay_push` | Push (or update) a single file overlay; `base_sha` enables drift detection, `deleted: true` previews a delete |
| `overlay_list` | List every overlay attached to the session — path / size / deleted / base_sha |
| `overlay_delete` | Remove one overlay from the session |
| `overlay_drop` | Tear down the session and discard every overlay |
| `overlay_keepalive` | Refresh the session's idle timer without re-pushing buffer content; cheap option for debugger / wizard pauses |
| `compare_with_overlay` | Run `find_usages` / `get_callers` / `get_call_chain` / `get_dependencies` / `get_dependents` against base AND overlay; returns added / removed / common ID sets |

HTTP transport mirrors the surface at `/v1/overlay/sessions/*`; the `/v1/tools/<name>` entry point reads the overlay session from `Mcp-Session-Id` (preferred), `X-Gortex-Overlay-Session`, or `?session_id=`. **Overlays are bound to their MCP session** — when the session ends the overlay is dropped synchronously, so abandoned buffers never linger. Idle TTL is a fail-safe (default 30 min, configurable via `GORTEX_OVERLAY_IDLE_TTL`); every tool call against a live overlay refreshes it.

## MCP Resources (16)

Read-only, URI-addressable, no args. Clients that speak resources can `resources/subscribe` once and receive `notifications/resources/updated` after each graph re-warm — no polling.
Expand Down
9 changes: 9 additions & 0 deletions cmd/gortex/daemon_state.go
Original file line number Diff line number Diff line change
Expand Up @@ -15,6 +15,7 @@ import (

"github.com/zzet/gortex/internal/config"
"github.com/zzet/gortex/internal/contracts"
"github.com/zzet/gortex/internal/daemon"
"github.com/zzet/gortex/internal/embedding"
"github.com/zzet/gortex/internal/graph"
"github.com/zzet/gortex/internal/indexer"
Expand Down Expand Up @@ -288,6 +289,14 @@ func buildDaemonState(logger *zap.Logger) (*daemonState, error) {
gortexmcp.Version = version
srv := gortexmcp.NewServer(eng, g, idx, nil, logger, cfg.Guards.Rules, multiOpts...)

// Editor-overlay manager. Idle TTL resolved via
// GORTEX_OVERLAY_IDLE_TTL > daemon.DefaultOverlayIdleTTL (30m).
// Server.ReleaseSession (called on MCP-client disconnect) drops
// the overlay synchronously, so the TTL is only the fallback
// path for missed disconnects.
overlays := daemon.NewOverlayManager(daemon.OverlayIdleTTLFromEnv(0))
srv.SetOverlayManager(overlays)

// Semantic manager, feedback, savings — same wiring as runServe.
if semMgr := idx.SemanticManager(); semMgr != nil {
srv.SetSemanticManager(semMgr)
Expand Down
10 changes: 9 additions & 1 deletion cmd/gortex/server.go
Original file line number Diff line number Diff line change
Expand Up @@ -362,8 +362,16 @@ func runServer(_ *cobra.Command, _ []string) error {
// an MCP client to push, query, and tear down between turns;
// short enough that a crashed client doesn't leak buffers
// indefinitely.
overlays := daemon.NewOverlayManager(5 * time.Minute)
// Editor-overlay manager. Idle TTL resolution:
// GORTEX_OVERLAY_IDLE_TTL env var > daemon.DefaultOverlayIdleTTL.
// Tests can disable expiry by setting GORTEX_OVERLAY_IDLE_TTL=0.
// Most relevant security guarantee: when the MCP session ends,
// Server.ReleaseSession drops the overlay immediately, so the
// TTL is a fail-safe for missed disconnects rather than the
// primary cleanup path.
overlays := daemon.NewOverlayManager(daemon.OverlayIdleTTLFromEnv(0))
serverHandler.SetOverlayManager(overlays)
srv.SetOverlayManager(overlays)

// Wire the multi-server router. When `~/.gortex/servers.toml` is
// present, every
Expand Down
20 changes: 20 additions & 0 deletions internal/agents/instructions.go
Original file line number Diff line number Diff line change
Expand Up @@ -371,6 +371,26 @@ The ` + "`flow_between`" + ` and ` + "`taint_paths`" + ` MCP tools answer **"whe
| Scoping a query to a project | Pass ` + "`project`" + ` param to any query tool |
| Filtering by reference tag | Pass ` + "`ref`" + ` param to any query tool |

### Live Editor Buffers (Shadow-Graph Overlay Sessions)

Editor extensions push in-flight (unsaved) buffers as **overlays**. Gortex composes a per-request **shadow view** on top of the immutable base graph and threads it through the tool dispatch context. After ` + "`overlay_register`" + ` and one or more ` + "`overlay_push`" + ` calls, every subsequent ` + "`tools/call`" + ` from the same MCP session reads through the shadow view — graph-walking tools (` + "`find_usages`" + `, ` + "`get_call_chain`" + `, ` + "`get_file_summary`" + `, …) and source-reading tools (` + "`get_symbol_source`" + `, ` + "`get_editing_context`" + `, …) all see the overlay.

**Load-bearing invariant: base is never mutated by overlay flow.** Concurrent sessions (A1 / A2 / B) each see their own view; the file watcher's reindex passes don't race with overlay queries; cross-file edges from non-overlaid files into overlaid symbols are preserved because base's edges are untouched.

| Instead of... | You MUST use... |
|---------------------------------------|------------------------------------------|
| Asking the user to save before a query | ` + "`overlay_register`" + ` then ` + "`overlay_push`" + ` — pushes one editor buffer; subsequent tool calls see the overlay |
| Listing what an extension has staged | ` + "`overlay_list`" + ` — path / size / deleted / base SHA for the current session |
| Cancelling a single overlay | ` + "`overlay_delete`" + ` with ` + "`path`" + ` — saved-buffer view returns for that path |
| Tearing down an overlay session | ` + "`overlay_drop`" + ` — discards every overlay attached to the session in one call |
| Previewing impact of an unsaved edit | ` + "`compare_with_overlay`" + ` with ` + "`kind: find_usages / get_callers / get_call_chain / get_dependencies / get_dependents`" + ` — runs the query against base and overlay simultaneously, returns added / removed / common ID sets |

Pass an editor-captured git blob SHA as ` + "`base_sha`" + ` on ` + "`overlay_push`" + ` to enable drift detection: when the next tool call needs that path, Gortex compares ` + "`base_sha`" + ` to the on-disk hash and returns ` + "`overlay base SHA mismatch`" + ` if they disagree, so the client knows to re-read and resubmit. Push with ` + "`deleted: true`" + ` to model a tombstone — the file's symbols vanish from the shadow view (but are untouched in base).

**Lifecycle.** Overlays are bound to the MCP session that registered them — when the MCP session ends, the overlay is dropped synchronously. Idle TTL (default 30 min, configurable via ` + "`GORTEX_OVERLAY_IDLE_TTL`" + `) is a fail-safe for missed disconnects; every tool call against a live overlay refreshes it. Use ` + "`overlay_keepalive`" + ` for genuine idle gaps without re-pushing content. ` + "`overlay_list`" + ` returns ` + "`expires_at`" + ` / ` + "`idle_seconds`" + ` so extensions can schedule keepalives proactively.

HTTP transport mirrors the surface at ` + "`/v1/overlay/sessions/*`" + `; the ` + "`/v1/tools/<name>`" + ` entry reads the active session from ` + "`Mcp-Session-Id`" + ` / ` + "`X-Gortex-Overlay-Session`" + ` / ` + "`?session_id=`" + `.

### MCP Resources

Bootstrap-state tools are also exposed as MCP resources (read-only, URI-addressable, no args). Subscribe via ` + "`resources/subscribe`" + ` once and receive ` + "`notifications/resources/updated`" + ` after each graph re-warm — no polling. Tools stay registered for back-compat with clients that don't speak resources; both surfaces share builder helpers so payloads match byte-for-byte.
Expand Down
Loading
Loading