- HANDOFF: rewritten resume point. First action on resume is `git push` (commits0f00ee5and665530fare local-only). Visual QA + bug bash is the active work; 4 plan-locked items + the structural task-lane fix all need real-browser verification. - CURRENT_TASK: add0f00ee5and665530fto the commit table; reframe "Just shipped" as a per-commit summary; flag the task-lane fix as needing visual confirmation. - SESSION_LOG: chronological entry for this session with full detail (audit, four polish items, race-condition wiring, structural task-lane fix, test status, files touched). - DECISIONS: new entry "Tag the task-lane state with an owner chatId" documenting the structural pattern, what was rejected, and the forward implication that future task-lane state slices follow the same owner-tagging pattern. Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com>
5.3 KiB
DECISIONS.md
Append-only architectural decision log. Newest entries at the top. Entry format:
## YYYY-MM-DD — <short title> **Context:** why this came up **Decision:** what we chose **Rejected:** what we didn't choose and why **Consequences:** what this means going forward
2026-04-28 — Tag the task-lane state with an owner chatId
Context: A recurring bug — every time the user returned to test escalation work, creating a new session would flash the previous session's task-lane data (questions, actions, "Tasks" pill counts) before the new session's AI response landed. The first attempt to fix it (8914391) added initializer-time guards (incomingPrefill || isPickup) that skipped the sessionStorage restore on mount. That covered exactly two entry paths and missed every other case: in-place URL navigation, mid-flight pickup, HMR re-runs, and the gap between setActiveChatId(B) and the AI response that finally populates B's questions/actions. The persistence effect made it worse by writing {chatId: activeChatId, questions: activeQuestions} — at any moment where activeChatId had flipped before the questions were updated, sessionStorage was stamped with {chatId: B, questions: [A's data]} and a subsequent restore would happily render A's data for B.
The root cause was that activeQuestions / activeActions / showTaskLane were three independent state slices implicitly assumed to be in sync with activeChatId. The synchronization was by convention, not by structure. Every code path that mutated them had to remember to call resetSessionDerivedState first; missing one created stale UI.
Decision: Add a taskLaneOwnerChatId state that records which chatId the in-memory questions/actions belong to, set at every site that populates them (sendPrefill, selectChat, handleSend, handleTaskSubmit, handleResumeNew, refreshFacts, handleApplyFix), cleared in resetSessionDerivedState. The persistence effect writes ownerChatId as the chatId tag. Render is gated on taskLaneOwnerChatId === activeChatId and ANDed into all three render conditions (toolbar Tasks button, narrow-viewport floating drawer, main side panel). The mount-time skipTaskLaneRestore guard stays as belt-and-braces for the prefill/pickup entry-flash window, which the owner-gate alone doesn't cover.
Rejected:
- More entry-path guards. That's whack-a-mole — the next path nobody anticipated will reproduce the bug. The owner-gate makes the bug structurally impossible regardless of which path triggers it.
- Combining the four state slices into a single tagged object. Cleaner long-term but a bigger refactor with more touch points. The owner-tracking approach gets the structural guarantee with a minimal diff and keeps the existing setState patterns.
- Inlining the comparison at every render site. Works but proliferates the comparison; one named derived value (
taskLaneIsForActiveChat) reads better and groups the gate with the persistence-effect / state declarations as a named concept.
Consequences:
- Stale task-lane data is structurally unable to display. The lane is hidden during any window where
ownerChatId !== activeChatId, no matter what mutation path got you there. - Adding new sites that populate
activeQuestions/activeActionsrequires also settingtaskLaneOwnerChatId. The pattern is documented in the commit message and visible in every existing populate site as a paired call. - The mount-time
skipTaskLaneRestoreguard is now redundant in steady-state but kept for the few-hundred-ms flash window between component mount and the first sendPrefill / selectChat effect. Deleting it would re-introduce a (smaller) flash without strong reason. - Future task-lane state slices (e.g.
facts,activeFix) follow the same pattern: gate their visibility on the owner check via the existing render conditions. Tagging more slices with their own*OwnerChatIdis a future refactor if the slices diverge.
2026-04-24 — Adopt dual-agent handoff system (.ai/ + CLAUDE.md + AGENTS.md)
Context: Claude Code hits session and weekly usage limits. Work stalls when the primary agent is locked out. Needed a structured way for OpenAI Codex to resume where Claude left off without losing architectural truth or drifting across sessions.
Decision: Split the old CLAUDE.md into .ai/PROJECT_CONTEXT.md (stable repo truth), agent-specific root files (CLAUDE.md, AGENTS.md) with a shared protocol block, and a small handoff toolkit (CURRENT_TASK.md, HANDOFF.md, TODO.md, DECISIONS.md, SESSION_LOG.md, README.md). Previous CLAUDE.md snapshotted in commit e110fed before the migration.
Rejected:
- Single symlinked CLAUDE.md/AGENTS.md — diverges silently, hides agent-specific tooling differences.
- Putting GitNexus/gstack content in AGENTS.md — Codex doesn't have those tools; would mislead the resume agent.
- Keeping the old CLAUDE.md as-is and adding AGENTS.md alongside it — duplicated truth, drift guaranteed.
Consequences:
- First read for either agent:
.ai/PROJECT_CONTEXT.md+.ai/CURRENT_TASK.md+.ai/HANDOFF.md. - Architectural changes in the repo require updating PROJECT_CONTEXT.md, not the root agent files.
- Git trailers differ per agent (
Claude Opus 4.7vsCodex) — preserved in each root file. - Legacy
SESSION-HANDOFF.mddeleted in the same commit; superseded by.ai/HANDOFF.md.