Just found a pretty wild bug We are storing a TON...
# proj-agent-canvas
w
Just found a pretty wild bug We are storing a TON of conversation metadata inside of localstorage. This is a huge antipattern--this data is already on the server side For instance, if I start a conversation in workspace A on my laptop, it shows up under "no workspace" when I load the UI from my phone. Because we're not reading the workspace from the server response--we're looking it up in localstorage
Screenshot 2026-08-20 at 4.31.08 PM.png
Dunno if this was a vibecoding artifact or what, but this should never have made it in
We should add something to the code review to never use localstorage unless specified by the user
it looks like this has been in place for a long time, dunno how this only just came up
curious why I only have a few dozen conversation IDs in there too, I've launched hundreds
s
Just a rando idea but it would make sense if it's dumping to localstorage whenever you lose connection, possibly?
w
hmm interesting idea but no, it looks like this is the official way of mapping conversations to workspaces
👍 1
c
I've noticed this too. In fact, that "no workspace" bug is exactly what prompted me to address github.com/OpenHands/OpenHands/issues/15598
switching between devices is a huge part of my workflow so this was a real pain point for me. it's to the point where I'm actually running dev with my changes instead of prod haha. I cannot work without the workspace transfer at the very least.
but yeah, I've discovered that the tendency to use localStorage seems to be a theme in the agent canvas repo. another bug that bugs me (hehe) is that archiving chats is also only tracked by the client. I was gonna dig into that one next so I don't have to purge a bunch of chats i've already archived when switching back to my laptop/phone.
thanks on a stick 1
w
ahh good catch!
yes we need to clean this up
once the codebase establishes a pattern like this the agent tends to blindly follow it, so it's a cascading problem
c
yeah ngl I was kinda surprised people weren't already pulling their hair out over this like I was but I guess most devs don't work from their phone as much as I do.
lemme see if there's already an issue about that archive bug or maybe we can just reference the same issue for that fix too. lmk.
w
CC @hundreds-twilight-32833 for visibility I think Brock's PR is an elegant, simple fix for the short term. I'm having OpenHands put together a better long-term solution but would need to hand it off to an engineer
❤️ 1
👀 1
c
I'd be more than happy to take on more work regarding this. Like I said, switching between devices is a big part of my workflow so this is a pain point I'd gladly put some time into resolving.
❤️ 1
looks like github.com/OpenHands/OpenHands/issues/16399 is also related. affects pinning
w
yup we're saving
pinned-conversations
in localstorage sadblob
🫠 1
@hundreds-twilight-32833 @limited-student-10747 we probably need a substantial sprint here to get everything out of localstorage and into the backend fortunately we have support for arbitrary
tags
on conversations which should make this reasonably easy to fix
excited 1
👍 1
l
@fresh-machine-37269 find all things in OpenHands/agent-canvas that are stored in localStorage and design an API in OpenHands/software-agent-sdk’s agent-server to store these in a way consistent with how other pieces of information are stored.
👀 1
f
🤖 On it! View progress here: oss-agent-canvas.ngrok.dev/conversations/…
w
FYI I just kicked off this prompt 🙂
investigate the use of localstorage in this repo
localstorage should only be used for:
* the list and configuration of backends (host, password, name)
* UI configuration that shouldn't carry across devices (e.g. what panel is open)
However, we're storing things like workspace metadata for each convo ID, the list of pinned conversations, etc in localstorage. this is a huge antipattern. these things need to persist across devices.
investigate the use of localstorage and open one issue on openhands/openhands per item that needs to be migrated. in the issue mention how the backend should track it. often this will be with tags on conversations in agent-server, e.g. for pinned conversations, or with metadata already available in the backend (e.g. the workspace). for each issue, put [localstorage] in the issue title, and tag it with
localstorage
if you can
lmk if you want me to kill it
f
✅ Done! I dug through both repos (
agent-canvas
frontend + `software-agent-sdk`'s
agent_server
). Here's the full inventory and a concrete API design that reuses the patterns the backend already has. What's in localStorage today Conversation-scoped state that is server truth (the real bug) •
openhands-agent-server-conversation-metadata
(
conversation-metadata-store.ts
) → per-conversation
{ selected_repository, selected_branch, git_provider, selected_workspace, workspace_mode, active_profile, plugins }
. This is the workspace→conversation mapping causing the "no workspace" bug. •
archived-conversations
(
archived-conversations-store.ts
) →
{ [backendId]: string[] }
•
pinned-conversations
(
pinned-conversations-store.ts
) →
{ [backendId]: string[] }
Per-conversation UI state (device-local, but currently leaks) —
conversation-state
-${id} in `conversation-local-storage.ts`:
selectedTab
,
unpinnedTabs
,
conversationMode
,
draftMessage
,
filesTabDiffView
,
filesTabContentViewMode
,
subConversationTaskId
, plus
pending-task-draft
. Global user preferences —
openhands-color-theme
,
openhands-sidebar
,
conversation-panel-preferences
,
home-store
(recent repos / last provider),
openhands-automations-view
,
oh:home-pinned-automations
,
desktop-layout-panel-width
,
openhands-onboarded
,
alert_banner_dismissed_at
. Backend/connection registry —
openhands-backends
,
openhands-active-backend
,
openhands-backend-health
,
openhands-last-conversation-by-backend
. Ephemeral / idempotency / consent —
openhands-child-conversation-launches:*
(tool-call dedupe ledger), draft keys,
openhands-telemetry-*
consent flags and PostHog keys. The key consistency insight The backend already has three established storage mechanisms to map onto: 1.
misc_settings
— an opaque, frontend-owned dict persisted via
PATCH /api/settings
(
misc_settings_diff
deep-merge). Already used for
misc_settings.app_preferences
. The server explicitly promises to never read/validate it. 2. Conversation
tags
— server-persisted
dict[str,str]
on every conversation (
ConversationInfo.tags
,
StoredConversation.tags
). Keys are lowercase alphanumeric, values ≤256 chars. Settable at
POST /api/conversations
and via
PATCH /api/conversations/{id}
(full-map replace). 3. Typed conversation metadata — fields on
ConversationInfo
/
UpdateConversationRequest
(like
title
), persisted into the conversation's own JSON, edited via
PATCH /api/conversations/{id}
. 4.
workspaces.json
file store (
PersistedWorkspaces
/
FileWorkspacesStore
) — for the saved-workspace picker. The natural split: structured, typed, per-conversation domain data → first-class fields on the conversation; boolean/flag state → tags; global UI prefs → `misc_settings`; device-local connection state + secrets → stay local. Proposed API design 1. Promote workspace metadata to typed conversation fields (server truth) Add to the SDK's
StoredConversation
/
_ConversationInfoBase
and
UpdateConversationRequest
(and the wire
ConversationInfo
):
Copy code
selected_repository: str | None = None
selected_branch: str | None = None
git_provider: str | None = None
selected_workspace: str | None = None   # user-attached local dir, distinct from workspace.working_dir
workspace_mode: Literal["local_repo", "new_worktree"] | None = None
These already exist today only in the client's
ConversationMetadata
— lift them verbatim. They round-trip automatically through
GET /api/conversations/search
and are edited via the existing
PATCH /api/conversations/{id}
(extend
UpdateConversationRequest
with the same optional fields). This directly fixes the "no workspace" bug: the workspace comes from the server response, not a local map. 2. Pin/archive via conversation tags (drop the two Zustand stores) Tags are the intended home for arbitrary per-conversation flags (the thread already pointed at them). Reserve two keys and serialize the backend-scoped lists into per-conversation tags: •
pinned=1
/ (absent = unpinned) •
archived=1
/ (absent = not archived) Because
PATCH tags
replaces the whole map (the SDK documents this footgun), add either: • a convenience endpoint to avoid read-modify-write races: ◦
POST /api/conversations/{id}/tags/{key}
(body: value) and
DELETE /api/conversations/{id}/tags/{key}
, or • an explicit `mode: "merge"`/`"replace"` flag on
UpdateConversationRequest
. Recommendation:
POST/DELETE /api/conversations/{id}/tags/{key}
— it eliminates the "drop every other tag (including
acpserver
)" hazard entirely and is trivially idempotent for pin/archive toggles. 3. Global UI prefs → misc_settings.ui_preferences Add a sibling
ui_preferences
namespace next to
app_preferences
(the
MiscSettings
container already anticipates this — the frontend comment literally says "adding a future category … is a non-breaking change"). Route through the existing
PATCH /api/settings
with `misc_settings_diff`:
Copy code
misc_settings.ui_preferences = {
  color_theme, sidebar_collapsed,
  conversation_panel: { show_full_metadata, show_llm_profiles, organize_mode, sort, thread_scope, ... },
  automations_view_mode, automations_view_mode,
  home_pinned_automations: { [backendId]: string[] },
  home_recent_repositories: [...], home_last_provider,
  panel_widths: {...},
  onboarded: true,
  alert_banner_dismissed_at
}
Frontend keeps its current
APP_PREFERENCE_FIELDS
splitter approach; just widen it with a
UI_PREFERENCE_FIELDS
set that mirrors these keys through
misc_settings_diff.ui_preferences
. The server needs zero new endpoints — it's the same opaque dict path as
app_preferences
. 4. Per-conversation UI state → tags (or a per-conversation blob) Most of
conversation-local-storage.ts
is legitimately client-side (MRU tab, draft) — it doesn't need cross-device sync, and
draftMessage
should arguably stay ephemeral. But if you want a consistent home for the persist-worthy subset (
selectedTab
,
filesTabDiffView
,
filesTabContentViewMode
,
conversationMode
,
unpinnedTabs
), store them as conversation tags (
selectedtab
,
filesdiffview
,
contentviewmode
,
conversationmode
,
unpinnedtabs
). Keep `draftMessage`/`pending-task-draft` local (ephemeral by design). 5. Leave these in localStorage (and codify why) •
openhands-backends
,
openhands-active-backend
,
openhands-backend-health
,
openhands-last-conversation-by-backend
— backend connection state contains
apiKey
, is device-originated, and is not portable between machines. Moving secrets to the server would be worse. •
openhands-child-conversation-launches:*
— a client-side idempotency ledger; the server has no notion of "already acted on this tool call" from the Canvas UI tool. •
openhands-telemetry-*
— consent already has a server home (
misc_settings.telemetry.consent
); the localStorage keys are a pending-sync/PostHog cache and should stay as such. 6. One-shot migration Follow the existing
app_preferences
migration precedent: on first
GET /api/settings
(and first
GET /api/conversations/search
), read the legacy localStorage keys, promote them into the new server fields/tags via
PATCH
, then delete the localStorage key. This gives existing users a clean handoff without a fresh-install-only experience. Recommendation summary This stays consistent with how the agent-server already stores things: typed fields for domain data, tags for flags, the opaque
misc_settings
blob for frontend-owned preferences, and `workspaces.json`/localStorage for device/secret-bound data. If you want, I can put this into a design doc or start the SDK-side PR (new
UpdateConversationRequest
fields + tags sub-resource +
ui_preferences
)., with interactive elements
👀 1
c
so i've been working on this but i want to clarify something regarding "Home pinned automations" (github.com/OpenHands/OpenHands/issues/16768): in the original design doc above, the agent suggested storing them in
misc_settings.ui_preferences
. looking back, it makes sense. unfortunately, i was working on this at one point by referencing the issues instead of this original design doc. that took me down a different path where i assumed that we needed an update to the automations service which led to github.com/OpenHands/automation/issues/394 and github.com/OpenHands/automation/pull/395. when i discovered this, my instinct was just to backtrack on my own but there were already comments and updates to the original issues that validated the new path that i had started heading down. I see that those comments and updates were made by an agent so it might be fine if I ignore them. just to be safe though, @limited-student-10747 is it safe to ignore those comments here and here and backtrack to what the original design doc suggested or should i move forward with these updates to the automation service?