Component
Other
Description
When using forge ui, refreshing the browser tab while an agent response is
still streaming (in-flight) can cause the in-progress turn — and sometimes
the entire visible chat session — to disappear. This is not actually data
corruption; it's three compounding, independent bugs:
-
forge-ui/chat.go:79 builds the outbound A2A request to the agent process
using r.Context() (the browser's own request context). When the browser
tab reloads, the underlying fetch is aborted, cancelling this context,
which propagates all the way into forge-core/runtime/loop.go's agent
loop. The loop checks ctx.Err() at multiple points (loop.go ~377/452/697)
and returns early on cancellation — but persistSession (loop.go
~674/683/848) is only called on the successful-completion path. So a
cancelled turn is never written to disk, including the user's own
just-sent message.
-
If the cancelled turn was the FIRST turn of a brand-new session, no
session file has ever been created (forge-core/runtime/memory_store.go
Save() only creates the file when called), so the whole session appears
to have never existed — GET /api/agents/{id}/sessions returns an empty
list.
-
Separately (and this is what actually caused my "successful chat vanished"
report): forge-ui/static/app.js keeps sessionId in pure in-memory
React state (useState(null)), with no persistence to the URL, a
cookie, or localStorage. Any page reload resets it to null. If the user
then sends another message from this blank-looking state, chat.go:51-54
mints a brand-new session ID (fmt.Sprintf("%s-%d", agentID, time.Now().UnixNano())) rather than resuming the old one. This forks a
second, disconnected session file — the original conversation's JSON
file is still fully intact on disk, but it's orphaned from the UI and
looks like it "vanished."
I verified this directly: on my machine, .forge/sessions/ for one agent
contains a 20KB file with a full multi-turn conversation, alongside two
~300-byte orphaned single-exchange session files created after refreshes —
confirming Save() does a full-file overwrite per task ID (atomic
temp+fsync+rename, forge-core/runtime/memory_store.go Save()), never an
append-across-files, and that no existing successful session data was
actually destroyed — it was silently orphaned instead.
A fourth, related gap: forge-core/a2a/taskstore.go is a pure in-memory
map with no disk backing, and forge-ui/static/app.js never calls
tasks/get or any resubscribe endpoint to reattach to a still-in-flight
task after reload — so even though the A2A spec supports resubscription,
the frontend has no reconnect logic at all.
Steps to reproduce
- Run
forge init to scaffold an agent, then forge run/forge serve to
start it, then forge ui to open the web dashboard.
- Open the agent's chat page, send a message, and while the response is
still streaming/loading, refresh the browser tab (F5 / Ctrl+R).
- Observe the chat pane is now empty (no session auto-loaded).
- Type a new message without clicking anything in the "Sessions" sidebar.
- Check .forge/sessions/ on disk — a new session JSON file has been
created alongside the original, and the original's conversation is no
longer reachable from the UI without manually finding it in the sidebar.
- For a brand-new agent with no prior turns, repeat steps 1-2 on the very
first message — the sessions list (sidebar) returns empty entirely,
since no file was ever written before the refresh interrupted the turn.
Expected behavior
- Refreshing the page should resume the same session automatically (session
id persisted in the URL, e.g. #/agent/{id}/session/{sid}), not silently
fork a new one.
- A user's sent message should be durably persisted as soon as it's
received by the agent, independent of whether the LLM response completes,
so a refresh never loses the user's own input.
- Ideally, reloading mid-response should reattach to the still-in-flight
task and continue streaming the answer in, rather than leaving the user
unsure whether their message was received.
- The server-side proxy call to the agent process should not be cancelled
just because the browser's own HTTP connection was dropped (a reload is
not a deliberate "stop generating" action).
Actual behavior
- The outbound proxy request in forge-ui/chat.go inherits the incoming
browser request's context, so a page reload cancels agent processing
outright instead of merely disconnecting the display.
- persistSession (forge-core/runtime/loop.go) only saves on successful
completion, so any cancelled turn — including the user's own message —
is never written to disk.
- The frontend (forge-ui/static/app.js) keeps sessionId only in React
state, with no URL/cookie/localStorage persistence, so every reload
drops the active session and risks forking a new one on next send.
- There is no resubscribe/reconnect path from the frontend to an
in-flight A2A task, so a reload during streaming looks like data loss
even when the agent is still working server-side.
Operating system
Windows 11
Go version
go1.25.1 windows/amd64
forge.yaml (if relevant)
agent_id: assistant
version: 0.1.0
framework: forge
model:
provider: openai
name: gpt-5.4
version: "latest"
compression:
enabled: true
egress:
mode: allowlist
allowed_domains:
- api.openai.com
- api.openweathermap.org
- cdn.jsdelivr.net
- cdn.tailwindcss.co
- registry.npmjs.org
secrets:
providers:
- env
scheduler:
backend: file
Logs / error output
No error is thrown client- or server-side — this is a silent behavioral
gap, not a crash. Confirmed via direct filesystem inspection of
.forge/sessions/*.json: one 20KB file (real multi-turn conversation,
untouched) alongside orphaned ~300-byte single-exchange files created
after refreshes, e.g.:
{
"task_id": "assistant-1785251106286801900",
"messages": [
{"role": "user", "content": "hi mate"},
{"role": "assistant", "content": "Hi mate — how can I help?"}
],
"created_at": "2026-07-28T15:05:10.0788456Z",
"updated_at": "2026-07-28T15:05:10.0788456Z"
}
Component
Other
Description
When using
forge ui, refreshing the browser tab while an agent response isstill streaming (in-flight) can cause the in-progress turn — and sometimes
the entire visible chat session — to disappear. This is not actually data
corruption; it's three compounding, independent bugs:
forge-ui/chat.go:79 builds the outbound A2A request to the agent process
using
r.Context()(the browser's own request context). When the browsertab reloads, the underlying fetch is aborted, cancelling this context,
which propagates all the way into forge-core/runtime/loop.go's agent
loop. The loop checks
ctx.Err()at multiple points (loop.go ~377/452/697)and returns early on cancellation — but
persistSession(loop.go~674/683/848) is only called on the successful-completion path. So a
cancelled turn is never written to disk, including the user's own
just-sent message.
If the cancelled turn was the FIRST turn of a brand-new session, no
session file has ever been created (forge-core/runtime/memory_store.go
Save() only creates the file when called), so the whole session appears
to have never existed — GET /api/agents/{id}/sessions returns an empty
list.
Separately (and this is what actually caused my "successful chat vanished"
report): forge-ui/static/app.js keeps
sessionIdin pure in-memoryReact state (
useState(null)), with no persistence to the URL, acookie, or localStorage. Any page reload resets it to null. If the user
then sends another message from this blank-looking state, chat.go:51-54
mints a brand-new session ID (
fmt.Sprintf("%s-%d", agentID, time.Now().UnixNano())) rather than resuming the old one. This forks asecond, disconnected session file — the original conversation's JSON
file is still fully intact on disk, but it's orphaned from the UI and
looks like it "vanished."
I verified this directly: on my machine, .forge/sessions/ for one agent
contains a 20KB file with a full multi-turn conversation, alongside two
~300-byte orphaned single-exchange session files created after refreshes —
confirming Save() does a full-file overwrite per task ID (atomic
temp+fsync+rename, forge-core/runtime/memory_store.go Save()), never an
append-across-files, and that no existing successful session data was
actually destroyed — it was silently orphaned instead.
A fourth, related gap: forge-core/a2a/taskstore.go is a pure in-memory
map with no disk backing, and forge-ui/static/app.js never calls
tasks/get or any resubscribe endpoint to reattach to a still-in-flight
task after reload — so even though the A2A spec supports resubscription,
the frontend has no reconnect logic at all.
Steps to reproduce
forge initto scaffold an agent, thenforge run/forge servetostart it, then
forge uito open the web dashboard.still streaming/loading, refresh the browser tab (F5 / Ctrl+R).
created alongside the original, and the original's conversation is no
longer reachable from the UI without manually finding it in the sidebar.
first message — the sessions list (sidebar) returns empty entirely,
since no file was ever written before the refresh interrupted the turn.
Expected behavior
id persisted in the URL, e.g. #/agent/{id}/session/{sid}), not silently
fork a new one.
received by the agent, independent of whether the LLM response completes,
so a refresh never loses the user's own input.
task and continue streaming the answer in, rather than leaving the user
unsure whether their message was received.
just because the browser's own HTTP connection was dropped (a reload is
not a deliberate "stop generating" action).
Actual behavior
browser request's context, so a page reload cancels agent processing
outright instead of merely disconnecting the display.
completion, so any cancelled turn — including the user's own message —
is never written to disk.
state, with no URL/cookie/localStorage persistence, so every reload
drops the active session and risks forking a new one on next send.
in-flight A2A task, so a reload during streaming looks like data loss
even when the agent is still working server-side.
Operating system
Windows 11
Go version
go1.25.1 windows/amd64
forge.yaml (if relevant)
Logs / error output