Skip to content

Lazily hydrate persisted conversations#4100

Merged
enyst merged 6 commits into
mainfrom
codex/simplify-lazy-conversation-loading
Jul 15, 2026
Merged

Lazily hydrate persisted conversations#4100
enyst merged 6 commits into
mainfrom
codex/simplify-lazy-conversation-loading

Conversation

@neubig

@neubig neubig commented Jul 13, 2026

Copy link
Copy Markdown
Member

HUMAN:
I (@neubig) have tested this and it is working quite well, much faster conversation load times and lower memory usage.

AGENT:

Why

Persisted conversation startup was hydrating every event history, which raised memory use and delayed readiness for large local corpora.

How to Test

Run the focused conversation-service and lease tests from the Validation section below. The Live acceptance testing section records a real Agent Server run against 120 persisted conversations and 55200 production-format event files.

Summary

  • load a lightweight persisted-conversation catalog at agent-server startup
  • hydrate an EventService and event history only when an operation needs a live runtime
  • serialize hydration so concurrent requests cannot create duplicate runtimes
  • serve search, count, and conversation metadata reads without hydrating event history
  • preserve startup recovery for conversations persisted in the running state
  • prevent forks from overwriting an unloaded persisted conversation

This is a smaller replacement for #3895.
Part of #3140

Root cause

The agent server currently reconstructs every persisted conversation and its event history during startup. On the local corpus used to reproduce the watchdog failures, 136 conversations containing 55,312 event files raise the server to roughly 3 GiB RSS before any active work. A long-running npm test or coverage process can then exhaust the host's 7.8 GiB memory limit, causing an exit-137/OOM kill. The watchdog health failure is a downstream symptom of that memory pressure and process death.

With the catalog-only startup path, the same corpus uses roughly 355 MiB RSS before runtime hydration.

Impact

Idle historical conversations no longer consume event-history memory. Search and count remain available from the catalog, while runtime operations transparently hydrate the requested conversation. A single hydration lock keeps check-and-load atomic and bounds transient memory pressure.

Validation

  • uv run pytest -q tests/agent_server/test_conversation_service.py tests/cross/test_conversation_lease_behavior.py -x — 90 passed
  • router, event-service, websocket, socket, and stress regression selection — 264 passed
  • repository pre-commit checks for both changed files — passed
  • real-corpus startup memory comparison — approximately 355 MiB lazy versus 3 GiB eager

Agent Server images for this PR

GHCR package: https://github.com/OpenHands/agent-sdk/pkgs/container/agent-server

Variants & Base Images

Variant Architectures Base Image Docs / Tags
java amd64, arm64 eclipse-temurin:17-jdk Link
python amd64, arm64 nikolaik/python-nodejs:python3.13-nodejs22-slim Link
golang amd64, arm64 golang:1.21-bookworm Link

Pull (multi-arch manifest)

# Each variant is a multi-arch manifest supporting both amd64 and arm64
docker pull ghcr.io/openhands/agent-server:808bcbd-python

Run

docker run -it --rm \
  -p 8000:8000 \
  --name agent-server-808bcbd-python \
  ghcr.io/openhands/agent-server:808bcbd-python

All tags pushed for this build

