Move per-conversation metadata from localStorage to agent-server tags#1289
Move per-conversation metadata from localStorage to agent-server tags#1289chuckbutkus wants to merge 1 commit into
Conversation
Eliminate the `openhands-agent-server-conversation-metadata` localStorage
blob. The five fields it held (selected_repository, selected_branch,
git_provider, selected_workspace, active_profile) now ride on each
conversation's server-side `tags` map, with the agent-server as the
source of truth.
Pairs with software-agent-sdk PR #3621, which relaxes the tag-key regex
from `^[a-z0-9]+$` to `^[a-z0-9_]+$` so snake_case keys validate.
Wire-shape changes:
- POST /api/conversations now sends the five metadata fields as `tags`
alongside the existing `acpserver` tag (via `mergeMetadataIntoTags`).
- PATCH /api/conversations/{id} is used to update single fields:
`updateConversationRepository` (existing, now tag-based) and the new
`updateConversationActiveProfile`. Both preserve unrelated tags.
- `AppConversation` keeps the same flat shape; `toAppConversation`
reads from server tags first and falls back to legacy localStorage
for un-migrated conversations.
Migration:
- `conversation-metadata-store.ts` is now `@deprecated` and only
retained for the read-side fallback above.
- A new `conversation-metadata-migration.ts` runs lazily inside the
`usePaginatedConversations` queryFn: for each conversation in the
current page, if a legacy localStorage entry exists, PATCH any
fields the server is missing onto its tags and clear the entry.
Best-effort and retried on the next list refresh on failure.
Tests:
- New `__tests__/api/conversation-metadata-migration.test.ts` covers
the migration helper (PATCH-and-clear, no-op when mirrored, preserve
unrelated tags, retry on failure, empty-list no-op).
- Rewrote `use-switch-llm-profile-and-log` and conversation-websocket
tests to assert against `updateConversationActiveProfile` calls
instead of localStorage state.
- Updated `use-create-conversation-metadata` to assert against the
new POST tags payload; remaining create-conversation call-shape
tests pick up the new `active_profile` field on the metadata arg.
AGENTS.md: dedupe Files-tab note, add migration note.
Co-authored-by: openhands <openhands@all-hands.dev>
|
The latest updates on your projects. Learn more about Vercel for GitHub.
|
❌ Mock-LLM E2E Tests36/54 passed · 9 failed · 9 skipped Commit:
🔍 Failure details (9)❌ mock-llm-acp-agent.spec.ts › mock-LLM ACP agent conversation › step 3: start ACP conversation and verify agent reply❌ mock-llm-automation.spec.ts › mock-LLM automation lifecycle › step 2: create automation and dispatch run via the UI❌ mock-llm-conversation.spec.ts › mock-LLM agent-server conversation › step 3: run a conversation with the mock LLM❌ mock-llm-files-and-git.spec.ts › files tab, git control bar, and browser tab › step 2: start conversation and attach workspace metadata❌ mock-llm-folder-workspace.spec.ts › mock-LLM folder browser → workspace → conversation › step 1: browse to a folder, add it as a workspace, and launch a conversation with the correct working_dir❌ mock-llm-image-upload.spec.ts › mock-LLM image upload › attaching an image embeds it as base64 in the LLM completion call❌ mock-llm-model-switch.spec.ts › mock-LLM /model slash command › step 2: start conversation, switch profile via /model, verify switch❌ mock-llm-onboarding-happy-path.spec.ts › onboarding happy path › completes the full onboarding flow and launches a conversation❌ mock-llm-preset-automation.spec.ts › preset automation → slash command conversation › automation card sends the correct slash command to a conversationPosted by the Mock-LLM E2E workflow · results are deterministic (scripted LLM responses) |
🛑 Mock-LLM Docker E2E Test Results17/43 passed · 8 failed · 18 skipped · Commit:
🔍 Failure details (8)❌ chromium › mock-llm-acp-agent.spec.ts › mock-LLM ACP agent conversation › step 3: start ACP conversation and verify agent reply❌ chromium › mock-llm-acp-agent.spec.ts › mock-LLM ACP agent conversation › step 3: start ACP conversation and verify agent reply❌ chromium › mock-llm-automation.spec.ts › mock-LLM automation lifecycle › step 2: create automation and dispatch run via the UI❌ chromium › mock-llm-automation.spec.ts › mock-LLM automation lifecycle › step 2: create automation and dispatch run via the UI❌ chromium › mock-llm-conversation.spec.ts › mock-LLM agent-server conversation › step 3: run a conversation with the mock LLM⏱️ chromium › mock-llm-conversation.spec.ts › mock-LLM agent-server conversation › step 1: create an LLM profile pointing at the mock LLM server❌ chromium › mock-llm-files-and-git.spec.ts › files tab, git control bar, and browser tab › step 2: start conversation and attach workspace metadata❌ chromium › mock-llm-files-and-git.spec.ts › files tab, git control bar, and browser tab › step 2: start conversation and attach workspace metadataPosted by the Mock-LLM E2E workflow · results are deterministic (scripted LLM responses) |
📸 Snapshot Test ReportWarning Snapshot comparison step crashed (timeout, OOM, or runner error) — diff results below may be incomplete or absent. ❌ 26 snapshots differ from the main branch baselines. Add the
🔴 Changed snapshots (26)
|
| Expected (main) | Actual (PR) | Diff |
|---|---|---|
![]() |
![]() |
![]() |
automations-no-automations
| Expected (main) | Actual (PR) | Diff |
|---|---|---|
![]() |
![]() |
![]() |
automations-search-no-results
| Expected (main) | Actual (PR) | Diff |
|---|---|---|
![]() |
![]() |
![]() |
backends-extended — 6 snapshots
backend-add-blank-disabled
| Expected (main) | Actual (PR) | Diff |
|---|---|---|
![]() |
![]() |
![]() |
backend-add-cloud-with-key-enabled
| Expected (main) | Actual (PR) | Diff |
|---|---|---|
![]() |
![]() |
![]() |
backend-add-local-ready
| Expected (main) | Actual (PR) | Diff |
|---|---|---|
![]() |
![]() |
![]() |
backend-add-two-column-layout
| Expected (main) | Actual (PR) | Diff |
|---|---|---|
![]() |
![]() |
![]() |
backend-add-whitespace-host-disabled
| Expected (main) | Actual (PR) | Diff |
|---|---|---|
![]() |
![]() |
![]() |
backend-manage-two-listed
| Expected (main) | Actual (PR) | Diff |
|---|---|---|
![]() |
![]() |
![]() |
backends — 2 snapshots
backend-add-modal
| Expected (main) | Actual (PR) | Diff |
|---|---|---|
![]() |
![]() |
![]() |
backend-manage-modal
| Expected (main) | Actual (PR) | Diff |
|---|---|---|
![]() |
![]() |
![]() |
changes-tab
changes-empty
| Expected (main) | Actual (PR) | Diff |
|---|---|---|
![]() |
![]() |
![]() |
mcp-page — 4 snapshots
mcp-custom-server-2-url-filled
| Expected (main) | Actual (PR) | Diff |
|---|---|---|
![]() |
![]() |
![]() |
mcp-empty-installed
| Expected (main) | Actual (PR) | Diff |
|---|---|---|
![]() |
![]() |
![]() |
mcp-search-filtered
| Expected (main) | Actual (PR) | Diff |
|---|---|---|
![]() |
![]() |
![]() |
mcp-slack-install-2-modal
| Expected (main) | Actual (PR) | Diff |
|---|---|---|
![]() |
![]() |
![]() |
onboarding
onboarding-step-2-setup-llm
| Expected (main) | Actual (PR) | Diff |
|---|---|---|
![]() |
![]() |
![]() |
settings-page — 4 snapshots
analytics-consent-modal
| Expected (main) | Actual (PR) | Diff |
|---|---|---|
![]() |
![]() |
![]() |
home-screen
| Expected (main) | Actual (PR) | Diff |
|---|---|---|
![]() |
![]() |
![]() |
settings-app-page
| Expected (main) | Actual (PR) | Diff |
|---|---|---|
![]() |
![]() |
![]() |
settings-page
| Expected (main) | Actual (PR) | Diff |
|---|---|---|
![]() |
![]() |
![]() |
settings-verification
condenser-settings
| Expected (main) | Actual (PR) | Diff |
|---|---|---|
![]() |
![]() |
![]() |
skills-page — 4 snapshots
skills-empty
| Expected (main) | Actual (PR) | Diff |
|---|---|---|
![]() |
![]() |
![]() |
skills-loaded
| Expected (main) | Actual (PR) | Diff |
|---|---|---|
![]() |
![]() |
![]() |
skills-no-match
| Expected (main) | Actual (PR) | Diff |
|---|---|---|
![]() |
![]() |
![]() |
skills-search-filtered
| Expected (main) | Actual (PR) | Diff |
|---|---|---|
![]() |
![]() |
![]() |
✅ Unchanged snapshots (48)
archived-conversation
- conversation-panel-with-archived-badges
- conversation-view-archived
- conversation-view-sandbox-error
automations
- automations-delete-modal
backends-extended
- backend-add-cloud-advanced-open
- backend-add-cloud-no-key-disabled
- backend-add-form-partially-filled
- backend-add-invalid-url-disabled
- backend-add-name-only-disabled
- backend-after-switch
- backend-cancel-nothing-saved
- backend-dropdown-two-backends
- backend-edit-prefilled
- backend-manage-after-removal
- backend-remove-cancelled
- backend-remove-confirmation
- backend-switch-overlay
backends
- backend-selector-open
changes-tab
- changes-deleted-file
- changes-diff-viewer
collapsible-thinking
- reasoning-content-collapsed
- reasoning-content-expanded
- think-action-collapsed
- think-action-expanded
mcp-page
- mcp-custom-server-1-editor-open
- mcp-custom-server-3-all-filled
- mcp-custom-server-4-installed
- mcp-custom-server-editor
- mcp-slack-install-1-marketplace
- mcp-slack-install-3-filled
- mcp-slack-install-4-installed
onboarding
- onboarding-step-0-check-backend
- onboarding-step-1-choose-agent
- onboarding-step-3-say-hello
projects-workspace-browser
- projects-workspace-browser
settings-page
- add-backend-modal
settings-secrets
- secrets-add-form-filled
- secrets-add-form
- secrets-after-save
- secrets-delete-confirm
- secrets-list
settings-verification
- verification-settings-critic-enabled
- verification-settings-off
- verification-settings-on
sidebar
- sidebar-collapsed
- sidebar-conversation-panel
- sidebar-filter-menu
skills-page
- skills-type-filter
Generated by the Snapshot Tests workflow. This comment was created by an AI agent (OpenHands) on behalf of the repo maintainers.














































































