Summary
The MCP CLI envelope (getTools / useTools) takes workspaceId on every call and falls back to 'default' when it is not explicitly passed. There is no per-session stickiness: a multi-call MCP session working in one territory must re-specify workspaceId on every envelope, or its traces, sessions and states silently pile into the default workspace.
The infrastructure to avoid this already exists (SessionContextManager), but the envelope path doesn't use it. Raising as a design issue with options rather than a cold PR, since the cleanest fix touches a boundary you may have an opinion on.
Evidence
Profiling the workspace event store (.nexus-data/data/workspaces/) on a long-lived vault:
ws_default = 119 MB / 26 shards; 91% of it is trace_added (9,933 events).
ws_default trace_added per month: 147 → 2,381 → 3,351 → 4,054 (Feb→May) — steady, growing.
- Territory workspaces (Academico, Advocacia, Desenvolvedor, …) stay small.
Almost all of that is MCP-session activity that belongs to a territory but was filed under default. It then feeds the cold-start fullRebuild cost — see #158 (the bloat is what turns that design deficiency into a hard failure).
Why it happens
- Schema nudges. The "Known-good example" in
getTools.ts and useTools.ts hardcodes "workspaceId":"default"; the param description is 'Workspace ID. Optional. Defaults to "default".'; toolManager/types.ts says 'Use "default" for the global workspace. Do not invent workspace IDs.'. An LLM client mirrors the example.
- The fallback.
ToolCliNormalizer.ts (v5.9.5, ~L556): workspaceId: params.workspaceId || 'default'.
- Existing infra is not consulted.
SessionContextManager already maps sessionId → WorkspaceContext (getWorkspaceContext / setWorkspaceContext / updateFromResult). baseTool.getInheritedWorkspaceContext + a workspaceContext param already give workspace inheritance between internal tools. Neither is wired into the MCP envelope.
- Secondary — name vs id.
workspaceId: "Desenvolvedor" (name) and the workspace UUID resolve to distinct event stores, so even a careful caller can fragment a workspace's history by alternating the two forms.
#159 fixed the handler that ignored the envelope workspaceId; this is the layer above — the envelope having no notion of "the workspace this session is already working in."
Options
Prerequisite for any of the below: stop hardcoding default in the schema examples/descriptions — otherwise any stickiness mechanism just sticks to default on the first example-mimicking call.
- Option A — session-sticky resolution in
ToolCliNormalizer. Resolution chain: explicit params.workspaceId → SessionContextManager.getWorkspaceContext(sessionId) → 'default'. An explicit workspaceId also writes the session context. First explicit call (typically getTools) establishes it; the rest inherit. Smallest change; one resolution point.
- Option B — A, plus
loadWorkspace binds the session. loadWorkspace calls setWorkspaceContext(sessionId, …), so loading a workspace also means "this session is now in that workspace." Two explicit bind points, inheritance for the rest.
- Option C — surface the existing
workspaceContext inheritance to the MCP envelope. Extend the internal workspaceContext mechanism (getInheritedWorkspaceContext / updateFromResult) to the getTools / useTools boundary, so the MCP path uses the same continuation Nexus already uses internally instead of a separate code path.
I lean toward C — it extends a mechanism that already exists rather than bolting session state onto the normalizer — but it touches the most surface and the envelope/result contract, so it's your call. Happy to prototype whichever option you choose, and to share the full trace profile / repro.
Environment
Nexus 5.9.5, Obsidian, macOS; used as MCP server via connector.js. Code references from the 5.9.5 tag.
Summary
The MCP CLI envelope (
getTools/useTools) takesworkspaceIdon every call and falls back to'default'when it is not explicitly passed. There is no per-session stickiness: a multi-call MCP session working in one territory must re-specifyworkspaceIdon every envelope, or its traces, sessions and states silently pile into thedefaultworkspace.The infrastructure to avoid this already exists (
SessionContextManager), but the envelope path doesn't use it. Raising as a design issue with options rather than a cold PR, since the cleanest fix touches a boundary you may have an opinion on.Evidence
Profiling the workspace event store (
.nexus-data/data/workspaces/) on a long-lived vault:ws_default= 119 MB / 26 shards; 91% of it istrace_added(9,933 events).ws_defaulttrace_addedper month: 147 → 2,381 → 3,351 → 4,054 (Feb→May) — steady, growing.Almost all of that is MCP-session activity that belongs to a territory but was filed under
default. It then feeds the cold-startfullRebuildcost — see #158 (the bloat is what turns that design deficiency into a hard failure).Why it happens
getTools.tsanduseTools.tshardcodes"workspaceId":"default"; the param description is'Workspace ID. Optional. Defaults to "default".';toolManager/types.tssays'Use "default" for the global workspace. Do not invent workspace IDs.'. An LLM client mirrors the example.ToolCliNormalizer.ts(v5.9.5, ~L556):workspaceId: params.workspaceId || 'default'.SessionContextManageralready mapssessionId → WorkspaceContext(getWorkspaceContext/setWorkspaceContext/updateFromResult).baseTool.getInheritedWorkspaceContext+ aworkspaceContextparam already give workspace inheritance between internal tools. Neither is wired into the MCP envelope.workspaceId: "Desenvolvedor"(name) and the workspace UUID resolve to distinct event stores, so even a careful caller can fragment a workspace's history by alternating the two forms.#159fixed the handler that ignored the envelopeworkspaceId; this is the layer above — the envelope having no notion of "the workspace this session is already working in."Options
Prerequisite for any of the below: stop hardcoding
defaultin the schema examples/descriptions — otherwise any stickiness mechanism just sticks todefaulton the first example-mimicking call.ToolCliNormalizer. Resolution chain: explicitparams.workspaceId→SessionContextManager.getWorkspaceContext(sessionId)→'default'. An explicitworkspaceIdalso writes the session context. First explicit call (typicallygetTools) establishes it; the rest inherit. Smallest change; one resolution point.loadWorkspacebinds the session.loadWorkspacecallssetWorkspaceContext(sessionId, …), so loading a workspace also means "this session is now in that workspace." Two explicit bind points, inheritance for the rest.workspaceContextinheritance to the MCP envelope. Extend the internalworkspaceContextmechanism (getInheritedWorkspaceContext/updateFromResult) to thegetTools/useToolsboundary, so the MCP path uses the same continuation Nexus already uses internally instead of a separate code path.I lean toward C — it extends a mechanism that already exists rather than bolting session state onto the normalizer — but it touches the most surface and the envelope/result contract, so it's your call. Happy to prototype whichever option you choose, and to share the full trace profile / repro.
Environment
Nexus 5.9.5, Obsidian, macOS; used as MCP server via
connector.js. Code references from the5.9.5tag.