ghcr.io/openhands/agent-server:808bcbd-golang-amd64
ghcr.io/openhands/agent-server:808bcbd5254ee9c7b65fae40147aaa8e8a66b2bb-golang-amd64
ghcr.io/openhands/agent-server:codex-simplify-lazy-conversation-loading-golang-amd64
ghcr.io/openhands/agent-server:808bcbd-golang_tag_1.21-bookworm-amd64
ghcr.io/openhands/agent-server:808bcbd-golang-arm64
ghcr.io/openhands/agent-server:808bcbd5254ee9c7b65fae40147aaa8e8a66b2bb-golang-arm64
ghcr.io/openhands/agent-server:codex-simplify-lazy-conversation-loading-golang-arm64
ghcr.io/openhands/agent-server:808bcbd-golang_tag_1.21-bookworm-arm64
ghcr.io/openhands/agent-server:808bcbd-java-amd64
ghcr.io/openhands/agent-server:808bcbd5254ee9c7b65fae40147aaa8e8a66b2bb-java-amd64
ghcr.io/openhands/agent-server:codex-simplify-lazy-conversation-loading-java-amd64
ghcr.io/openhands/agent-server:808bcbd-eclipse-temurin_tag_17-jdk-amd64
ghcr.io/openhands/agent-server:808bcbd-java-arm64
ghcr.io/openhands/agent-server:808bcbd5254ee9c7b65fae40147aaa8e8a66b2bb-java-arm64
ghcr.io/openhands/agent-server:codex-simplify-lazy-conversation-loading-java-arm64
ghcr.io/openhands/agent-server:808bcbd-eclipse-temurin_tag_17-jdk-arm64
ghcr.io/openhands/agent-server:808bcbd-python-amd64
ghcr.io/openhands/agent-server:808bcbd5254ee9c7b65fae40147aaa8e8a66b2bb-python-amd64
ghcr.io/openhands/agent-server:codex-simplify-lazy-conversation-loading-python-amd64
ghcr.io/openhands/agent-server:808bcbd-nikolaik_s_python-nodejs_tag_python3.13-nodejs22-slim-amd64
ghcr.io/openhands/agent-server:808bcbd-python-arm64
ghcr.io/openhands/agent-server:808bcbd5254ee9c7b65fae40147aaa8e8a66b2bb-python-arm64
ghcr.io/openhands/agent-server:codex-simplify-lazy-conversation-loading-python-arm64
ghcr.io/openhands/agent-server:808bcbd-nikolaik_s_python-nodejs_tag_python3.13-nodejs22-slim-arm64
ghcr.io/openhands/agent-server:808bcbd-golang
ghcr.io/openhands/agent-server:808bcbd5254ee9c7b65fae40147aaa8e8a66b2bb-golang
ghcr.io/openhands/agent-server:codex-simplify-lazy-conversation-loading-golang
ghcr.io/openhands/agent-server:808bcbd-golang_tag_1.21-bookworm
ghcr.io/openhands/agent-server:808bcbd-java
ghcr.io/openhands/agent-server:808bcbd5254ee9c7b65fae40147aaa8e8a66b2bb-java
ghcr.io/openhands/agent-server:codex-simplify-lazy-conversation-loading-java
ghcr.io/openhands/agent-server:808bcbd-eclipse-temurin_tag_17-jdk
ghcr.io/openhands/agent-server:808bcbd-python
ghcr.io/openhands/agent-server:808bcbd5254ee9c7b65fae40147aaa8e8a66b2bb-python
ghcr.io/openhands/agent-server:codex-simplify-lazy-conversation-loading-python
ghcr.io/openhands/agent-server:808bcbd-nikolaik_s_python-nodejs_tag_python3.13-nodejs22-slim

About Multi-Architecture Support

  • Each variant tag (e.g., 808bcbd-python) is a multi-arch manifest supporting both amd64 and arm64
  • Docker automatically pulls the correct architecture for your platform
  • Individual architecture tags (e.g., 808bcbd-python-amd64) are also available if needed

Closes #3140

Live evidence

On 2026-07-14 UTC I launched real python -m openhands.agent_server processes against a deterministic production-format corpus: 120 persisted conversations, 460 events each, 55,200 event JSON files total, corpus SHA-256 c6a5867f0bb11bf9d64cd28ab296a657b8ae22bcd2035aeb6ccbb4b8099e64c4. The benchmark used fetched origin/main 56ac31719f91059dc9b319dbc6bb79f17ef60cd7 as the before ref and current PR head 808bcbd5254ee9c7b65fae40147aaa8e8a66b2bb as the after ref. The harness used real HTTP endpoints plus production owner_lease.json files and resume logs; no test-only endpoint or monkeypatch was used.

Check Current main PR head
Startup time 12.555 s 5.625 s
Startup RSS 594,624 KiB 388,560 KiB
Hydrated runtimes at readiness 120 0
Resume logs at readiness 120 0
Hydrated runtimes after search/count 120 0
Eight concurrent event-count requests all returned 460 all returned 460
Hydrated runtimes after target event-count access 120 1

Additional PR checks: duplicate fork to an existing unloaded conversation ID returned HTTP 409 with the target meta.json unchanged. Startup with one persisted running record hydrated only that record, retained all 120 catalog records, and reported one recovered error record.

@github-actions

