Skip to content

fix(chat): persist agent plan in chat thread across reload#39

Merged
AVADSA25 merged 1 commit intomainfrom
fix/chat-plan-persist
May 3, 2026
Merged

fix(chat): persist agent plan in chat thread across reload#39
AVADSA25 merged 1 commit intomainfrom
fix/chat-plan-persist

Conversation

@AVADSA25
Copy link
Copy Markdown
Owner

@AVADSA25 AVADSA25 commented May 3, 2026

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)

  1. codec_chat.html:819 — pushed only "Project drafted: agent_xxx" to local chatHist and didn't call saveMessages() at all for the project flow. The actual plan card with all the info lived in the DOM only.
  2. Even if saved, addMessage() renders plain text — no re-render path knew the message was a project drop.
  3. startNewSession() ran on every page load, generating a fresh sessionId. Hard refresh orphaned the previous chat under an id only reachable via the sidebar.

Fix

  1. Embed a marker [CODEC_AGENT_PLAN:<id>] in the saved assistant message content. Pattern locked to the JS regex:
    var AGENT_PLAN_MARKER_RE = /\[CODEC_AGENT_PLAN:(agent_[a-z0-9]+)\]/;
  2. Save the assistant message via /api/qchat/save (was missing).
  3. Refactor inline HTML into renderAgentPlanCard(id, info) that handles BOTH response shapes:
    • POST /api/agents flat: {agent_id, status, project_dir}
    • GET /api/agents/<id> nested: {manifest, plan, state, grants}
      Status-aware buttons:
    • pending → Approve + Reject + View
    • blocked_* → View+resolve + Abort
    • terminal (done/aborted/plan_failed) → View only
  4. New rehydrateAgentPlanCards() scans #messages .msg.assistant .msg-bubble after loadSession(), extracts agent_ids, fetches in parallel, replaces bubbles with live cards. On failure, shows "agent state not found — manifest may have been removed."
  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.

Tests

tests/test_chat_plan_persistence.py — 6/6 pass:

  • extractAgentIdFromMessage (Python parity port of the JS regex) finds real markers
  • Returns empty for no-marker / null / malformed
  • Ignores malformed markers (wrong case, missing brackets, no agent_ prefix)
  • 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"

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)

  • Drop a project in Project mode → card renders ✓ (existing behavior)
  • Click another session in the sidebar → return → card still there
  • Hard refresh (cmd+R) → land back on same session → card still there
  • Approve the plan → card updates to show running status (status-aware re-render)
  • Block the agent on a permission → card shows "View+resolve" + "Abort" buttons
  • Delete the manifest manually → card shows "agent state not found" notice instead of disappearing

🤖 Generated with Claude Code

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>
@AVADSA25 AVADSA25 merged commit 2b9be5b into main May 3, 2026
1 check passed
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.

2 participants