fix(chat): persist agent plan in chat thread across reload#39
Merged
Conversation
User-visible bug: drop a project in Project mode → CODEC drafts a plan
and renders an inline card with Approve/Reject/View-plan buttons →
refresh the chat or click another session → card is gone, only the
original user message + a "blocked_on_permission" status pill remain.
Reported multiple times — blocked the forex anchor demo on 2026-05-03.
Root cause:
1. codec_chat.html line 819 (pre-PR) only pushed
"Project drafted: agent_xxx" to local chatHist and DID NOT call
saveMessages() at all. The actual plan card lived in the DOM.
2. Even if saved, addMessage() renders content as plain text — no
re-render path knew the message was a project drop.
3. startNewSession() ran on every page load, generating a fresh
sessionId, so a hard refresh orphaned the previous chat under an
id only reachable via the sidebar.
Fix:
1. Embed a marker token "[CODEC_AGENT_PLAN:<id>]" inside the saved
assistant message content. Marker pattern locked to the JS regex:
/\[CODEC_AGENT_PLAN:(agent_[a-z0-9]+)\]/
2. Save the assistant message via /api/qchat/save (was missing
entirely for the project flow).
3. Refactor the inline plan-card HTML into renderAgentPlanCard(id, info)
that handles both shapes: POST /api/agents flat result AND
GET /api/agents/<id>'s {manifest, plan, state, grants}.
Status-aware buttons: pending=approve+reject+view, blocked_*=resolve+abort,
terminal=view-only.
4. New rehydrateAgentPlanCards() scans #messages .msg.assistant
.msg-bubble after loadSession(), extracts the agent_id via the
regex, fetches /api/agents/<id> in parallel, replaces the bubble
with the live card. Failure path keeps the marker text visible
with an "agent state not found" notice.
5. Persist sessionId in localStorage('codec-chat-session'). On boot,
if a saved session exists and is non-empty, replay it through the
same code path the sidebar uses. Hard refresh now lands the user
back on the same chat with cards intact.
Tests (tests/test_chat_plan_persistence.py — 6 cases, all pass):
- extractAgentIdFromMessage (Python parity port) finds real markers
- returns empty for no marker / null / malformed
- ignores malformed markers (wrong case, missing brackets, no agent_)
- byte-for-byte SQLite save→load round-trip preserves the marker
- multi-marker session preserves ordering
- locked to real-world hex id format (agent_1416ea3e1b02), rejects
underscored "ids"
Test approach: hermetic — re-creates qchat schema in a temp sqlite,
exercises the same INSERT/SELECT shape codec_dashboard.qchat_save and
qchat_session use. Avoids the codec_dashboard import (pynput chain).
Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
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.
User-visible bug
Drop a project in Project mode → CODEC drafts a plan and renders an inline card with Approve / Reject / View-plan buttons → refresh the chat OR click another session → card is gone, only the original user message + a "blocked_on_permission" status pill remain.
The user reported this multiple times. Blocked the forex anchor demo on 2026-05-03.
Root cause (3 stacked bugs)
codec_chat.html:819— pushed only"Project drafted: agent_xxx"to localchatHistand didn't callsaveMessages()at all for the project flow. The actual plan card with all the info lived in the DOM only.addMessage()renders plain text — no re-render path knew the message was a project drop.startNewSession()ran on every page load, generating a freshsessionId. Hard refresh orphaned the previous chat under an id only reachable via the sidebar.Fix
[CODEC_AGENT_PLAN:<id>]in the saved assistant message content. Pattern locked to the JS regex:/api/qchat/save(was missing).renderAgentPlanCard(id, info)that handles BOTH response shapes:/api/agentsflat:{agent_id, status, project_dir}/api/agents/<id>nested:{manifest, plan, state, grants}Status-aware buttons:
rehydrateAgentPlanCards()scans#messages .msg.assistant .msg-bubbleafterloadSession(), extracts agent_ids, fetches in parallel, replaces bubbles with live cards. On failure, shows "agent state not found — manifest may have been removed."sessionIdinlocalStorage('codec-chat-session'). On boot, if a saved session exists and is non-empty, replay it through the same code path the sidebar uses.Tests
tests/test_chat_plan_persistence.py— 6/6 pass:extractAgentIdFromMessage(Python parity port of the JS regex) finds real markersagent_prefix)agent_1416ea3e1b02), rejects underscored "ids"Hermetic: re-creates qchat schema in a temp sqlite, exercises the same INSERT/SELECT codec_dashboard.qchat_save/qchat_session use. Avoids the codec_dashboard import (pynput chain).
Test plan (manual after merge)
🤖 Generated with Claude Code