github-actions Bot commented Jul 13, 2026

Copy link
Copy Markdown
Contributor

Python API breakage checks — ✅ PASSED

Result:PASSED

Action log

@github-actions

github-actions Bot commented Jul 13, 2026

Copy link
Copy Markdown
Contributor

REST API breakage checks (OpenAPI) — ✅ PASSED

Result:PASSED

Action log

@github-actions

github-actions Bot commented Jul 13, 2026

Copy link
Copy Markdown
Contributor

Coverage

Coverage Report •
FileStmtsMissCoverMissing
openhands-agent-server/openhands/agent_server
   conversation_service.py75010187%156–157, 166, 192–193, 197–198, 203, 288–289, 292–293, 305–306, 320, 472–473, 520, 539–540, 552, 563, 575, 584, 606, 618–621, 627–628, 637, 639, 679, 682–686, 690–691, 697, 724, 730, 818, 824, 829, 835, 843–844, 853–856, 865, 877, 885, 900, 903–904, 907, 944–945, 1006, 1048–1050, 1052–1053, 1056–1057, 1077, 1185–1187, 1190–1191, 1195–1197, 1200–1201, 1205–1207, 1210–1211, 1240, 1249, 1289, 1297–1299, 1355, 1358, 1381, 1397, 1436, 1480, 1757, 1760
TOTAL361721555157% 

@neubig neubig marked this pull request as ready for review July 13, 2026 07:54

@all-hands-bot all-hands-bot left a comment

Copy link
Copy Markdown
Collaborator

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

✅ QA Report: PASS

Lazy persisted-conversation loading works as described in a real backend workflow: idle conversations stay unloaded after restart while search/count still work, runtime access hydrates on demand, concurrent access reuses one runtime, and duplicate fork IDs are rejected without overwriting persisted metadata.

Does this PR achieve its stated goal?

Yes. I created persisted conversations, restarted the conversation service, and compared main versus this PR using the same workflow. On main, restart eagerly loaded all 3 persisted conversations into live runtimes; on this PR, restart loaded 0 live runtimes but kept 3 catalog entries, search/count returned all 3 conversations without hydration, and get_event_service() hydrated only the requested conversation. Concurrent hydration returned the same service object for all callers, and a fork using an existing unloaded conversation ID failed with ValueError while preserving the target metadata.

Phase Result
Environment Setup make build completed successfully; no tests/linters run locally.
CI Status ⏳ 19 successful, 9 pending, 1 skipped, 0 failing when checked.
Functional Verification ✅ Backend persisted-conversation workflow passed before/after verification.
Functional Verification

Test 1: Startup no longer eagerly hydrates idle persisted conversations

Step 1 — Establish baseline on main:
Ran rm -rf /tmp/qa-lazy-data-main && cd /tmp/qa-sdk-main && OPENHANDS_SUPPRESS_BANNER=1 uv run python /tmp/qa_lazy_conversation_loading.py /tmp/qa-lazy-data-main.

Relevant output:

{
  "count": 3,
  "initial_live_runtimes_after_restart": 3,
  "live_runtimes_after_search_and_count": 3,
  "single_hydration_returned_service": true,
  "live_runtimes_after_single_hydration": 3
}

This confirms the old behavior: simply restarting with 3 idle persisted conversations immediately created 3 live runtimes before any runtime operation.

Step 2 — Apply the PR's changes:
Checked out PR branch codex/simplify-lazy-conversation-loading at bf0c3d3f70a39c1fe2c15ec2ecd5852260b9ce48.

Step 3 — Re-run with the fix in place:
Ran rm -rf /tmp/qa-lazy-data-pr && uv run python /tmp/qa_lazy_conversation_loading.py /tmp/qa-lazy-data-pr.

Relevant output:

{
  "catalog_entries_after_restart": 3,
  "count": 3,
  "initial_live_runtimes_after_restart": 0,
  "live_runtimes_after_search_and_count": 0,
  "single_hydration_returned_service": true,
  "live_runtimes_after_single_hydration": 1
}

This shows the PR behavior: restart builds a lightweight catalog, search/count still find all conversations, and only the requested conversation hydrates when runtime access is needed.

Test 2: Concurrent hydration and duplicate fork protection

Step 1 — Establish baseline on main:
The same baseline run returned:

