Bug fix release v0.3.46#50
Merged
Merged
Conversation
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Sign up for free
to join this conversation on GitHub.
Already have an account?
Sign in to comment
Add this suggestion to a batch that can be applied as a single commit.This suggestion is invalid because no changes were made to the code.Suggestions cannot be applied while the pull request is closed.Suggestions cannot be applied while viewing a subset of changes.Only one suggestion per line can be applied in a batch.Add this suggestion to a batch that can be applied as a single commit.Applying suggestions on deleted lines is not supported.You must change the existing code in this line in order to create a valid suggestion.Outdated suggestions cannot be applied.This suggestion has been applied or marked resolved.Suggestions cannot be applied from pending reviews.Suggestions cannot be applied on multi-line comments.Suggestions cannot be applied while the pull request is queued to merge.Suggestion cannot be applied right now. Please check back later.
Jiva v0.3.46 - Multi-Tenant Concurrency Fixes
Release Date: May 15, 2026
Summary
This release fixes a set of concurrency bugs that made the HTTP/Cloud Run interface unsafe for parallel multi-tenant use. The root cause was a shared mutable context on the
StorageProvidersingleton: every HTTP session calledsetContext()on the same object, so concurrent requests from different tenants would corrupt each other's GCS paths. Three related singleton problems inOrchestrationLoggerandSessionManagercompounded the issue.Bug Fixes
1.
JIVA_STORAGE_PROVIDER=gcpfell through toLocalStorageProviderSymptom: With
JIVA_STORAGE_PROVIDER=gcpset in Cloud Run, the service silently usedLocalStorageProvider(ephemeral container filesystem) instead of GCS. Per-tenant MCP server lists, directives, and conversation history were never read from GCS. All GCS diagnostic logs were absent because theGCPBucketProvidercode never ran.Root cause:
StorageProviderType.GCP_BUCKEThas the value'gcp-bucket', but the documented/common short form'gcp'did not match any switch case, so the factory fell through to thedefault: LocalStorageProviderbranch.Fix:
src/storage/factory.tsnow normalises well-known aliases before the switch:Both the short and canonical forms are now accepted. The Cloud Run env var
JIVA_STORAGE_PROVIDERhas been updated togcp-bucketfor clarity.2. Shared
StorageProvider.contextcaused cross-tenant GCS path corruptionSymptom: Under concurrent load, tenant A's storage operations (reading config, saving conversations, loading directives) would silently use tenant B's GCS path. Data appeared to go missing or be saved in the wrong location.
Root cause: A single
GCPBucketProviderinstance is created at server startup and shared across all sessions viaSessionConfig.storageProvider. Itsthis.contextfield (set bysetContext()) was overwritten by each new session creation. BecausecreateSession()is async and makes several GCS calls, a concurrentsetContext()call from another tenant could corrupt the path mid-flight.Fix: Added
StorageProvider.createSessionScoped(context)— a new method that returns an isolated provider instance with a fixed, immutable context.GCPBucketProvideroverrides it to share the GCSBucketclient (stateless, safe for concurrent use) and the per-tenantconfigCache(aMapkeyed bytenantId, so reads from different tenants are isolated), but gives each session its owncontext,logBuffer, and other mutable state.LocalStorageProvideroverrides it trivially (creates a new instance with the samebasePath).SessionManager.createSession()now callscreateSessionScoped()instead ofsetContext()and stores the returned instance inActiveSession.storageProvider. Every storage operation for that session (config reads, MCP server loading, workspace directive, conversation history, log flushing) goes through this per-session instance. The shared singleton'scontextfield is never mutated during request handling.Files changed:
src/storage/provider.ts— addedcreateSessionScoped()src/storage/gcp-bucket-provider.ts— overrodecreateSessionScoped()src/storage/local-provider.ts— overrodecreateSessionScoped()src/interfaces/http/session-manager.ts— uses per-session provider throughout3.
GCPBucketProvider.setConfig()captured context after anawaitSymptom: Sporadic config writes going to the wrong tenant's GCS path, particularly during session initialisation when model config was being saved for the first time.
Root cause:
setConfig()calledthis.requireContext()andthis.getConfigPath()afterawait this.loadConfigCache(). If a concurrentsetContext()call arrived during that await, the path used for the write reflected the new tenant.Fix: Context and config path are now captured at the very top of
setConfig(), before the firstawait. (src/storage/gcp-bucket-provider.ts)4.
SessionManager.getOrCreateSession()had a TOCTOU race creating duplicate sessionsSymptom: Under concurrent requests for the same
(tenantId, sessionId)— e.g. two simultaneous HTTP requests arriving on a cold-start session — two separateDualAgentinstances and two sets of MCP sub-processes could be created for the same session key. The second one silently overwrote the first in the sessions map, leaking the sub-processes.Fix:
SessionManagernow maintains apendingSessions: Map<string, Promise<ActiveSession>>alongside thesessionsmap. When a creation is in progress, subsequent concurrent callers await the samePromiseinstead of starting a second creation. The pending entry is removed (in afinallyblock) whether creation succeeds or fails. (src/interfaces/http/session-manager.ts)5.
SessionManager.destroySession()saved conversation to the wrong tenant pathSymptom: When a session was destroyed (idle timeout or graceful shutdown),
saveConversation()andflushLogs()were called on the shared singleton provider, which held whatever context the most recently created session had set. Under concurrent load, conversations could be saved to the wrong tenant.Fix:
destroySession()now retrievessession.storageProvider(the session-scoped instance stored inActiveSession) and uses it for all teardown writes. (src/interfaces/http/session-manager.ts)6.
OrchestrationLoggersingleton mixed events across concurrent sessionsSymptom: All Manager/Worker/Client orchestration events from all concurrent sessions were logged to the most-recently-registered session's GCS path. Debug logs from tenant A appeared in tenant B's orchestration log.
Root cause:
OrchestrationLoggerwas a strict singleton.setStorageProvider(provider, sessionId)mutated sharedthis.storageProviderandthis.sessionIdfields; concurrent sessions overwrote each other.Fix: The
privateconstructor restriction is removed. The class now accepts optional(storageProvider, sessionId)constructor arguments: when both are provided it enters cloud mode immediately (no filesystem log file); when omitted it falls back to CLI mode (filesystem log in~/.jiva/logs/).static getInstance()and the module-levelorchestrationLoggersingleton export are kept for CLI backward compatibility.DualAgentConfiggains an optionalorchestrationLogger?: OrchestrationLoggerfield.SessionManager.createSession()instantiates a freshnew OrchestrationLogger(storageProvider, sessionId)and passes it toDualAgentvia this field.DualAgentstores it asthis.orchLoggerand passes it down toManagerAgent,WorkerAgent, andClientAgentconstructors (each accepts an optional last parameter, defaulting to the singleton for CLI use). AllorchestrationLogger.logXxx()calls in agent code becamethis.orchLogger.logXxx(). The deprecatedsetStorageProvider()method is kept (marked deprecated) so any existing integrations continue to compile.7. Additional bug fixes from v0.3.45
The dev release v0.3.45 included additional bug fixes as documented in v0.3.45
Files changed:
src/utils/orchestration-logger.tssrc/core/dual-agent.tssrc/core/manager-agent.tssrc/core/worker-agent.tssrc/core/client-agent.tssrc/interfaces/http/session-manager.tsArchitecture Changes
Per-session provider isolation model
Backward compatibility
orchestrationLoggersingleton continues to work.DualAgentconstructed withoutorchestrationLoggerin config falls back to singleton.JIVA_STORAGE_PROVIDER=gcp: still accepted (now aliased).gcp-bucketis the preferred value.StorageProvider,GCPBucketProvider, agent constructors) are additive changes only — no removals.Upgrade
Cloud Run: update
JIVA_STORAGE_PROVIDERThe environment variable value
gcpnow works correctly (it is aliased), but update to the canonical value for clarity:Cloud Run: per-tenant MCP and LLM config
Each tenant's configuration lives at
gs://{bucket}/{tenantId}/config.jsonand is now guaranteed to be read from GCS on every new session (the in-memory cache is invalidated viasetContext()override). To configure per-tenant MCP servers or a different LLM, upload:{ "models": { "reasoning": { "endpoint": "...", "apiKey": "...", "model": "..." }, "multimodal": null }, "mcpServers": [ { "name": "tavily-mcp", "command": "npx", "args": ["-y", "tavily-mcp@latest"], "env": { "TAVILY_API_KEY": "..." } } ] }