H:
AGENT:
This PR was created by an AI agent (OpenHands) on behalf of the user. Per the AGENTS.md "PR Description Human Check" policy, only humans may edit the
H:section or check the human-tested checkbox.End-to-end verification ran in the sandbox:
npm run lint— typecheck + ESLint + Prettier all cleannpx vitest run --reporter=dot— 414 files, 3143 tests passing, 12 skipped, 9 todo (no regressions vs. main)__tests__/api/conversation-metadata-migration.test.ts(5/5 pass) covers the migration helper end-to-endI did not exercise the Playwright mock-LLM or live E2E suites in this sandbox; CI will run them.
Why
This is option 5 from the localStorage-elimination cleanup tracked in repo notes:
The frontend was stashing per-conversation metadata (
selected_repository,selected_branch,git_provider,selected_workspace,active_profile) in a single localStorage blob (openhands-agent-server-conversation-metadata). That has two well-known problems:The agent-server already supports an arbitrary
tags: Record<str, str>map per conversation. The only blocker was the validator regex^[a-z0-9]+$, which rejected the snake_case keys we want — that is fixed in the paired SDK PR.Summary
tagsmap; the SDK paired PR (Allow underscores in conversation tag keys software-agent-sdk#3621) relaxes the tag-key regex to^[a-z0-9_]+$so snake_case keys validate.POST /api/conversationsnow stamps the metadata as tags viamergeMetadataIntoTags. Two new PATCH paths cover incremental updates:updateConversationRepository(existing; now tag-based) andupdateConversationActiveProfile(new). Both preserve unrelated tags likeacpserver.toAppConversationreads from server tags first and falls back to the legacy localStorage entry;conversation-metadata-store.tsis@deprecatedbut kept alive for that fallback. A newconversation-metadata-migration.tsruns insideusePaginatedConversations'squeryFnand lazily PATCHes legacy entries onto missing tags, then clears the localStorage entry.Issue Number
Pairs with OpenHands/software-agent-sdk#3621 (must merge first so the relaxed
^[a-z0-9_]+$regex is published).How to Test
git checkout deprecate-conversation-metadata-storenpm cinpm run lint— should be greennpx vitest run— should be green (414 files, 3143 tests pass)active_profileserver tag survives reload).selected_workspaceserver tag →useHasAttachedSource).selected_repository/selected_branch/git_providerserver tags).localStorage.setItem('openhands-agent-server-conversation-metadata', JSON.stringify({ 'conv-xyz': { selected_repository: 'octocat/hello-world', selected_branch: 'main', git_provider: 'github' }}))) for an existing conversation, reload the conversation list, then watch the network tab: a PATCH against/api/conversations/conv-xyzwithtagsis fired and the localStorage entry is cleared.Video/Screenshots
N/A — pure refactor of the persistence boundary, no visible UI change.
Type
Notes
selected_repository,selected_branch,git_provider,selected_workspace,active_profile) all contain underscores and would 422 against the old^[a-z0-9]+$validator. Do not merge this PR before the SDK PR is released into the agent-server version the frontend pins to.toAppConversationfalls back to the legacy store) and are actively migrated on each conversation list refresh, so users will not lose metadata across the upgrade. Once one upgrade cycle has passed and the legacy blob is no longer expected to exist in the wild,src/api/conversation-metadata-store.ts, the fallback branches intoAppConversation, andsrc/api/conversation-metadata-migration.tscan all be deleted.tagsfield at all (same as before). Conversations with at least one field send atagsmap containing only the keys with non-empty values, merged with the pre-existingacpservertag when applicable.@chuckbutkus can click here to continue refining the PR
🐳 Docker images for this PR
• GHCR package: https://github.com/OpenHands/agent-canvas/pkgs/container/agent-canvas
ghcr.io/openhands/agent-canvasghcr.io/openhands/agent-server:1.27.0-pythonopenhands-automation==1.0.0a677596e758cf0547c99daee19f24abdd6284966fcPull (multi-arch manifest)
# Multi-arch manifest — Docker automatically pulls the correct architecture docker pull ghcr.io/openhands/agent-canvas:sha-77596e7Run
All tags pushed for this build
About Multi-Architecture Support
sha-77596e7) is a multi-arch manifest supporting both amd64 and arm64sha-77596e7-amd64) are also available if needed