{
  "concurrent_hydration_returned_services": [true, true, true, true, true],
  "unique_hydrated_service_objects": 1,
  "live_runtimes_after_concurrent_hydration": 3,
  "fork_duplicate_result": "ValueError: Conversation with id c3802f2a-8831-4cef-a4cc-710b769f2105 already exists",
  "fork_target_meta_unchanged": true
}

Because main had already loaded every conversation at startup, concurrent access reused the existing runtime and duplicate fork IDs were rejected against loaded state.

Step 2 — Re-run with the PR in place:
The PR run returned:

{
  "concurrent_hydration_returned_services": [true, true, true, true, true],
  "unique_hydrated_service_objects": 1,
  "live_runtimes_after_concurrent_hydration": 2,
  "fork_duplicate_result": "ValueError: Conversation with id b91c5ed0-be2d-4e8a-b693-0b5fa01f83c6 already exists",
  "fork_target_meta_unchanged": true
}

This confirms concurrent requests did not create duplicate runtimes under lazy hydration, and duplicate fork protection also works for catalog-only persisted conversations without clobbering the existing meta.json.

Issues Found

None.

This review was created by an AI agent (OpenHands) on behalf of the user.

@neubig

neubig commented Jul 13, 2026

Copy link
Copy Markdown
Member Author

PR Artifacts Cleaned Up

The .pr/ directory has been automatically removed.

neubig and others added 2 commits July 13, 2026 09:36
Co-authored-by: openhands <openhands@all-hands.dev>
Co-authored-by: openhands <openhands@all-hands.dev>
@enyst

enyst commented Jul 13, 2026

Copy link
Copy Markdown
Member

@OpenHands Check this PR, see its temporary .pr/ directory for evidence; /codereview and consider simplicity and maintainability in your review. Post it as a review on gh api on this PR; feel free to comment inline; and to add the event you see fit, approved, comment, request changes; only important stuff, not nits.

@openhands-ai

openhands-ai Bot commented Jul 13, 2026

Copy link
Copy Markdown

I'm on it! enyst can track my progress at all-hands.dev

@enyst enyst left a comment

Copy link
Copy Markdown
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

This review was generated by an OpenHands AI agent on behalf of @enyst.

🔴 Taste Rating: Needs improvement

The catalog/runtime split is a good simplification and the live evidence supports the startup-memory win, but lifecycle synchronization is incomplete. I left three important inline findings, each reproduced deterministically against the current head:

  • status-filtered search/count can disagree with fresh persisted state in shared-storage deployments;
  • delete can report success while a waiting request resurrects the runtime and catalog entry;
  • shutdown can finish while in-flight hydration publishes an open, unclosed runtime afterward.

Focused regression suites still pass (90 passed), so the existing tests do not cover these interleavings. The required Validate PR description check is also failing: the human author needs to restore the HUMAN: / AGENT: template structure and provide the human-written note; an AI agent must not fill that field.

[RISK ASSESSMENT]

  • [Overall PR] ⚠️ Risk Assessment: 🔴 HIGH
    This changes core persisted-runtime ownership and teardown behavior. The performance approach is sound, but the confirmed races can leak runtimes or make a deleted conversation reappear. Recommendation: do not auto-merge; have a human maintainer validate the lifecycle-locking fix and shared-storage semantics.

VERDICT:
Needs rework: make hydration, removal, and shutdown share one lifecycle synchronization boundary, and refresh unloaded status from base_state.json when filtering.

KEY INSIGHT:
The hydration lock must protect runtime publication against every competing lifecycle transition, not only duplicate hydration.


Improve this review? If any feedback above seems incorrect or irrelevant to this repository, you can teach the reviewer to do better:

  1. Add a .agents/skills/custom-codereview-guide.md file to your branch (or edit it if one already exists) with the /codereview trigger and the context the reviewer is missing. See the customization docs.
  2. Re-request a review; the reviewer reads guidelines from the PR branch.
  3. When merged, the guideline goes through normal maintainer review.

Resolve with AI? Install the iterate skill and run /iterate to drive the PR through CI, review, and QA.

Was this review helpful? React with 👍 or 👎.

Comment thread openhands-agent-server/openhands/agent_server/conversation_service.py Outdated
@enyst

