Skip to content

feat(storage): account isolation via organization UUID - #29

Merged
DevanshuNEU merged 8 commits into
OpenCodeIntel:mainfrom
DevanshuNEU:feat/lco-account-isolation
Apr 6, 2026
Merged

feat(storage): account isolation via organization UUID#29
DevanshuNEU merged 8 commits into
OpenCodeIntel:mainfrom
DevanshuNEU:feat/lco-account-isolation

Conversation

@DevanshuNEU

@DevanshuNEU DevanshuNEU commented Apr 5, 2026

Copy link
Copy Markdown
Contributor

Summary

  • All storage keys are now namespaced with the Claude organization UUID, isolating data between accounts sharing the same browser
  • Organization UUID extracted from the API URL (/organizations/{orgId}/chat_conversations/.../completion) by inject.ts
  • Threaded through the entire pipeline: inject.ts -> bridge -> content script -> background -> storage
  • Legacy read-through migration: old global keys are checked as fallback and copied to new scoped keys on first read

Why this matters

Before this PR, switching Claude accounts (or using multiple accounts in the same browser) mixed conversation history, token costs, and daily summaries. The TODAY card, HISTORY list, and ACTIVE CONVERSATION all showed combined data from every account. This was a data integrity bug.

Architecture

The organization UUID is already present in every Claude API completion URL. inject.ts extracts it inline (/organizations\/([0-9a-f-]+)\//i) and attaches it to TOKEN_BATCH and STREAM_COMPLETE bridge payloads. The content script captures it and forwards it on all background messages. The background uses it to scope every storage call.

Storage keys changed from conv:{convId} to conv:{orgId}:{convId}, convIndex to convIndex:{orgId}, etc. All index and summary functions accept accountId as their first parameter.

Test plan

  • bun run compile -- no type errors
  • bun run test -- 516 passing (all tests updated with TEST_ORG constant)
  • bun run build -- clean build, 996 kB
  • Manual: send messages on Account A, verify data shows only Account A
  • Manual: switch to Account B, verify empty state (no cross-contamination)
  • Manual: switch back to Account A, verify data still intact
  • Manual: verify legacy conversations migrate on first read

Summary by CodeRabbit

  • New Features

    • Pages and the extension detect and propagate organization context so actions are scoped to the correct account.
  • Refactor

    • Conversation data, daily/weekly summaries, indices, and cleanup are now stored per-organization with transparent migration from legacy unscoped data.
    • Active conversation state across tabs includes organization context; bridge/streaming messages include organization info.
  • Bug Fixes

    • Finalization, recording, summaries, pruning, and tab-clear logic now run per-organization to prevent cross-account mixups.
  • Tests

    • Added org-scoped tests, organization ID validation, migration and isolation coverage.

All storage keys are now namespaced with the Claude organization UUID,
isolating conversation history, daily summaries, and weekly summaries
between accounts sharing the same browser.

Data pipeline:
- inject.ts extracts the org UUID from the API URL
  (/organizations/{orgId}/chat_conversations/.../completion) and
  attaches it as organizationId on TOKEN_BATCH and STREAM_COMPLETE
  bridge payloads.
- Content script captures organizationId from bridge messages and
  forwards it on RECORD_TURN, FINALIZE_CONVERSATION, SET_ACTIVE_CONV,
  and GET_CONVERSATION messages to the background.
- Background writes activeOrg_{tabId} alongside activeConv_{tabId}
  in session storage so the side panel knows which account to query.

Storage layer (lib/conversation-store.ts):
- All key constants replaced with functions that accept accountId:
  convKey(accountId, convId), convIndexKey(accountId), etc.
- Every public function gains accountId as first parameter:
  recordTurn, getConversation, listConversations, finalizeConversation,
  computeDailySummary, getDailySummary, computeWeeklySummary, etc.
- Legacy read-through migration in getConversation: checks old global
  key as fallback, copies to new scoped key on first read.
- extractOrganizationId() added alongside extractConversationId().

Background alarms:
- getActiveOrgIds() collects all known org IDs from session storage
  (activeOrg_*) plus a persistent knownOrgIds set in local storage.
- computeDailySummary, computeWeeklySummary, pruneConversations alarms
  now iterate over all known accounts.

Side panel:
- useDashboardData reads activeOrg_{tabId} from session storage.
- All queries pass the org ID through to the storage layer.

Message types:
- organizationId added to TokenBatchPayload, StreamCompletePayload,
  RecordTurnMessage, FinalizeConversationMessage, GetConversationMessage,
  SetActiveConvMessage.
- Bridge validation accepts optional organizationId string.

Tests: all 516 tests updated with TEST_ORG constant, all passing.
@coderabbitai

coderabbitai Bot commented Apr 5, 2026

Copy link
Copy Markdown

Note

Reviews paused

It looks like this branch is under active development. To avoid overwhelming you with review comments due to an influx of new commits, CodeRabbit has automatically paused this review. You can configure this behavior by changing the reviews.auto_review.auto_pause_after_reviewed_commits setting.

Use the following commands to manage reviews:

  • @coderabbitai resume to resume automatic reviews.
  • @coderabbitai review to trigger a single review.

Use the checkboxes below for quick actions:

  • ▶️ Resume reviews
  • 🔍 Trigger review
📝 Walkthrough

Walkthrough

Organization-scoped conversation handling was added: org IDs are detected from Claude-related requests and propagated from inject → content → background, used to namespace storage keys and APIs, added per-org alarm handling, and implemented read-through migration from legacy unscoped records into per-org keys.

Changes

Cohort / File(s) Summary
Background Service Worker Org Scoping
entrypoints/background.ts
Track activeOrg_<tabId> in session storage; add getActiveOrgIds(); make cleanup, RECORD_TURN, FINALIZE_CONVERSATION, GET_CONVERSATION organization-scoped; clear active keys on non-claude navigation; run alarms (daily/weekly/prune) per org.
Content Script Org-aware Flow
entrypoints/claude-ai.content.ts
Add currentOrgId state; defer restores until org known; include organizationId in RECORD_TURN/FINALIZE/GET and SET_ACTIVE_CONV; capture org via ORGANIZATION_DETECTED or other bridge messages.
Inject: Org Detection & Propagation
entrypoints/inject.ts
Add extractOrgId(url); emit ORGANIZATION_DETECTED once per page for org URLs; thread organizationId into TOKEN_BATCH / STREAM_COMPLETE and into SSE decoding; compute org for completion requests.
Storage Layer: Account-scoped Keys & Migration
lib/conversation-store.ts
Introduce per-account key builders and per-account indexes; export extractOrganizationId(url); require accountId on convo/daily/weekly APIs; implement read-through migration from legacy unscoped keys and prefer scoped index when present.
Bridge Types & Validation
lib/message-types.ts, lib/bridge-validation.ts
Add ORGANIZATION_DETECTED message type; add optional organizationId to token/stream payloads; require organizationId for background messages where appropriate; validate organizationId when present.
Dashboard Hook Org Awareness
entrypoints/sidepanel/hooks/useDashboardData.ts
Add orgIdRef; early-return when org missing; load functions call org-scoped APIs; read both activeConv_<tabId> and activeOrg_<tabId>; refresh/clear dashboard on org changes or logout; adjust load sequence and storage-change triggers.
Tests Updated for Org Scoping
tests/unit/...conversation-*.test.ts, tests/unit/bridge-validation.test.ts
Add TEST_ORG; update tests to call org-scoped APIs and assert org-prefixed index keys; add tests for extractOrganizationId, org isolation, legacy migration, and organizationId validation.

Sequence Diagram

sequenceDiagram
    participant Inject as Inject (page)
    participant Content as ContentScript
    participant Background as BackgroundScript
    participant Store as StorageLayer
    participant Sidepanel as Sidepanel/Dashboard

    Note right of Inject: extractOrgId(url) -> organizationId
    Inject->>Content: ORGANIZATION_DETECTED {organizationId}
    Content->>Background: SET_ACTIVE_CONV {conversationId, organizationId}
    Content-->>Background: RECORD_TURN {organizationId, conversationId, tokens...}
    Background->>Store: recordTurn(accountId, convId, ...)
    Store-->>Background: ack
    Background->>Background: set session keys activeConv_{tab}, activeOrg_{tab}
    Sidepanel->>Background: getActiveOrgIds()
    Background-->>Sidepanel: [orgA, orgB]
    Sidepanel->>Store: listConversations(orgA,...)
    Store-->>Sidepanel: org-scoped conversation list
Loading

Estimated code review effort

🎯 4 (Complex) | ⏱️ ~60 minutes

Possibly related PRs

Poem

🐰 I sniffed the URLs and found each burrow's name,

I hopped through tabs and saved each chat with care,
Old nests moved gently into labeled lanes,
Now every story sits in its own lair,
🥕 Hooray — neat hops and tidy things to share!

🚥 Pre-merge checks | ✅ 2 | ❌ 1

❌ Failed checks (1 warning)

Check name Status Explanation Resolution
Docstring Coverage ⚠️ Warning Docstring coverage is 71.79% which is insufficient. The required threshold is 80.00%. Write docstrings for the functions missing them to satisfy the coverage threshold.
✅ Passed checks (2 passed)
Check name Status Explanation
Title check ✅ Passed The title 'feat(storage): account isolation via organization UUID' directly and accurately describes the primary change: implementing organization UUID-based namespacing for storage key isolation between accounts.
Description check ✅ Passed The pull request description comprehensively covers all required template sections: Summary, Type of Change (feat), What Was Changed, How to Test (with planned manual steps), comprehensive Checklist with test status, Related Issues section, and Notes for Reviewer with architectural rationale.

✏️ Tip: You can configure your own custom pre-merge checks in the settings.

✨ Finishing Touches
🧪 Generate unit tests (beta)
  • Create PR with unit tests

Thanks for using CodeRabbit! It's free for OSS, and your support helps us grow. If you like it, consider giving us a shout-out.

❤️ Share

Comment @coderabbitai help to get the list of available commands and usage tips.

19 new tests covering the account isolation feature:

extractOrganizationId (6 tests):
- Extracts UUID from completion API URL
- Lowercases the UUID
- Handles query strings
- Returns null for page URLs, empty strings, non-matching URLs

Account isolation (5 tests):
- Same conversation ID under two accounts produces separate records
- listConversations returns only the queried account's data
- Daily summaries are isolated per account (same date, different tokens)
- pruneConversations only deletes from the target account
- finalizeConversation only affects the target account

Legacy data migration (3 tests):
- Reads from old global key when scoped key does not exist
- Copies legacy data to new scoped key on first read
- New scoped key takes precedence over old global key

Bridge validation: organizationId (5 tests):
- TOKEN_BATCH with valid organizationId passes
- TOKEN_BATCH without organizationId passes (backward compat)
- TOKEN_BATCH with non-string organizationId fails
- STREAM_COMPLETE with valid organizationId passes
- STREAM_COMPLETE with non-string organizationId fails

Total: 535 passing (was 516).

@coderabbitai coderabbitai Bot left a comment

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Actionable comments posted: 3

Caution

Some comments are outside the diff and can’t be posted inline due to platform limitations.

⚠️ Outside diff range comments (2)
entrypoints/background.ts (2)

280-284: ⚠️ Potential issue | 🟠 Major

Missing activeOrg_ storage on RECORD_TURN.

The handler stores activeConv_{tabId} but not activeOrg_{tabId}. If RECORD_TURN fires before or without a SET_ACTIVE_CONV message, the tab will have a conversation ID but no org ID. When the tab closes, cleanTabStorage (line 174) skips finalization because orgId is undefined.

Fix: Store both keys together
            if (tabId !== undefined) {
+             const setData: Record<string, string> = {
+               [`activeConv_${tabId}`]: message.conversationId,
+             };
+             if (message.organizationId) {
+               setData[`activeOrg_${tabId}`] = message.organizationId;
+             }
              browser.storage.session.set({
-               [`activeConv_${tabId}`]: message.conversationId,
+               ...setData,
              }).catch(() => { /* non-critical */ });
            }
🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed.

In `@entrypoints/background.ts` around lines 280 - 284, In the RECORD_TURN message
handler, you're only persisting [`activeConv_${tabId}`] but not the
corresponding [`activeOrg_${tabId}`], which leads cleanTabStorage to skip
finalization when orgId is missing; update the RECORD_TURN branch (the code that
checks tabId !== undefined and calls browser.storage.session.set) to store both
keys atomically by including [`activeOrg_${tabId}`]: message.orgId along with
[`activeConv_${tabId}`]: message.conversationId so both conversation and org are
set together.

195-198: ⚠️ Potential issue | 🟡 Minor

Bug: Orphan cleanup does not include activeOrg_ keys.

The regex excludes activeOrg_ from orphan detection. If the service worker misses a tab-close event (e.g., browser crash), stale activeOrg_ keys will accumulate in session storage.

Fix: Add `activeOrg` to the regex alternation
-    const match = key.match(/^(?:tabState|sessionCost|activeConv)_(\d+)$/);
+    const match = key.match(/^(?:tabState|sessionCost|activeConv|activeOrg)_(\d+)$/);
🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed.

In `@entrypoints/background.ts` around lines 195 - 198, The orphan detection regex
used to build orphanKeys (filtering keys from allData against activeIds) omits
keys starting with "activeOrg_", so stale activeOrg_* entries aren't cleaned;
update the regex in the orphanKeys filter (the key.match(...) call used to
compute orphanKeys) to include "activeOrg" in the alternation (e.g., add
activeOrg alongside tabState, sessionCost, activeConv) so match[1] still
captures the id and orphan entries for activeOrg_* are detected and removed.
🧹 Nitpick comments (2)
lib/conversation-store.ts (1)

279-301: Legacy migration preserves old data for rollback safety.

The read-through migration copies data from the legacy key to the new scoped key but does not delete the old key. This is a reasonable approach for safe rollout since it allows rollback without data loss. However, consider adding a future cleanup mechanism to remove legacy keys after sufficient migration time.

🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed.

In `@lib/conversation-store.ts` around lines 279 - 301, getConversation currently
migrates legacy records by copying from legacyConvKey(id) to the new
convKey(accountId, id) and updating the index but leaves the old legacy key in
place; add a cleanup mechanism so legacy keys are removed after successful
migration (or scheduled for later deletion) to avoid stale duplicates: after
successful store().set({ [key]: oldRecord }) and
addToIndex(convIndexKey(accountId), id) either delete the legacy entry via
store().delete(legacyConvKey(id)) or enqueue a background task for deferred
deletion, and expose a helper like scheduleLegacyCleanup(id) to centralize that
logic and make the cleanup policy configurable.
entrypoints/sidepanel/hooks/useDashboardData.ts (1)

130-144: Storage change listener should check for org-scoped key patterns.

The listener checks k.startsWith('conv:') which will match both legacy keys (conv:{convId}) and new org-scoped keys (conv:{orgId}:{convId}). This is acceptable but consider that after full migration, only org-scoped keys will exist. The convIndex check on line 132 only matches the legacy global key, not org-scoped convIndex:{orgId}.

Consider updating the index key check
-                const hasConvChange = keys.some(k => k.startsWith('conv:') || k === 'convIndex');
+                const hasConvChange = keys.some(k => k.startsWith('conv:') || k.startsWith('convIndex'));

This would also trigger on org-scoped index changes like convIndex:org-123.

🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed.

In `@entrypoints/sidepanel/hooks/useDashboardData.ts` around lines 130 - 144, The
storage-change listener currently only treats the legacy global index key
'convIndex' specially; update the index check so it also matches org-scoped
index keys (e.g., 'convIndex:{orgId}') by changing the test used in the
hasConvChange branch to consider keys that equal 'convIndex' OR start with
'convIndex:'. Locate the block using keys.some(...) alongside loadConversations,
loadActiveConversation, loadToday and tabIdRef, and replace the existing
convIndex equality check with a startsWith-aware check so org-scoped index
updates trigger the same refresh behavior.
🤖 Prompt for all review comments with AI agents
Verify each finding against the current code and only fix it if needed.

Inline comments:
In `@entrypoints/background.ts`:
- Around line 374-404: The alarm handlers currently call getActiveOrgIds()
without handling rejections, causing unhandled promise rejections if that call
fails; update each branch (alarm.name === 'computeDailySummary',
'computeWeeklySummary', and 'pruneOldData') to add a .catch on the
getActiveOrgIds() promise and log the error (include context such as which alarm
failed) instead of leaving it unhandled, while keeping the existing per-org
.catch for computeDailySummary, computeWeeklySummary, and pruneConversations to
handle per-org errors.
- Around line 174-176: The current guard (if (convId && orgId)) skips
finalizeConversation when convId exists but orgId is missing, leaving
conversations unfinalized; update the block so that when convId is present but
orgId is falsy you attempt a fallback org ID (e.g., derive from activeOrg_
state, a stored default org, or a getDefaultOrgId helper) and call
finalizeConversation(orgFallback, convId), and if no fallback is available emit
a processLogger.warn/console.warn indicating missing activeOrg_ and the convId
so the issue is visible; ensure the finalizeConversation call remains wrapped
with .catch(() => { /* non-critical */ }) to preserve non-blocking behavior.

In `@entrypoints/claude-ai.content.ts`:
- Around line 114-120: Currently the initial restore calls
fetchStoredRecord(currentOrgId, currentConversationId) even when currentOrgId is
null, causing legacy unscoped records to be missed; change the logic around
browser.runtime.sendMessage/SET_ACTIVE_CONV so you only call fetchStoredRecord
when currentOrgId is non-null (i.e., defer restore on initial load), and add the
same restore call into the bridge message handler that sets currentOrgId so
legacy records are fetched once the bridge supplies the org id (ensure you
update the handler that sets currentOrgId/currentConversationId to invoke
fetchStoredRecord when appropriate).

---

Outside diff comments:
In `@entrypoints/background.ts`:
- Around line 280-284: In the RECORD_TURN message handler, you're only
persisting [`activeConv_${tabId}`] but not the corresponding
[`activeOrg_${tabId}`], which leads cleanTabStorage to skip finalization when
orgId is missing; update the RECORD_TURN branch (the code that checks tabId !==
undefined and calls browser.storage.session.set) to store both keys atomically
by including [`activeOrg_${tabId}`]: message.orgId along with
[`activeConv_${tabId}`]: message.conversationId so both conversation and org are
set together.
- Around line 195-198: The orphan detection regex used to build orphanKeys
(filtering keys from allData against activeIds) omits keys starting with
"activeOrg_", so stale activeOrg_* entries aren't cleaned; update the regex in
the orphanKeys filter (the key.match(...) call used to compute orphanKeys) to
include "activeOrg" in the alternation (e.g., add activeOrg alongside tabState,
sessionCost, activeConv) so match[1] still captures the id and orphan entries
for activeOrg_* are detected and removed.

---

Nitpick comments:
In `@entrypoints/sidepanel/hooks/useDashboardData.ts`:
- Around line 130-144: The storage-change listener currently only treats the
legacy global index key 'convIndex' specially; update the index check so it also
matches org-scoped index keys (e.g., 'convIndex:{orgId}') by changing the test
used in the hasConvChange branch to consider keys that equal 'convIndex' OR
start with 'convIndex:'. Locate the block using keys.some(...) alongside
loadConversations, loadActiveConversation, loadToday and tabIdRef, and replace
the existing convIndex equality check with a startsWith-aware check so
org-scoped index updates trigger the same refresh behavior.

In `@lib/conversation-store.ts`:
- Around line 279-301: getConversation currently migrates legacy records by
copying from legacyConvKey(id) to the new convKey(accountId, id) and updating
the index but leaves the old legacy key in place; add a cleanup mechanism so
legacy keys are removed after successful migration (or scheduled for later
deletion) to avoid stale duplicates: after successful store().set({ [key]:
oldRecord }) and addToIndex(convIndexKey(accountId), id) either delete the
legacy entry via store().delete(legacyConvKey(id)) or enqueue a background task
for deferred deletion, and expose a helper like scheduleLegacyCleanup(id) to
centralize that logic and make the cleanup policy configurable.
🪄 Autofix (Beta)

Fix all unresolved CodeRabbit comments on this PR:

  • Push a commit to this branch (recommended)
  • Create a new PR with the fixes

ℹ️ Review info
⚙️ Run configuration

Configuration used: Organization UI

Review profile: CHILL

Plan: Pro

Run ID: ed531336-7cd7-4cd1-ac89-19c9505db9c2

📥 Commits

Reviewing files that changed from the base of the PR and between b45c474 and ac4f4e7.

📒 Files selected for processing (9)
  • entrypoints/background.ts
  • entrypoints/claude-ai.content.ts
  • entrypoints/inject.ts
  • entrypoints/sidepanel/hooks/useDashboardData.ts
  • lib/bridge-validation.ts
  • lib/conversation-store.ts
  • lib/message-types.ts
  • tests/unit/conversation-dna.test.ts
  • tests/unit/conversation-store.test.ts

Comment thread entrypoints/background.ts
Comment thread entrypoints/background.ts
Comment thread entrypoints/claude-ai.content.ts Outdated
The initial SET_ACTIVE_CONV fires at page load before any API call, so
organizationId is null. The background never writes activeOrg_{tabId},
and the side panel cannot scope its queries. Result: dashboard shows
all zeros and "No conversations yet" despite the overlay working fine.

Fix: when the content script receives the first bridge message carrying
an organizationId (from TOKEN_BATCH or STREAM_COMPLETE), re-send
SET_ACTIVE_CONV with the now-known org ID. The background writes
activeOrg_{tabId}, the side panel reads it, and all queries are scoped.

@coderabbitai coderabbitai Bot left a comment

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Actionable comments posted: 1

🧹 Nitpick comments (1)
tests/unit/conversation-store.test.ts (1)

612-638: Also assert that migration makes the conversation discoverable.

This only verifies the copied record. getConversation() also updates the scoped index in lib/conversation-store.ts:279-301; if that regresses, listConversations(TEST_ORG, ...) breaks while this test still passes.

Suggested assertion
         // New scoped key should now exist.
         const scopedKey = `conv:${TEST_ORG}:migrate-me`;
         expect(mockStore._raw[scopedKey]).toBeDefined();
         expect((mockStore._raw[scopedKey] as ConversationRecord).totalInputTokens).toBe(100);
+        expect(await listConversations(TEST_ORG, 10)).toEqual(
+            expect.arrayContaining([
+                expect.objectContaining({ id: 'migrate-me' }),
+            ]),
+        );
🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed.

In `@tests/unit/conversation-store.test.ts` around lines 612 - 638, The test
currently only asserts the legacy record was copied to the new scoped key but
doesn't verify the migration updated the scoped index, so add an assertion that
the migrated conversation is discoverable via the listing API: after calling
getConversation(TEST_ORG, 'migrate-me') call listConversations(TEST_ORG, ...)
(or the existing helper used in tests) and assert the returned list includes the
conversation id 'migrate-me' (or the scoped key), ensuring the code paths in
getConversation and the scoped index update (the logic referenced around
getConversation/listConversations) are exercised.
🤖 Prompt for all review comments with AI agents
Verify each finding against the current code and only fix it if needed.

Inline comments:
In `@tests/unit/conversation-store.test.ts`:
- Around line 496-499: The test for extractOrganizationId is using a non-UUID
fragment ("aabb0042-ccdd-eeff") which allows partial hex/hyphen matches; update
the test to use a full valid UUID string (e.g., 8-4-4-4-12 format) in the URL
passed to extractOrganizationId and assert that exact UUID is returned, and add
a negative case asserting that a partial fragment does NOT return a value (or
returns null/undefined) to prevent accepting malformed org IDs; reference the
extractOrganizationId function in your changes.

---

Nitpick comments:
In `@tests/unit/conversation-store.test.ts`:
- Around line 612-638: The test currently only asserts the legacy record was
copied to the new scoped key but doesn't verify the migration updated the scoped
index, so add an assertion that the migrated conversation is discoverable via
the listing API: after calling getConversation(TEST_ORG, 'migrate-me') call
listConversations(TEST_ORG, ...) (or the existing helper used in tests) and
assert the returned list includes the conversation id 'migrate-me' (or the
scoped key), ensuring the code paths in getConversation and the scoped index
update (the logic referenced around getConversation/listConversations) are
exercised.
🪄 Autofix (Beta)

Fix all unresolved CodeRabbit comments on this PR:

  • Push a commit to this branch (recommended)
  • Create a new PR with the fixes

ℹ️ Review info
⚙️ Run configuration

Configuration used: Organization UI

Review profile: CHILL

Plan: Pro

Run ID: 1d972806-31f6-40a4-9d1d-e416541f05b8

📥 Commits

Reviewing files that changed from the base of the PR and between ac4f4e7 and 71f9ad2.

📒 Files selected for processing (3)
  • entrypoints/claude-ai.content.ts
  • tests/unit/bridge-validation.test.ts
  • tests/unit/conversation-store.test.ts
🚧 Files skipped from review as they are similar to previous changes (1)
  • entrypoints/claude-ai.content.ts

Comment thread tests/unit/conversation-store.test.ts
listConversations now checks the legacy global convIndex when the
account-scoped index is empty. If legacy entries exist, all records
are copied to account-scoped keys and added to the new index in one
pass. This ensures HISTORY shows pre-migration conversations.

Side panel init order changed: loadConversations runs before loadToday
so the bulk migration completes before the daily summary tries to
aggregate from the (now-populated) account-scoped conversation records.
Three changes to handle logout and account switching:

1. background.ts: tabs.onUpdated listener detects when a tab navigates
   away from claude.ai (logout redirect to accounts.google.com). Clears
   activeConv_{tabId} and activeOrg_{tabId} from session storage so the
   side panel knows the account context is gone.

2. useDashboardData: loadActiveConversation now detects when the org ID
   changes (account switch) or is cleared (logout). On logout: resets
   orgIdRef and clears today/conversations/activeConv state to empty.
   On account switch: re-runs loadConversations and loadToday with the
   new org scope.

3. Live subscription: now listens for activeOrg_ changes in session
   storage (not just activeConv_), so the dashboard refreshes when the
   background clears the org key on logout.

@coderabbitai coderabbitai Bot left a comment

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Actionable comments posted: 3

Caution

Some comments are outside the diff and can’t be posted inline due to platform limitations.

⚠️ Outside diff range comments (1)
entrypoints/background.ts (1)

268-277: ⚠️ Potential issue | 🟠 Major

Reject scoped writes until organizationId is resolved.

The content and bridge path can still produce organizationId: null or undefined before the first org-bearing message arrives. Passing that straight into recordTurn() or finalizeConversation() creates pseudo-scoped keys like conv:null:* instead of isolating data per account. Fail fast here when the org is missing.

Suggested guard at the message boundary
       if (message.type === 'RECORD_TURN') {
         const tabId = sender.tab?.id;
+        if (!message.organizationId) {
+          sendResponse({ ok: false });
+          return false;
+        }
         recordTurn(message.organizationId, message.conversationId, {
           inputTokens: message.inputTokens,
           outputTokens: message.outputTokens,
           model: message.model,
@@
       if (message.type === 'FINALIZE_CONVERSATION') {
+        if (!message.organizationId) {
+          sendResponse({ ok: false });
+          return false;
+        }
         finalizeConversation(message.organizationId, message.conversationId)
           .then(() => sendResponse({ ok: true }))

Also applies to: 294-301

🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed.

In `@entrypoints/background.ts` around lines 268 - 277, The message handler must
reject scoped writes when organizationId is missing: in the RECORD_TURN branch
(where recordTurn(...) is called) add a guard that checks message.organizationId
is non-null/undefined and if missing, log or ignore and return early to avoid
creating keys like conv:null:*, and apply the same early-return guard in the
finalizeConversation branch (the place that calls finalizeConversation(...)) so
no scoped writes occur until an org-bearing message has been resolved.
🤖 Prompt for all review comments with AI agents
Verify each finding against the current code and only fix it if needed.

Inline comments:
In `@entrypoints/background.ts`:
- Around line 354-361: The onUpdated listener that checks changeInfo.url
currently only removes activeConv_{tabId} and activeOrg_{tabId}; change it to
call the centralized per-tab cleanup (e.g., cleanTabStorage(tabId)) or a new
helper that first finalizes the conversation and then clears all per-tab keys
(activeConv_, activeOrg_, tabState_{tabId}, sessionCost_{tabId}, etc.) so
sessionCost_ and tabState_ do not persist across navigations; update the
browser.tabs.onUpdated callback to invoke cleanTabStorage(tabId) (or the
extracted helper) instead of removing only the two keys.
- Around line 321-329: When handling SetActiveConvMessage in
entrypoints/background.ts, avoid leaving a stale activeOrg_${tabId} when
message.conversationId is set but message.organizationId is null; update the
branch that uses convKey and orgKey so that if message.conversationId exists you
always set activeConv_${tabId} and explicitly remove activeOrg_${tabId} when
message.organizationId is null (otherwise set it when present), using
browser.storage.session.set/remove (preserving the existing .catch(() => {})
behavior) so the panel won't continue querying the new conversation under the
old org.

In `@lib/conversation-store.ts`:
- Around line 407-423: The current bulk migration loop that reads
LEGACY_CONV_INDEX_KEY and copies every legacy id into the first accountId breaks
org isolation; instead, remove the bulk copy and implement deferred, per-record
migration: stop iterating legacyIndex in the listConversations path and leave
legacy entries in place, then change the code paths that read individual
conversations (e.g., the function that calls store().get(legacyConvKey(id)) or
getConversation) to migrate a single legacy record only when its org provenance
is known or matches the current accountId — use legacyConvKey and readIndex to
locate the record, inspect the record metadata for org/account ownership, and
only call store().set({ [convKey(accountId, id)]: record }) and
addToIndex(convIndexKey(accountId), id) when the record truly belongs to that
account; alternatively move legacy records to a neutral legacy bucket instead of
importing them into an account index to prevent double-imports.

---

Outside diff comments:
In `@entrypoints/background.ts`:
- Around line 268-277: The message handler must reject scoped writes when
organizationId is missing: in the RECORD_TURN branch (where recordTurn(...) is
called) add a guard that checks message.organizationId is non-null/undefined and
if missing, log or ignore and return early to avoid creating keys like
conv:null:*, and apply the same early-return guard in the finalizeConversation
branch (the place that calls finalizeConversation(...)) so no scoped writes
occur until an org-bearing message has been resolved.
🪄 Autofix (Beta)

Fix all unresolved CodeRabbit comments on this PR:

  • Push a commit to this branch (recommended)
  • Create a new PR with the fixes

ℹ️ Review info
⚙️ Run configuration

Configuration used: Organization UI

Review profile: CHILL

Plan: Pro

Run ID: fc6a5b22-e4f3-4fbf-9717-f85d27d0cda7

📥 Commits

Reviewing files that changed from the base of the PR and between 71f9ad2 and 4ab6942.

📒 Files selected for processing (3)
  • entrypoints/background.ts
  • entrypoints/sidepanel/hooks/useDashboardData.ts
  • lib/conversation-store.ts
🚧 Files skipped from review as they are similar to previous changes (1)
  • entrypoints/sidepanel/hooks/useDashboardData.ts

Comment thread entrypoints/background.ts
Comment thread entrypoints/background.ts
Comment thread lib/conversation-store.ts Outdated
The org ID was only extracted from completion endpoints, which meant
Saar was blind to the account until the user sent their first message.
Between login and first message, the side panel showed stale data from
the previous account.

Fix: inject.ts now watches ALL fetches to /api/organizations/ (not just
completions). Claude.ai makes dozens of API calls on page load:
conversations list, settings, user info, etc. The first one that hits
/api/organizations/{orgId}/ triggers ORGANIZATION_DETECTED, a new
lightweight bridge message that carries just the org ID.

The content script handles ORGANIZATION_DETECTED by setting currentOrgId
and immediately re-sending SET_ACTIVE_CONV with the org ID. The side
panel picks up the org change via its session storage listener and
scopes all queries to the correct account.

Timeline:
- Before: org ID available after first user message (~seconds to minutes)
- After: org ID available within milliseconds of page load (first API call)

The TOKEN_BATCH/STREAM_COMPLETE fallback path is kept for robustness in
case ORGANIZATION_DETECTED is missed (inject.ts loaded late, etc.).
- background.ts: add .catch() on getActiveOrgIds() in all three alarm handlers
- background.ts: log warning in cleanTabStorage when convId exists but orgId missing
- background.ts: RECORD_TURN now stores activeOrg_{tabId} alongside activeConv_{tabId}
- background.ts: RECORD_TURN guards against missing organizationId
- background.ts: orphan cleanup regex now includes activeOrg_ keys
- background.ts: onUpdated calls cleanTabStorage (finalize + full cleanup) on navigation away
- background.ts: SET_ACTIVE_CONV removes stale activeOrg_ when organizationId is null
- content: fetchStoredRecord deferred until org ID is known; ORGANIZATION_DETECTED retries restore
- conversation-store: delete legacy key after per-record migration in getConversation
- conversation-store: listConversations reads legacy data without bulk-migrating to first accountId
- useDashboardData: convIndex check uses startsWith to catch scoped convIndex:{orgId} keys

@coderabbitai coderabbitai Bot left a comment

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Actionable comments posted: 1

Caution

Some comments are outside the diff and can’t be posted inline due to platform limitations.

⚠️ Outside diff range comments (2)
entrypoints/claude-ai.content.ts (1)

143-155: ⚠️ Potential issue | 🟠 Major

Add the missing event.source bridge check.

This listener currently validates origin, namespace, token, and schema, but it still skips the source layer required for the Claude bridge.

Suggested fix
 window.addEventListener('message', (event) => {
+        if (event.source !== window) return;
         if (event.origin !== window.location.origin) return;
         if (!event.data || event.data.namespace !== LCO_NAMESPACE) return;
         if (event.data.token !== sessionToken) return;
         if (!isValidBridgeSchema(event.data)) return;

As per coding guidelines, "Validate all incoming postMessages with 5 layers: origin, source, namespace LCO_V1, session token, and schema validation; drop messages that fail any check".

🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed.

In `@entrypoints/claude-ai.content.ts` around lines 143 - 155, The message event
listener (window.addEventListener callback) is missing the required event.source
check: add a guard that ensures event.source === window to validate the Claude
bridge sender before proceeding (in addition to existing checks against
LCO_NAMESPACE, sessionToken, isValidBridgeSchema, and origin). Update the
listener where msg is derived (and before casting to LcoBridgeMessage) to
early-return when event.source is not the expected window reference so all five
validation layers (origin, source, namespace LCO_NAMESPACE, sessionToken, and
isValidBridgeSchema) are enforced.
entrypoints/sidepanel/hooks/useDashboardData.ts (1)

69-117: ⚠️ Potential issue | 🟠 Major

Use the freshly read activeOrg_ value as the source of truth.

When the tab gets its first org ('' -> uuid), orgChanged stays false, so today and conversations never reload. The inverse edge also bites: if activeConv_ exists while activeOrg_ has been cleared, this code keeps orgIdRef.current from the previous account and can query the wrong scope. Base the guard and refresh logic on orgId from session storage, clear the ref immediately when it is absent, and treat any org transition as a refresh.

Suggested fix
             const convId = result[cKey] as string | undefined;
             const orgId = result[oKey] as string | undefined;

-            // Detect account switch or logout: org ID changed or was cleared.
             const prevOrg = orgIdRef.current;
-            if (orgId) {
-                orgIdRef.current = orgId;
-            }
-
-            // Account changed (switched accounts or logged out and back in).
-            // Re-fetch history and today for the new account scope.
-            const orgChanged = prevOrg !== '' && orgId !== undefined && orgId !== prevOrg;
+            const nextOrg = orgId ?? '';
+            const orgChanged = nextOrg !== prevOrg;
+            orgIdRef.current = nextOrg;

-            if (!convId || !orgIdRef.current) {
+            if (!convId || !nextOrg) {
                 setActiveConv(null);
                 setActiveHealth(null);
-                // Org cleared (logout): reset dashboard to empty state.
-                if (!orgId && prevOrg) {
-                    orgIdRef.current = '';
+                if (orgChanged) {
                     setToday(null);
                     setConversations([]);
                 }
                 return;
             }

-            const conv = await getConversation(orgIdRef.current, convId);
+            const conv = await getConversation(nextOrg, convId);
             setActiveConv(conv);
@@
             // Account switched: reload history and today for the new org.
             if (orgChanged) {
-                loadConversations();
-                loadToday();
+                await Promise.all([loadConversations(), loadToday()]);
             }
🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed.

In `@entrypoints/sidepanel/hooks/useDashboardData.ts` around lines 69 - 117, The
code is using orgIdRef.current instead of the freshly read orgId from session
storage which prevents reloads on first-login and can query the wrong org after
logout; update the logic in the block that reads cKey/oKey so that you: treat
the session-read orgId as the source of truth (assign orgIdRef.current = orgId
immediately when orgId is present and clear it to '' immediately when orgId is
absent), compute orgChanged by comparing prevOrg to the freshly read orgId (not
orgIdRef.current), use orgId (or early-return if absent) when calling
getConversation, and trigger loadConversations/loadToday whenever prevOrg !==
orgId to ensure any org transition reloads history and today; keep existing
calls to setActiveConv, setActiveHealth, setToday, and setConversations but base
their control flow on the session-read orgId and convId.
♻️ Duplicate comments (1)
entrypoints/claude-ai.content.ts (1)

151-200: ⚠️ Potential issue | 🟠 Major

Extract a shared hydration path and use it in the fallback branch too.

ORGANIZATION_DETECTED restores stored totals, but the TOKEN_BATCH / STREAM_COMPLETE fallback only sets currentOrgId. If early org detection is missed, the first resumed turn is still computed and recorded from a zero baseline. Pull the restore sequence into an orchestrator helper and run it before processing the first fallback message.

Based on learnings, "Applies to entrypoints/claude-ai.content.ts : Message bridge (postMessage listener, validation, forwarding) in content script must remain a thin relay with no business logic; keep it separate from orchestrator section".

🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed.

In `@entrypoints/claude-ai.content.ts` around lines 151 - 200, The
restore-on-org-detection logic in the ORGANIZATION_DETECTED branch (the sequence
that calls fetchStoredRecord, updates
cumulativeInput/cumulativeOutput/cumulativeCost, calls applyRestoredConversation
and buildConvStateFromRecord, computes health via computeHealthScore, sets
state/convState and calls overlay.render) must be extracted into a single helper
(e.g., restoreConversationForOrg or similar) and invoked from both the
ORGANIZATION_DETECTED path and the fallback that checks 'organizationId' in msg
(TOKEN_BATCH/STREAM_COMPLETE fallback) before proceeding; ensure the helper
accepts currentOrgId and currentConversationId and preserves the navGeneration
check, returns/updates state/convState/cumulative* values, and keep the
message-bridge thin by only calling that orchestrator helper from these branches
and not duplicating the logic.
🤖 Prompt for all review comments with AI agents
Verify each finding against the current code and only fix it if needed.

Inline comments:
In `@lib/conversation-store.ts`:
- Around line 107-122: The scoped key builders (convKey, convIndexKey, dailyKey,
dailyIndexKey, weeklyKey, weeklyIndexKey) currently accept empty accountId and
produce keys like "conv::id"; update each builder to validate the accountId and
reject empty/missing values at the boundary (e.g., if accountId is falsy or
empty string throw an Error or assert) so callers cannot create scoped keys for
the empty-org bucket; keep the legacy key builders (legacyConvKey,
LEGACY_CONV_INDEX_KEY, legacyDailyKey, LEGACY_DAILY_INDEX_KEY) unchanged for
read-through migration.

---

Outside diff comments:
In `@entrypoints/claude-ai.content.ts`:
- Around line 143-155: The message event listener (window.addEventListener
callback) is missing the required event.source check: add a guard that ensures
event.source === window to validate the Claude bridge sender before proceeding
(in addition to existing checks against LCO_NAMESPACE, sessionToken,
isValidBridgeSchema, and origin). Update the listener where msg is derived (and
before casting to LcoBridgeMessage) to early-return when event.source is not the
expected window reference so all five validation layers (origin, source,
namespace LCO_NAMESPACE, sessionToken, and isValidBridgeSchema) are enforced.

In `@entrypoints/sidepanel/hooks/useDashboardData.ts`:
- Around line 69-117: The code is using orgIdRef.current instead of the freshly
read orgId from session storage which prevents reloads on first-login and can
query the wrong org after logout; update the logic in the block that reads
cKey/oKey so that you: treat the session-read orgId as the source of truth
(assign orgIdRef.current = orgId immediately when orgId is present and clear it
to '' immediately when orgId is absent), compute orgChanged by comparing prevOrg
to the freshly read orgId (not orgIdRef.current), use orgId (or early-return if
absent) when calling getConversation, and trigger loadConversations/loadToday
whenever prevOrg !== orgId to ensure any org transition reloads history and
today; keep existing calls to setActiveConv, setActiveHealth, setToday, and
setConversations but base their control flow on the session-read orgId and
convId.

---

Duplicate comments:
In `@entrypoints/claude-ai.content.ts`:
- Around line 151-200: The restore-on-org-detection logic in the
ORGANIZATION_DETECTED branch (the sequence that calls fetchStoredRecord, updates
cumulativeInput/cumulativeOutput/cumulativeCost, calls applyRestoredConversation
and buildConvStateFromRecord, computes health via computeHealthScore, sets
state/convState and calls overlay.render) must be extracted into a single helper
(e.g., restoreConversationForOrg or similar) and invoked from both the
ORGANIZATION_DETECTED path and the fallback that checks 'organizationId' in msg
(TOKEN_BATCH/STREAM_COMPLETE fallback) before proceeding; ensure the helper
accepts currentOrgId and currentConversationId and preserves the navGeneration
check, returns/updates state/convState/cumulative* values, and keep the
message-bridge thin by only calling that orchestrator helper from these branches
and not duplicating the logic.
🪄 Autofix (Beta)

Fix all unresolved CodeRabbit comments on this PR:

  • Push a commit to this branch (recommended)
  • Create a new PR with the fixes

ℹ️ Review info
⚙️ Run configuration

Configuration used: Organization UI

Review profile: CHILL

Plan: Pro

Run ID: 652534b8-1717-4b4c-a3c5-2bd6e9b3a977

📥 Commits

Reviewing files that changed from the base of the PR and between 4ab6942 and 8cfab29.

📒 Files selected for processing (7)
  • entrypoints/background.ts
  • entrypoints/claude-ai.content.ts
  • entrypoints/inject.ts
  • entrypoints/sidepanel/hooks/useDashboardData.ts
  • lib/bridge-validation.ts
  • lib/conversation-store.ts
  • lib/message-types.ts
✅ Files skipped from review due to trivial changes (1)
  • entrypoints/background.ts
🚧 Files skipped from review as they are similar to previous changes (3)
  • lib/bridge-validation.ts
  • entrypoints/inject.ts
  • lib/message-types.ts

Comment thread lib/conversation-store.ts
- conversation-store: key builders validate accountId and throw on empty string
  preventing silent writes to the empty-org bucket (conv::id, convIndex:, etc.)
- content: add event.source === window as layer 2 of the bridge message listener;
  the check was present on the LCO_TOKEN_REQ listener but missing from the main
  bridge, allowing same-origin iframes to pass the origin check
- content: extract restore logic into scheduleConversationRestore helper;
  now called from both ORGANIZATION_DETECTED and the TOKEN_BATCH/STREAM_COMPLETE
  fallback so state is restored regardless of which path sets the org ID first
- useDashboardData: treat session-read orgId as the source of truth; assign
  orgIdRef.current = orgId ?? '' immediately (not conditionally) and compute
  orgChanged as prevOrg !== (orgId ?? '') so first-login transitions (prevOrg ''
  to real org) correctly trigger loadConversations and loadToday; use orgId
  directly in getConversation call for type safety

@coderabbitai coderabbitai Bot left a comment

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

🧹 Nitpick comments (1)
entrypoints/sidepanel/hooks/useDashboardData.ts (1)

134-138: Comment is misleading: listConversations does not trigger bulk migration.

The comment states that loadConversations triggers bulk legacy migration, but listConversations explicitly avoids bulk migration (per lines 411-415 in conversation-store.ts). It reads legacy records directly without migrating them. Per-record migration only happens in getConversation when individual conversations are accessed.

The sequential await is still correct for initialization ordering, but the comment should reflect the actual behavior.

Suggested comment fix
-            // loadConversations first: it triggers bulk legacy migration if
-            // the account-scoped index is empty. loadToday depends on migrated
-            // conversation records to compute the daily summary correctly.
+            // loadConversations before loadToday: ensures the conversation list
+            // is populated before computing daily summary. Note: legacy records
+            // are read directly without migration; per-record migration happens
+            // in getConversation when individual conversations are accessed.
🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed.

In `@entrypoints/sidepanel/hooks/useDashboardData.ts` around lines 134 - 138,
Update the misleading comment above the sequential awaits: state that
loadConversations (which calls listConversations) does NOT perform a bulk legacy
migration but reads legacy records in place, and that per-record migration
occurs in getConversation when individual conversations are accessed; keep the
sequential awaits (await loadConversations(); await loadToday();) because
loadToday depends on conversation data being available, but remove the claim
that loadConversations triggers a bulk migration and reference the functions
loadConversations, listConversations, getConversation, and loadToday in the
updated comment.
🤖 Prompt for all review comments with AI agents
Verify each finding against the current code and only fix it if needed.

Nitpick comments:
In `@entrypoints/sidepanel/hooks/useDashboardData.ts`:
- Around line 134-138: Update the misleading comment above the sequential
awaits: state that loadConversations (which calls listConversations) does NOT
perform a bulk legacy migration but reads legacy records in place, and that
per-record migration occurs in getConversation when individual conversations are
accessed; keep the sequential awaits (await loadConversations(); await
loadToday();) because loadToday depends on conversation data being available,
but remove the claim that loadConversations triggers a bulk migration and
reference the functions loadConversations, listConversations, getConversation,
and loadToday in the updated comment.

ℹ️ Review info
⚙️ Run configuration

Configuration used: Organization UI

Review profile: CHILL

Plan: Pro

Run ID: 43af39f1-6895-4497-9dac-d571465f389c

📥 Commits

Reviewing files that changed from the base of the PR and between 8cfab29 and 51f7a47.

📒 Files selected for processing (3)
  • entrypoints/claude-ai.content.ts
  • entrypoints/sidepanel/hooks/useDashboardData.ts
  • lib/conversation-store.ts

@DevanshuNEU
DevanshuNEU merged commit 7e0b9f9 into OpenCodeIntel:main Apr 6, 2026
5 checks passed
DevanshuNEU added a commit to DevanshuNEU/lco-fork that referenced this pull request Apr 6, 2026
bridge-validation.test.ts:
- add ORGANIZATION_DETECTED section (4 tests): valid message, missing
  organizationId, empty string organizationId, non-string organizationId

conversation-store.test.ts:
- add legacy fallback via listConversations (3 tests): returns legacy records
  when scoped index empty, does not migrate to scoped index, both accounts
  can read the same legacy records independently
- add extractOrgId parity (4 tests): assert lib/conversation-store and
  inject.ts inline regex produce identical output for 4 URL patterns
- add empty accountId throws (2 tests): recordTurn and getConversation both
  reject empty string accountId with the assertAccountId error

set-active-conv.test.ts:
- update mirrored SetActiveConvMessage to include organizationId: string | null
- update mirrored handleSetActiveConv to mirror full background.ts logic:
  write activeOrg_{tabId} when organizationId present, remove stale key when
  null, remove both keys on clear
- update 8 existing tests to include organizationId: null and fix assertions
  for the two-key remove (was ['activeConv_42'], now ['activeConv_42', 'activeOrg_42'])
- add 3 new tests: writes activeOrg_ alongside activeConv_, removes stale
  activeOrg_ when organizationId null, removes both keys on null conversationId

tests/integration/account-isolation.test.ts (new file, 3 tests):
- listConversations and daily summaries are scoped per account end-to-end
- legacy records visible to both accounts without migration (no scoped index
  created, legacy convIndex untouched)
- same conversation ID across two accounts produces independent records
  with independent finalization state
DevanshuNEU added a commit that referenced this pull request Apr 6, 2026
…st-hardening

test(account-isolation): harden coverage for PR #29
DevanshuNEU added a commit that referenced this pull request May 4, 2026
…-36]

Rule 0 returns Healthy when turnCount <= 2 AND contextPct < 30,
before the per-model classifier runs. Blocks stale growthRate,
isDetailHeavy, and any future projection wrapper from escalating
a session that has no real history yet.

Acceptance criteria:
- Healthy on any conversation with turnCount <= 2 AND contextPct < 30
  regardless of prior tab/conversation state
- Wrappers like escalateForProjection still run on the returned
  HealthScore so a real draft can escalate after the guard
- AC #3 (overlay resets on new chat) already satisfied by existing
  SPA-nav reset path in claude-ai.content.ts (PR #29)

12 new tests: positive path (5), boundary (3), does-not-mask (4).
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.

1 participant