enyst commented Jul 13, 2026

Copy link
Copy Markdown
Member

@OpenHands I’m curious about some parts of the code in this PR: is it all necessary, is it introducing redundancies, does it follow the patterns in the codebase, or, can we think about achieving its goal simpler?

Examples to look into:

  • “direct embedders and existing test fixtures” — if test fixtures need updated for what the PR is doing, we should update them, we should NOT introduce code in the main codebase just to keep the same test structures. Is this the case here?
  • ConversationRecord dataclass: is it necessary? I seem to recall we have some class for the meta.json or do I misremember? can’t we reuse what exists
  • if event_service is not None and event_service.is_open() — this is repeated in the same method, didn’t we return already? tell me if we can simplify
  • is _reconcile_active_records() necessary? I saw code that adds to records a conversation in use. If you think it’s necessary, can you make a test to prove that, if this method is not called, then some conversation could, in real-world use, be missed?

Do not modify the PR. Just answer me here on github. Intro yourself first so that readers know who is speaking (you are OpenHands-GPT-5.6)

@OpenHands OpenHands deleted a comment from openhands-ai Bot Jul 13, 2026
@openhands-ai

openhands-ai Bot commented Jul 13, 2026

Copy link
Copy Markdown

I'm on it! enyst can track my progress at all-hands.dev

Co-authored-by: openhands <openhands@all-hands.dev>
@neubig neubig requested review from enyst and tofarr July 13, 2026 18:40
@smolpaws

smolpaws commented Jul 13, 2026

Copy link
Copy Markdown
Contributor

I’m OpenHands-GPT-5.6, commenting through the smolpaws account. I reviewed the current PR head (2e97329c3), its base, the follow-up lifecycle fix, and the relevant tests.

Short version: the lazy catalog/runtime split is the right design, but the implementation carries compatibility code for tests that mutate private state. I would remove that compatibility rather than preserve it in production.

1. “Direct embedders and existing test fixtures” — ❌ Test-only compatibility

In this repository, _reconcile_active_records() accommodates test structure, not a demonstrated production path.

I searched non-test callers. The server constructs ConversationService normally and uses public methods; I found no production embedder that injects _event_services. In contrast, test_conversation_service.py has many tests that write mocked services directly into that private dict.

I also replaced _reconcile_active_records() with a no-op at runtime and ran two paths:

  • the persisted lifecycle test test_startup_and_search_do_not_hydrate_idle_conversation passed;
  • test_search_conversations_basic failed because it inserts a mock only into _event_services, leaving the catalog empty.

That is strong evidence that reconciliation repairs an invalid fixture rather than a real lifecycle gap. Since _event_services is private, unknown external mutation should not define the service contract. Update those tests to use the public lifecycle or explicitly establish both sides of the invariant in narrow unit tests.

The missing-record fallback in get_conversation() has the same issue: normal lifecycle paths already publish both entries. Quiet repair makes the invariant harder to reason about and can hide real bugs.

2. Is _ConversationRecord necessary? ✅

You remembered correctly: StoredConversation is already the typed model for meta.json, and this PR reuses it.

_ConversationRecord is not another meta model. It combines:

  • stored: StoredConversation from meta.json; and
  • execution_status, which lives in base_state.json, not meta.json.

The dataclass is therefore reasonable if status is intentionally cached for startup recovery and filtering. Replacing it with StoredConversation alone would lose the second value or move it to another parallel structure.

The larger redundancy is the two parallel maps, _conversation_records and _event_services. They can drift, which is why reconciliation appeared. The cleanest eventual model is one authoritative record per conversation with an optional runtime, conceptually:

_ConversationRecord(stored, execution_status, event_service=None)

That removes cross-map reconciliation. It is a larger refactor, though. The minimal clean change here is to keep the dataclass and two maps, enforce their invariant in lifecycle methods, and fix tests instead of repairing drift during reads.

3. Repeated open-service check ✅

At the head that existed when the question was posted, the repeated check inside _get_or_load_event_service() was necessary double-checking:

  1. check before waiting for _hydration_lock;
  2. await the lock;
  3. check again because another coroutine may have hydrated the conversation while this one waited.

The first return does not make the second check unreachable across an await.

The follow-up commit 2e97329c3 replaced that hydration lock with a lifecycle lock acquired before the check and moved the body into _get_or_load_event_service_locked(). On the current head, that control flow has only one check, so it has already been simplified correctly. Similar guards in _conversation_info() and _execution_status() are separate read paths, not duplicate branches in one method.

4. Is _reconcile_active_records() necessary? ❌ No real-world need found

I do not think it is necessary under the intended production invariant, and I cannot honestly propose a real-world test proving otherwise from this codebase.

Such a test would need a supported production path where a live EventService appears without a catalog record. I found none. _start_event_service() publishes both; delete removes both; startup creates the catalog before hydration; shutdown clears both under _lifecycle_lock.

The only reproducible missing-record path is direct mutation of _event_services in tests. That belongs at the test boundary, not in runtime reconciliation. Reconciliation also runs only in search/count while get_conversation() has a separate repair path; this asymmetry is a design smell.

Recommended simplification

For this PR, I would:

  1. keep _ConversationRecord;
  2. remove _reconcile_active_records() and its search/count calls;
  3. remove read-time repair of missing catalog entries;
  4. update tests that directly inject _event_services to establish valid state;
  5. document and enforce: every live runtime has exactly one catalog record, and lifecycle mutation happens under _lifecycle_lock.

One additional simplification: delete_conversation() currently hydrates an unloaded conversation — including event history and lease — only to close and delete it. An unloaded delete should operate from the catalog/persisted state; only an already-live runtime needs closing. That better follows this PR’s central rule: hydrate only when an operation actually needs a runtime.

I ran seven focused current-head regressions covering lazy search, concurrent hydration, shared-state status refresh, delete/hydration ordering, shutdown/hydration ordering, duplicate fork protection, and the legacy direct-injection search path; all seven pass as written.

Bottom line: good performance architecture, but _reconcile_active_records() is production complexity serving private test setup. Keep the record abstraction; remove reconciliation/repair behavior; make the catalog authoritative.

This comment was generated by OpenHands-GPT-5.6, an AI agent, on behalf of Engel Nyst.

@enyst enyst left a comment

Copy link
Copy Markdown
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Thank you for this! We definitely shouldn’t load entire conversations just for a few details like knowing how many they are or their titles. It looks like this gives the largest gain in performance.

My agent verified a few potential nits, and it seems that 2 of them are happening (which come down to the same root cause I think). I think maybe they are opportunities for simpler code: #4100 (comment)

@enyst enyst merged commit d06ff0b into main Jul 15, 2026
38 of 40 checks passed
@enyst enyst deleted the codex/simplify-lazy-conversation-loading branch July 15, 2026 09:38
@enyst

enyst commented Jul 15, 2026

Copy link
Copy Markdown
Member

@OpenHands pick the latest nits from here #4100 (comment) and open a new PR from main (we merged this one) as a follow up to address the simplification suggested there

@openhands-ai

openhands-ai Bot commented Jul 15, 2026

Copy link
Copy Markdown

I'm on it! enyst can track my progress at all-hands.dev

enyst commented Jul 15, 2026

Copy link
Copy Markdown
Member

Opened follow-up PR #4122 from current main: #4122

It removes read-time catalog reconciliation/repair, makes test fixtures establish the catalog/runtime invariant explicitly, and deletes unloaded conversations without hydrating event history while respecting leases. Focused conversation/lease tests pass (98), and adjacent router/event/plugin/restore regressions pass (262).

This comment was created by an AI agent (OpenHands) on behalf of the requester.

@openhands-ai

openhands-ai Bot commented Jul 15, 2026

Copy link
Copy Markdown

Opened draft follow-up PR: #4122

Implemented the requested simplification:

  • removed read-time catalog reconciliation and repair
  • made the persisted catalog authoritative
  • updated tests to preserve the catalog/runtime invariant
  • deleted unloaded conversations without hydrating event history, with lease handling

Validation:

  • pre-commit passed
  • focused conversation/lease suites: 98 passed
  • adjacent router/event/plugin/restore suites: 262 passed
  • git diff --check passed

Commit: 35b60a75

I also replied on merged PR #4100 with the follow-up link. The socket getter test failure noted in the PR reproduces on clean merged main and is unrelated to these changes.

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

perf: agent server loads all conversations eagerly at startup

4 participants