Skip to content

Add typed raw accessors with drift logging; migrate workspace tabs and resolver#94

Merged
wpak-ai merged 6 commits into
masterfrom
feat/typed-accessor-methods
Jun 5, 2026
Merged

Add typed raw accessors with drift logging; migrate workspace tabs and resolver#94
wpak-ai merged 6 commits into
masterfrom
feat/typed-accessor-methods

Conversation

@bradjin8
Copy link
Copy Markdown
Collaborator

@bradjin8 bradjin8 commented Jun 3, 2026

Summary

  • Introduce models/raw_access.py with shared optional-key readers and schema-drift WARNING logging when structurally important keys are missing or mistyped (projectLayouts, bubbleId in headers, etc.).
  • Add typed accessors on Composer and Bubble (newly_created_files, relevant_files, usage_data, bubble_timestamp_ms(), and related fields) so services can use model fields instead of silent .get() chains after from_dict validation.
  • Migrate services/workspace_tabs.py and services/workspace_resolver.py to pass Composer / Bubble through the hot path (bubble_map stores Bubble instances; determine_project_for_conversation accepts Composer | dict).
  • Add tests/test_raw_accessors.py for drift logging and bridge helpers.

Closes #90

Test plan

  • python -m pytest tests/test_raw_accessors.py -v
  • python -m pytest tests/test_workspace_assignment_fallback.py tests/test_models.py -v
  • python -m pytest tests/ -q (371 passed)
  • python -m mypy models services/workspace_tabs.py services/workspace_resolver.py
  • Manual: open workspace tabs in the UI for a workspace with real Cursor data; confirm tabs load and logs do not spam warnings for normal composers missing optional keys (newlyCreatedFiles, relevantFiles)

Notes

  • Commonly absent optional fields return empty defaults without a missing-key warning; wrong types still log drift.
  • Follow-up (out of scope here): migrate remaining .get() usage in workspace_listing.py, api/search.py, and scripts/export.py.

Summary by CodeRabbit

  • New Features

    • Conversations and bubbles now expose richer usage/metrics, file-change rollups, model info, and more reliable timestamps; workspace tabs and project inference assemble from typed conversation/bubble objects for clearer displays.
    • Text extraction accepts broader bubble inputs for more reliable content display.
  • Bug Fixes

    • Fewer crashes and incorrect displays when conversation JSON is malformed; safer defaults and schema-drift warnings replace errors.
  • Tests

    • Added tests covering typed raw accessors and schema-drift logging.

@bradjin8 bradjin8 self-assigned this Jun 3, 2026
@coderabbitai
Copy link
Copy Markdown

coderabbitai Bot commented Jun 3, 2026

Review Change Stack

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

Adds defensive, raw-backed properties to Composer and Bubble, a new models/raw_access module with optional-field readers and schema-drift warnings, refactors workspace resolver/tabs to use these typed accessors and model objects, updates text extraction to accept model objects, and adds tests for drift-logging behavior.

Changes

Typed Raw Accessor Layer

Layer / File(s) Summary
Composer and Bubble typed properties
models/conversation.py
Composer and Bubble dataclasses gain typed, raw-backed properties and helpers that validate shapes, log schema-drift warnings on mismatches, and return safe defaults.
Raw access helper module with generic and domain-specific extractors
models/raw_access.py
New module with generic optional readers (optional_raw_value, optional_raw_list, optional_raw_dict, optional_raw_str, optional_raw_number), missing-key warning helper, domain extractors (conversation_header_bubble_id, message_request_context_project_layouts), and polymorphic composer/bubble accessors.
Workspace resolver refactor to use typed accessors
services/workspace_resolver.py
Replaces direct nested JSON reads with raw_access helpers (project layouts, newlyCreatedFiles, codeBlockData, headers, bubble-derived file/URI candidates); determine_project_for_conversation accepts `Composer
Workspace tabs refactor to use typed Bubble and Composer
services/workspace_tabs.py
Stores Bubble instances in bubble_map, reads bubble/composer fields via model properties/methods (bubble_timestamp_ms(), model_info, usage_data, total_*, etc.), and extracts messageRequestContext layouts via message_request_context_project_layouts.
Schema-drift test coverage and text extraction update
tests/test_raw_accessors.py, utils/text_extract.py, tests/test_blob_parsing_fuzz.py
New tests exercise drift-warning logging and safe defaults; extract_text_from_bubble now accepts dicts or objects with a raw attribute and unwraps accordingly; small Hypothesis adjustment.

Sequence Diagram(s)

sequenceDiagram
  participant Client as service caller
  participant Tabs as services.workspace_tabs
  participant Resolver as services.workspace_resolver
  participant Models as models.raw_access / models.conversation

  Client->>Tabs: request tabs assembly (composer raw rows)
  Tabs->>Models: Composer.from_dict / Composer properties (usage_data, model_name_from_config)
  Tabs->>Models: conversation_header_bubble_id(header)
  Tabs->>Resolver: determine_project_for_conversation(composer)
  Resolver->>Models: composer_newly_created_files / composer_code_block_data / bubble helpers
  Models-->>Resolver: validated lists/dicts or safe defaults
  Resolver-->>Tabs: resolved project/workspace
  Tabs-->>Client: assembled tab payloads
Loading

Estimated code review effort

🎯 4 (Complex) | ⏱️ ~60 minutes

Possibly related PRs

Suggested reviewers

  • clean6378-max-it
  • timon0305
  • wpak-ai

Poem

🐰 I dug through raw dict fields at dawn,
Found keys that slipped and types withdrawn.
I logged a whisper when the schema swayed,
Returned safe defaults where values frayed.
Hops of joy — the rabbit’s patch is calm.

🚥 Pre-merge checks | ✅ 4 | ❌ 1

❌ Failed checks (1 warning)

Check name Status Explanation Resolution
Docstring Coverage ⚠️ Warning Docstring coverage is 28.57% which is insufficient. The required threshold is 80.00%. Write docstrings for the functions missing them to satisfy the coverage threshold.
✅ Passed checks (4 passed)
Check name Status Explanation
Description Check ✅ Passed Check skipped - CodeRabbit’s high-level summary is enabled.
Title check ✅ Passed The title clearly and concisely summarizes the main changes: adding typed raw accessors with drift logging and migrating workspace tabs/resolver to use them.
Linked Issues check ✅ Passed The PR successfully addresses all acceptance criteria from issue #90: implements typed accessors for top raw dict keys, adds schema-drift WARNING logging, includes type annotations for mypy verification, migrates workspace_tabs.py and workspace_resolver.py, and provides comprehensive test coverage.
Out of Scope Changes check ✅ Passed All changes are directly aligned with issue #90 objectives: models/raw_access.py provides the accessor infrastructure, models/conversation.py adds typed properties, service migrations use these accessors, tests validate drift-logging, and minor updates to text_extract.py and test_blob_parsing_fuzz.py support the core functionality.

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

✨ Finishing Touches
🧪 Generate unit tests (beta)
  • Create PR with unit tests
  • Commit unit tests in branch feat/typed-accessor-methods

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

Copy link
Copy Markdown

@coderabbitai coderabbitai Bot left a comment

Choose a reason for hiding this comment

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

Actionable comments posted: 2

🧹 Nitpick comments (3)
tests/test_raw_accessors.py (1)

26-30: ⚡ Quick win

Make the “missing key” setup explicit.

This test currently relies on GOOD_COMPOSER_RAW not containing newlyCreatedFiles. To keep the test stable and self-descriptive, remove the key explicitly in-test before calling Composer.from_dict(...).

Suggested patch
-        bare = Composer.from_dict(GOOD_COMPOSER_RAW, composer_id="cid-2")
+        raw = dict(GOOD_COMPOSER_RAW)
+        raw.pop("newlyCreatedFiles", None)
+        bare = Composer.from_dict(raw, composer_id="cid-2")
🤖 Prompt for AI Agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.

In `@tests/test_raw_accessors.py` around lines 26 - 30, The test
test_composer_newly_created_files_empty_when_key_missing should explicitly
remove the 'newlyCreatedFiles' key from the input before calling
Composer.from_dict to avoid relying on the global GOOD_COMPOSER_RAW shape;
create a shallow or deep copy of GOOD_COMPOSER_RAW, pop the 'newlyCreatedFiles'
key if present on that copy, then pass the modified dict into
Composer.from_dict(..., composer_id="cid-2") and keep the existing with
self.assertNoLogs and self.assertEqual(bare.newly_created_files, []). Ensure you
do not mutate the global GOOD_COMPOSER_RAW variable.
utils/text_extract.py (1)

24-27: ⚡ Quick win

Typing precision: bubble: dict | object is effectively just object for static type checking (since dict is a subtype of object), so it loses most type value; narrow it to the specific “has raw” shape (e.g., a Protocol) to keep mypy/pyright helpful.

Suggested patch
+from typing import Any, Protocol
+
+class HasRaw(Protocol):
+    raw: dict[str, Any]
+
-def extract_text_from_bubble(bubble: dict | object) -> str:
+def extract_text_from_bubble(bubble: dict[str, Any] | HasRaw) -> str:
     """Extract displayable text from a bubble object (text, richText, codeBlocks)."""
-    if hasattr(bubble, "raw"):
-        bubble = bubble.raw  # type: ignore[union-attr]
+    if hasattr(bubble, "raw"):
+        bubble = bubble.raw
🤖 Prompt for AI Agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.

In `@utils/text_extract.py` around lines 24 - 27, The parameter typing for
extract_text_from_bubble is too broad (dict | object) so static checkers lose
precision; define a small Protocol (e.g., HasRaw with attribute raw: dict) or a
TypedDict for the expected bubble shape and change the function signature to use
that Protocol/TypedDict (e.g., bubble: HasRaw | dict) so mypy/pyright can infer
attributes like raw and the contained fields used later in
extract_text_from_bubble; update any imports and type comments to keep
compatibility with Python versions in the project.
models/conversation.py (1)

3-9: 💤 Low value

Move _logger below the imports to avoid E402.

The _logger = logging.getLogger(__name__) assignment at Line 7 sits between from typing import Any and the from models.errors import SchemaError block, so the latter is a module-level import that no longer appears at the top of the file. Most linters (flake8/ruff E402) will flag this.

♻️ Proposed reordering
 import logging
 from dataclasses import dataclass, field
 from typing import Any
 
-_logger = logging.getLogger(__name__)
-
 from models.errors import SchemaError
 from models.from_dict_validation import (
     require_dict,
     require_key,
     require_non_empty_str,
     require_non_empty_str_field,
     require_type,
 )
+
+_logger = logging.getLogger(__name__)
🤖 Prompt for AI Agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.

In `@models/conversation.py` around lines 3 - 9, _the module-level logger
`_logger` is defined before the final import `from models.errors import
SchemaError`, causing an import-after-code lint error (E402); move the `_logger
= logging.getLogger(__name__)` line so it appears after all import statements
(including `from models.errors import SchemaError`) — locate `_logger` and the
import `SchemaError` in this file and reorder so all imports come first, then
initialize `_logger`.
🤖 Prompt for all review comments with AI agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.

Inline comments:
In `@models/raw_access.py`:
- Around line 158-190: composer_headers currently calls optional_raw_list which
emits a missing-key WARNING, while composer_newly_created_files silently returns
[] when the key is absent; align them to the silent-on-missing contract by
changing composer_headers to mirror composer_newly_created_files: if data is a
Composer return data.full_conversation_headers_only, otherwise read value =
data.get("fullConversationHeadersOnly") when data is a dict, return [] if value
is None, warn only if value exists but is not a list (similar warning message
pattern used in composer_newly_created_files), and avoid relying on
optional_raw_list to emit missing-key warnings.
- Around line 203-242: The three accessors bubble_relevant_files,
bubble_attached_file_uris, and bubble_context silently drop values of the wrong
type instead of logging drift; change their dict-path handling to call the
existing helpers optional_raw_list (for keys "relevantFiles" and
"attachedFileCodeChunksUris") and optional_raw_dict (for key "context") rather
than manually checking types, so type-mismatches are logged consistently (remove
the duplicated extraction logic and return the helper results when present).

---

Nitpick comments:
In `@models/conversation.py`:
- Around line 3-9: _the module-level logger `_logger` is defined before the
final import `from models.errors import SchemaError`, causing an
import-after-code lint error (E402); move the `_logger =
logging.getLogger(__name__)` line so it appears after all import statements
(including `from models.errors import SchemaError`) — locate `_logger` and the
import `SchemaError` in this file and reorder so all imports come first, then
initialize `_logger`.

In `@tests/test_raw_accessors.py`:
- Around line 26-30: The test
test_composer_newly_created_files_empty_when_key_missing should explicitly
remove the 'newlyCreatedFiles' key from the input before calling
Composer.from_dict to avoid relying on the global GOOD_COMPOSER_RAW shape;
create a shallow or deep copy of GOOD_COMPOSER_RAW, pop the 'newlyCreatedFiles'
key if present on that copy, then pass the modified dict into
Composer.from_dict(..., composer_id="cid-2") and keep the existing with
self.assertNoLogs and self.assertEqual(bare.newly_created_files, []). Ensure you
do not mutate the global GOOD_COMPOSER_RAW variable.

In `@utils/text_extract.py`:
- Around line 24-27: The parameter typing for extract_text_from_bubble is too
broad (dict | object) so static checkers lose precision; define a small Protocol
(e.g., HasRaw with attribute raw: dict) or a TypedDict for the expected bubble
shape and change the function signature to use that Protocol/TypedDict (e.g.,
bubble: HasRaw | dict) so mypy/pyright can infer attributes like raw and the
contained fields used later in extract_text_from_bubble; update any imports and
type comments to keep compatibility with Python versions in the project.
🪄 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: defaults

Review profile: CHILL

Plan: Pro

Run ID: 563cb83f-22bd-4ffb-a6d0-7d58c209538a

📥 Commits

Reviewing files that changed from the base of the PR and between 2839571 and 1e98ae3.

📒 Files selected for processing (6)
  • models/conversation.py
  • models/raw_access.py
  • services/workspace_resolver.py
  • services/workspace_tabs.py
  • tests/test_raw_accessors.py
  • utils/text_extract.py

Comment thread models/raw_access.py Outdated
Comment thread models/raw_access.py
@bradjin8 bradjin8 force-pushed the feat/typed-accessor-methods branch from 1e98ae3 to 19eb9da Compare June 4, 2026 00:00
Copy link
Copy Markdown

@coderabbitai coderabbitai Bot left a comment

Choose a reason for hiding this comment

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

Caution

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

⚠️ Outside diff range comments (2)
services/workspace_tabs.py (2)

136-143: ⚠️ Potential issue | 🟡 Minor | ⚡ Quick win

Don't treat an empty raw bubble payload as "missing".

bubble_map.get() returns None only when the id is absent. With the new Bubble | dict bridge, if not bubble_entry also drops {} payloads before Bubble.from_dict(...) runs, so context-only bubbles can disappear silently in the single-tab path.

Proposed fix
         bubble_id = conversation_header_bubble_id(header, composer_id=composer_id)
         if not bubble_id:
             continue
         bubble_entry = bubble_map.get(bubble_id)
-        if not bubble_entry:
+        if bubble_entry is None:
             continue
         if isinstance(bubble_entry, Bubble):
             bubble = bubble_entry
         else:
🤖 Prompt for AI Agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.

In `@services/workspace_tabs.py` around lines 136 - 143, The code is incorrectly
treating empty dict payloads as missing because it uses a falsy check; change
the presence check to a strict None test so empty mappings are preserved —
replace "if not bubble_entry" with "if bubble_entry is None" (or alternately
check "if bubble_id not in bubble_map") before the isinstance/Bubble.from_dict
branch to ensure {} payloads are passed into Bubble.from_dict rather than
skipped.

573-617: ⚠️ Potential issue | 🟠 Major | ⚡ Quick win

Validate summary rows with Composer.from_dict() too.

This path still bypasses the typed composer contract and reads cd.get(...) directly. A drifted row can therefore appear in list_workspace_tab_summaries() even though assemble_single_tab() and assemble_workspace_tabs() reject the same composer via Composer.from_dict(), which leaves the sidebar and detail endpoints disagreeing on what a valid conversation is.

Proposed fix
             if not isinstance(cd, dict):
                 parse_warnings.record_composer_skipped()
                 continue
             try:
+                composer = Composer.from_dict(cd, composer_id=composer_id)
                 if (
                     composer_id not in composer_id_to_ws
                     and composer_id not in project_layouts_map
                 ):
                     project_layouts_map[composer_id] = load_project_layouts_for_composer(
                         global_db, composer_id,
                     )
                 pid = determine_project_for_conversation(
-                    cd, composer_id, project_layouts_map,
+                    composer, composer_id, project_layouts_map,
                     project_name_map, workspace_path_map,
                     workspace_entries, {}, composer_id_to_ws, invalid_workspace_ids,
                 )
@@
-                headers = cd.get("fullConversationHeadersOnly") or []
+                headers = composer.full_conversation_headers_only
                 if not headers:
                     continue

-                title = cd.get("name") or f"Conversation {composer_id[:8]}"
+                title = composer.name or f"Conversation {composer_id[:8]}"

-                _early_model_config = cd.get("modelConfig") or {}
-                _early_model_name = _early_model_config.get("modelName")
+                _early_model_name = composer.model_name_from_config()
                 _early_model_names = [_early_model_name] if _early_model_name and _early_model_name != "default" else None
@@
                 tab_entry: dict = {
                     "id": composer_id,
                     "title": title,
-                    "timestamp": to_epoch_ms(cd.get("lastUpdatedAt")) or to_epoch_ms(cd.get("createdAt")) or int(datetime.now().timestamp() * 1000),
+                    "timestamp": to_epoch_ms(composer.last_updated_at) or to_epoch_ms(composer.created_at) or int(datetime.now().timestamp() * 1000),
                     "messageCount": len(headers),
                 }
+            except SchemaError as e:
+                _logger.warning(
+                    "Failed to parse Composer from composerData:%s: %s",
+                    composer_id,
+                    e,
+                )
+                parse_warnings.record_composer_skipped()
+                continue
🤖 Prompt for AI Agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.

In `@services/workspace_tabs.py` around lines 573 - 617, The summary-row path in
list_workspace_tab_summaries currently reads raw composer dicts (cd) directly
and can include drifted rows that Composer.from_dict() would reject; update the
logic in list_workspace_tab_summaries to attempt constructing a typed Composer
via Composer.from_dict(cd) (same validation used by
assemble_single_tab()/assemble_workspace_tabs()), and if Composer.from_dict
raises/returns invalid, skip that summary row (continue) so the sidebar and
detail endpoints remain consistent; reference the composer dict variable cd and
functions Composer.from_dict, assemble_single_tab, assemble_workspace_tabs when
locating where to insert this validation.
🤖 Prompt for all review comments with AI agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.

Outside diff comments:
In `@services/workspace_tabs.py`:
- Around line 136-143: The code is incorrectly treating empty dict payloads as
missing because it uses a falsy check; change the presence check to a strict
None test so empty mappings are preserved — replace "if not bubble_entry" with
"if bubble_entry is None" (or alternately check "if bubble_id not in
bubble_map") before the isinstance/Bubble.from_dict branch to ensure {} payloads
are passed into Bubble.from_dict rather than skipped.
- Around line 573-617: The summary-row path in list_workspace_tab_summaries
currently reads raw composer dicts (cd) directly and can include drifted rows
that Composer.from_dict() would reject; update the logic in
list_workspace_tab_summaries to attempt constructing a typed Composer via
Composer.from_dict(cd) (same validation used by
assemble_single_tab()/assemble_workspace_tabs()), and if Composer.from_dict
raises/returns invalid, skip that summary row (continue) so the sidebar and
detail endpoints remain consistent; reference the composer dict variable cd and
functions Composer.from_dict, assemble_single_tab, assemble_workspace_tabs when
locating where to insert this validation.

ℹ️ Review info
⚙️ Run configuration

Configuration used: defaults

Review profile: CHILL

Plan: Pro

Run ID: d9b0fc91-65e8-46d2-955e-ee7f60388a4f

📥 Commits

Reviewing files that changed from the base of the PR and between 19eb9da and 8d45e95.

📒 Files selected for processing (2)
  • services/workspace_tabs.py
  • tests/test_blob_parsing_fuzz.py

@bradjin8 bradjin8 requested a review from clean6378-max-it June 4, 2026 13:49
@clean6378-max-it
Copy link
Copy Markdown
Collaborator

Must fix

models/raw_access.py:150-155, services/workspace_tabs.py:887, services/workspace_resolver.py:132 — stop WARNING-logging when projectLayouts is absent on messageRequestContext rows scanned in bulk (e.g. warn_if_missing=False on optional_raw_list, or a probe-specific helper that only warns on wrong type)

(most context blobs omit projectLayouts; current code emits one WARNING per row on every tab load and during infer_workspace_name_from_context, drowning real drift signals)

Should fix

models/conversation.py:73-85, models/raw_access.py:28-39, Doc/Issues/cppa-cursor-browser issue 90.md:9 — align missing-key policy with issue #90: either document “absent-by-design” fields (no warn) vs “drift-sensitive” fields (warn), or add missing-key warnings on the Composer/Bubble properties tests currently forbid

(issue AC says warn on missing; properties and tests treat absent newlyCreatedFiles/relevantFiles as silent empty lists while optional_raw_* warns on missing keys)

services/workspace_resolver.py:245-258 — narrow bubble_map to Mapping[str, Bubble | dict[str, Any]] (or Bubble only after full migration) in determine_project_for_conversation

(issue #90 asks for mypy-verifiable flow from model to service; untyped dict hides misuse now that values are Bubble instances)

models/raw_access.py:158-178, models/raw_access.py:181-196 — use the same missing-key/type policy in composer_headers / composer_newly_created_files dict branches as on Composer properties (dict path for fullConversationHeadersOnly returns [] with no drift log)

(inconsistent drift detection between Composer instances and dict bridges in the resolver)

Nice to have

models/raw_access.py:103-126 — remove or wire up optional_raw_number (currently unused dead code)

models/raw_access.py:181-253, models/conversation.py:73-246 — deduplicate property logic and composer_* / bubble_* bridge helpers into one implementation called by both

(eval monolith-duplication theme; reduces fix-once-apply-twice risk on schema changes)

services/workspace_tabs.py (remaining ~59 .get() calls, especially ctx.get(...) in _assemble_tab_from_composer_data) — follow-up accessors for messageRequestContext fields after the top-10 composer/bubble keys

(issue #90 primary target only partly migrated; acceptable as follow-up if PR scope is intentionally bounded)

@clean6378-max-it
Copy link
Copy Markdown
Collaborator

@CodeRabbit, Do you think my comments review are valid?
Can you do the deep research in your side again?

@coderabbitai
Copy link
Copy Markdown

coderabbitai Bot commented Jun 4, 2026

Tip

For best results, initiate chat on the files or code changes.

🧩 Analysis chain

🏁 Script executed:

#!/bin/bash
echo "=== models/raw_access.py ==="
cat models/raw_access.py

echo ""
echo "=== models/conversation.py (relevant sections) ==="
sed -n '1,250p' models/conversation.py

echo ""
echo "=== services/workspace_resolver.py ==="
cat services/workspace_resolver.py

echo ""
echo "=== services/workspace_tabs.py (lines 860-920) ==="
sed -n '860,920p' services/workspace_tabs.py

echo ""
echo "=== Count .get() calls in workspace_tabs.py ==="
grep -n '\.get(' services/workspace_tabs.py | wc -l
echo "--- Lines with .get( in workspace_tabs.py ---"
grep -n '\.get(' services/workspace_tabs.py

echo ""
echo "=== tests/test_raw_accessors.py ==="
cat tests/test_raw_accessors.py

echo ""
echo "=== optional_raw_number usages across codebase ==="
rg -n 'optional_raw_number' --type py

echo ""
echo "=== bubble_map type annotations ==="
rg -n 'bubble_map' services/workspace_resolver.py services/workspace_tabs.py

echo ""
echo "=== determine_project_for_conversation signature ==="
rg -n 'determine_project_for_conversation' services/workspace_resolver.py -A 5

echo ""
echo "=== warn_missing_raw_key / optional_raw_list call sites for projectLayouts ==="
rg -n 'projectLayouts\|message_request_context_project_layouts' --type py

echo ""
echo "=== workspace_tabs.py full file line count ==="
wc -l services/workspace_tabs.py

echo ""
echo "=== workspace_tabs.py lines 1-100 ==="
sed -n '1,100p' services/workspace_tabs.py

Length of output: 50749



@clean6378-max-it Yes, all your review comments are well-founded. Here's the detailed verification from the actual code:


Must Fix

✅ WARNING flood on absent projectLayouts (raw_access.py:150-155, workspace_tabs.py:887, workspace_resolver.py:132)

Confirmed. message_request_context_project_layouts unconditionally calls optional_raw_listoptional_raw_valuewarn_missing_raw_key whenever "projectLayouts" is absent. The test test_project_layouts_warns_when_key_missing even asserts this fires on {}. Since this is called in two bulk loops:

  • workspace_tabs.py line ~887: iterating all messageRequestContext:* KV rows in one pass
  • infer_workspace_name_from_context: nested loop over composer_ids × rows

…the result is one WARNING per row whenever projectLayouts is absent (which is the common case). Real drift signals will drown in noise. A warn_if_missing=False flag or a probe-specific helper that only warns on wrong-type is the right fix.


Should Fix

✅ Missing-key policy mismatch with issue #90 (conversation.py:73-85, raw_access.py:28-39)

Confirmed. There are two contradictory contracts in the same PR:

Code path Absent key behaviour
Composer.newly_created_files silent return []
Bubble.relevant_files silent return []
optional_raw_list warn_missing_raw_key WARNING
message_request_context_project_layouts WARNING (via optional_raw_list)

The tests explicitly lock in the silent-empty-list behaviour via assertNoLogs("models.conversation", level="WARNING"). This directly contradicts the issue #90 acceptance criterion ("warn on missing"). Either the AC needs to be updated (documenting which fields are "absent-by-design" vs "drift-sensitive"), or the properties need to match the optional_raw_* semantics.

bubble_map: dict untyped in determine_project_for_conversation (workspace_resolver.py:245-258)

Confirmed. The parameter is annotated as bare dict in the resolver:

# workspace_resolver.py line 245
bubble_map: dict,

But callers pass properly typed values:

# workspace_tabs.py line 811
bubble_map: dict[str, Bubble] = {}
# workspace_tabs.py line 102
bubble_map: Mapping[str, Bubble | dict[str, Any]],

The docstring still says {bubble_id: bubble_dict}, which is stale. Narrowing to Mapping[str, Bubble | dict[str, Any]] (or Mapping[str, Bubble] post-full-migration) would give mypy end-to-end coverage as #90 requires.

✅ Inconsistent drift detection in dict branches of composer_headers / composer_newly_created_files (raw_access.py:158-196)

Confirmed. The dict-path branches call .get() silently:

# composer_newly_created_files dict branch — no drift log on absent key
value = data.get("newlyCreatedFiles") if isinstance(data, dict) else None
if value is None:
    return []  # silent

This is actually consistent with the Composer property (also silent when absent), but it means the bridge functions have a different contract from optional_raw_list / message_request_context_project_layouts. If the policy decision is "absent optional fields are silent", that needs to be documented and optional_raw_list (and the projectLayouts helper) must be updated. If the policy is "warn on missing", the dict bridges and properties need updating instead.


Nice to Have

optional_raw_number is dead code (raw_access.py:103-126)

Confirmed. rg -n 'optional_raw_number' returns only the definition — no call sites anywhere. Composer uses its own _optional_counter helper instead. optional_raw_number can be removed or wired into _optional_counter.

✅ Property logic vs bridge-function duplication (raw_access.py:181-253, conversation.py:73-246)

Confirmed. Composer.newly_created_files and composer_newly_created_files's dict branch contain identical null-check → isinstance-check → warning → return logic. Same pattern repeated across all Bubble properties and bubble_* bridge functions. A single shared utility (the existing optional_raw_* helpers, if used consistently) would eliminate this.

✅ ~59 remaining .get() calls in workspace_tabs.py

Confirmed exactly. grep -c '\.get(' services/workspace_tabs.py = 59. Most are in _assemble_tab_from_composer_data (e.g., ctx.get("bubbleId"), ctx.get("gitStatusRaw"), m.get("inputTokens"), etc.) that are the primary #90 target. Acceptable as a follow-up if the PR scope is intentionally bounded to the top-10 composer/bubble keys.


Summary: All your "Must fix" and "Should fix" points are valid and backed by the code. The "Nice to have" items are also accurate. The most urgent one is the WARNING flood — it will make the logs unreadable in any real install with even a moderate number of conversations.

🐇 (\_/)
(•ᴥ•) Good catch on all counts.

@clean6378-max-it
Copy link
Copy Markdown
Collaborator

I was asking you to find others yourself as well, @CodeRabbit

@coderabbitai
Copy link
Copy Markdown

coderabbitai Bot commented Jun 4, 2026

Tip

For best results, initiate chat on the files or code changes.

🧩 Analysis chain

🏁 Script executed:

#!/bin/bash
echo "=== utils/text_extract.py ==="
cat utils/text_extract.py

echo ""
echo "=== tests/test_blob_parsing_fuzz.py ==="
cat tests/test_blob_parsing_fuzz.py

echo ""
echo "=== models/conversation.py full ==="
cat models/conversation.py

echo ""
echo "=== models/raw_access.py full ==="
cat models/raw_access.py

echo ""
echo "=== services/workspace_tabs.py full ==="
cat services/workspace_tabs.py

echo ""
echo "=== services/workspace_resolver.py full ==="
cat services/workspace_resolver.py

echo ""
echo "=== tests/test_raw_accessors.py full ==="
cat tests/test_raw_accessors.py

echo ""
echo "=== Check for circular imports ==="
rg -n 'from models' --type py
echo "=== raw_access imports ==="
head -20 models/raw_access.py

echo ""
echo "=== bubble_timestamp_ms - any callers? ==="
rg -n 'bubble_timestamp_ms' --type py

echo ""
echo "=== HasBubbleRaw usage ==="
rg -n 'HasBubbleRaw' --type py

echo ""
echo "=== extract_text_from_bubble callers ==="
rg -n 'extract_text_from_bubble' --type py -A 2

echo ""
echo "=== Check conversation_header_bubble_id callers vs ctx.get bubbleId ==="
rg -n 'bubbleId\|bubble_id\|conversation_header_bubble_id' --type py

echo ""
echo "=== Check for assertNoLogs availability (Python 3.10+) ==="
python3 --version

echo ""
echo "=== Check composer_headers callers ==="
rg -n 'composer_headers\b' --type py

echo ""
echo "=== Check full_conversation_headers_only property ==="
rg -n 'full_conversation_headers_only' --type py -B 2 -A 5

echo ""
echo "=== Check last_updated_at / created_at on Composer ==="
rg -n 'last_updated_at\|created_at' models/conversation.py -A 2

echo ""
echo "=== Check _optional_counter definition ==="
rg -n '_optional_counter' models/conversation.py -A 8

echo ""
echo "=== Check thread safety / caching ==="
rg -n 'lru_cache\|cache\|lock\|thread' --type py

echo ""
echo "=== Check test coverage for bubble_timestamp_ms ==="
rg -n 'timestamp\|bubble_timestamp' tests/ --type py

echo ""
echo "=== Check Bubble.from_dict usage ==="
rg -n 'Bubble\.from_dict\|Composer\.from_dict' --type py -B 1 -A 3

echo ""
echo "=== Check model_name_from_config usage ==="
rg -n 'model_name_from_config' --type py -A 3

echo ""
echo "=== Check total cost fallback in workspace_tabs ==="
rg -n 'total_cost\|totalCost\|cost' services/workspace_tabs.py -A 2

echo ""
echo "=== Check workspace_resolver bubble_map annotation ==="
sed -n '240,270p' services/workspace_resolver.py

echo ""
echo "=== Check if optional_raw_number has bool guard (documented as treating bool as invalid) ==="
grep -n 'bool\|isinstance' models/raw_access.py

echo ""
echo "=== Check test_blob_parsing_fuzz type:ignore annotation ==="
grep -n 'type: ignore\|blacklist_categories' tests/test_blob_parsing_fuzz.py

echo ""
echo "=== Check missing __all__ / exports ==="
grep -n '__all__' models/raw_access.py models/conversation.py

echo ""
echo "=== Check whether composer_code_block_data dict branch returns None vs {} inconsistency ==="
grep -n 'code_block_data\|codeBlockData' models/raw_access.py models/conversation.py

echo ""
echo "=== Look for any bare except / broad exception handling ==="
rg -n 'except\s*:' --type py

echo ""
echo "=== Check workspace_tabs bubble_map population ==="
grep -n 'bubble_map' services/workspace_tabs.py -A 2

echo ""
echo "=== Check for Bubble instances where dict expected - extract_text_from_bubble call sites ==="
grep -n 'extract_text_from_bubble' services/workspace_tabs.py -B 2 -A 2

echo ""
echo "=== Check if Bubble has .raw attribute (required by HasBubbleRaw protocol) ==="
grep -n 'self\.raw\|raw:' models/conversation.py | head -30

Length of output: 126842



@clean6378-max-it — apologies for not doing this proactively. Here are the additional issues I found independently after a full read of all changed files:


Additional Must Fix

🔴 conversation_header_bubble_id WARNING flood — same class as projectLayouts (unreported)

services/workspace_tabs.py line 133 and services/workspace_resolver.py line 303 both iterate every header calling conversation_header_bubble_id, which calls optional_raw_strwarn_missing_raw_key. The test test_conversation_header_bubble_id_warns_when_missing explicitly asserts this fires. A conversation with 20 messages produces 20 WARNINGs per tab load if any headers lack bubbleId. This is the same flooding pattern as projectLayouts.


🔴 Falsy-zero timestamp bug — workspace_tabs.py lines 288 and 416

# line 288
"timestamp": to_epoch_ms(bubble.bubble_timestamp_ms()) or int(datetime.now().timestamp() * 1000)

# line 416
"timestamp": to_epoch_ms(composer.last_updated_at) or to_epoch_ms(composer.created_at) or int(datetime.now().timestamp() * 1000)

If bubble_timestamp_ms() returns 0 (or to_epoch_ms coerces any input to 0), the or falls through to datetime.now(), making every such bubble's timestamp non-deterministic and sort-unstable. This breaks idempotency. Use is not None guards:

ts = to_epoch_ms(bubble.bubble_timestamp_ms())
"timestamp": ts if ts is not None else int(datetime.now().timestamp() * 1000)

🔴 bubble_timestamp_ms() has zero test coverage for its edge cases

No tests anywhere cover:

  • createdAt = True → excluded by bool guard → should return None
  • createdAt = 0 → returns 0, but 0 or fallback triggers the bug above
  • both createdAt and timestamp present → should prefer createdAt
  • neither key present → None

Additional Should Fix

🟡 extract_text_from_bubble variable shadowing — utils/text_extract.py lines 36-38

def extract_text_from_bubble(bubble: HasBubbleRaw | dict[str, Any]) -> str:
    raw: dict[str, Any] = bubble if isinstance(bubble, dict) else bubble.raw
    if not raw:
        return ""
    bubble = raw   # ← rebinds the parameter; mypy loses the original type

The parameter bubble is silently rebound to a dict. mypy cannot verify the HasBubbleRaw branch after this point, and # type: ignore suppressions downstream become necessary. Use a distinct name (payload = raw) and operate on payload throughout.

🟡 api/search.py line 162 — incomplete migration of extract_text_from_bubble call site

text = extract_text_from_bubble(bubble.raw)  # passes dict, not Bubble

HasBubbleRaw was introduced precisely so callers can pass Bubble directly. This call site was not updated, so the protocol is unused in practice from that file.

🟡 bubble.tool_results property called twice per AI bubble — workspace_tabs.py lines 228-230

"toolResultsCount": ... or (len(bubble.tool_results) if bubble.tool_results else None),
"toolResults":  bubble.tool_results if bubble.tool_results else None,

Each property access re-executes the isinstance guard and the dict lookup. Assign to a local variable once.

🟡 token_count, thinking, thinking_duration_ms have no type validation or drift warning — models/conversation.py lines 250, 282, 286

These three properties return bare self.raw.get(...) with no isinstance guard and no _logger.warning. In workspace_tabs.py, token_count is immediately checked with isinstance(token_count, dict) — if it drifts to a non-dict, no drift signal is emitted. Inconsistent with every other new property.

🟡 messageCount in summary path counts raw headers, not renderable bubbles — workspace_tabs.py line 617

"messageCount": len(headers),

_assemble_tab_from_composer_data silently skips headers with no matching bubble in bubble_map, bubbles failing Bubble.from_dict, and bubbles with no renderable content. The summary UI can show "12 messages" for a tab the full view renders as 8. Either document this as approximate or compute from the same filter.

🟡 optional_raw_value passes bool as a number when expected_type=(int, float)models/raw_access.py lines 39-47

bool is a subclass of int, so isinstance(True, (int, float)) is True. optional_raw_number has an explicit isinstance(value, bool) pre-check, but optional_raw_value called directly with a numeric expected_type would silently return True/False as valid numbers. Any future caller using optional_raw_value for a numeric field will get different behaviour than optional_raw_number.

🟡 Double header iteration in fallback resolution — services/workspace_resolver.py lines ~303-345

The fallback block iterates headers twice: once probing bubble paths (returns on first match), then again building path_segments for the last-resort segment heuristic. Both loops call conversation_header_bubble_id, bubble_map.get, bubble_relevant_files, bubble_attached_file_uris, and bubble_context. Collect candidates in one pass and process them in two phases.

🟡 infer_invalid_workspace_aliases passes raw dicts to determine_project_for_conversationservices/workspace_resolver.py lines 385-400

cd = json.loads(row["value"])
...
inferred = determine_project_for_conversation(cd, cid, ...)  # dict, not Composer

Despite the updated signature accepting Composer | dict, this call site skips Composer.from_dict entirely. The dict bridge path gives inconsistent drift detection and defeats the issue #90 goal of typed flow through the resolver.


Additional Nice to Have

bubble_timestamp_ms() and model_name_from_config() are methods, not @property

Every other new accessor on Composer and Bubble is a @property (accessed as bubble.relevant_files). These two require call-site parentheses (bubble.bubble_timestamp_ms(), composer.model_name_from_config()). No strong reason to deviate; making them properties would close the API surface consistently.

Bubble.text property is a footgun

The property docstring warns that richText is handled by extract_text_from_bubble, but the name .text looks like "full message text". cursor_md_exporter.py and scripts/export.py still use b.get("text") on raw dicts; if they migrate to Bubble objects and reach for bubble.text, they silently drop richText and codeBlocks. Consider naming it bubble.plain_text or adding a prominent # NOT the full rendered text comment.


Summary table of newly found items:

Severity Location Issue
Must workspace_tabs.py:133, workspace_resolver.py:303 bubbleId WARNING flood per header
Must workspace_tabs.py:288,416 Falsy-zero timestamp or-fallback bug
Must tests/ Zero coverage for bubble_timestamp_ms() edge cases
Should utils/text_extract.py:38 bubble = raw variable shadowing breaks mypy
Should api/search.py:162 HasBubbleRaw protocol unused at migration target
Should workspace_tabs.py:228-230 bubble.tool_results called twice
Should models/conversation.py:250,282,286 token_count/thinking/thinking_duration_ms lack type validation
Should workspace_tabs.py:617 messageCount counts raw headers, not renderable bubbles
Should models/raw_access.py:41 optional_raw_value passes bool as number
Should workspace_resolver.py:303-345 Headers iterated twice in fallback
Should workspace_resolver.py:385-400 infer_invalid_workspace_aliases bypasses Composer.from_dict
NTH models/conversation.py:316,147 bubble_timestamp_ms/model_name_from_config inconsistently not @property
NTH models/conversation.py:188 Bubble.text naming footgun

🐇 (_/)
(•ᴥ•) Found a fair bit more than I initially surfaced — thanks for the push.

@clean6378-max-it
Copy link
Copy Markdown
Collaborator

bubble_map typing in determine_project_for_conversation issue is existed yet.
infer_invalid_workspace_aliases(..., bubble_map: dict, ...) at services/workspace_resolver.py:379

@clean6378-max-it
Copy link
Copy Markdown
Collaborator

bubble_timestamp_ms() test coverage

test_bubble_timestamp_ms_prefers_created_at_and_handles_zero covers: createdAt=0, prefer createdAt over timestamp, neither key → None, createdAt=True → None (no log). Gap: no test that _bubble_entry_timestamp_ms keeps epoch 0 (does not fall back to now()).

@clean6378-max-it
Copy link
Copy Markdown
Collaborator

token_count / thinking / thinking_duration_ms drift

token_count and thinking_duration_ms have type guards + warnings. thinking still returns raw .get("thinking") with no wrong-type warning (only "thinking" not in self.raw → None).

@clean6378-max-it
Copy link
Copy Markdown
Collaborator

@CodeRabbit. I found 3 should fixs are not addressed fully.
Do you agree with me?
Do you think rest are addressed now?

@coderabbitai
Copy link
Copy Markdown

coderabbitai Bot commented Jun 4, 2026

Tip

For best results, initiate chat on the files or code changes.

🧩 Analysis chain

🏁 Script executed:

#!/bin/bash
echo "=== infer_invalid_workspace_aliases signature and bubble_map usage ==="
rg -n 'infer_invalid_workspace_aliases\|bubble_map' services/workspace_resolver.py -A 3 -B 1

echo ""
echo "=== bubble_timestamp_ms tests ==="
rg -n 'bubble_timestamp_ms\|_bubble_entry_timestamp\|epoch\|keeps.*0\|zero.*fallback' tests/ --type py -A 5 -B 2

echo ""
echo "=== thinking property in models/conversation.py ==="
rg -n 'def thinking\|thinking_duration\|token_count' models/conversation.py -A 10

echo ""
echo "=== workspace_resolver.py lines 370-410 ==="
sed -n '370,415p' services/workspace_resolver.py

echo ""
echo "=== Check current state of bubble_map annotation at determine_project_for_conversation ==="
sed -n '240,260p' services/workspace_resolver.py

echo ""
echo "=== Check all bubble_map: dict occurrences ==="
rg -n 'bubble_map\s*:' --type py

echo ""
echo "=== falsy-zero timestamp or-fallback - current state ==="
rg -n 'bubble_timestamp_ms\(\)\|to_epoch_ms.*or\b' services/workspace_tabs.py -A 1 -B 1

echo ""
echo "=== tool_results double call ==="
sed -n '225,235p' services/workspace_tabs.py

echo ""
echo "=== optional_raw_value bool-as-number check ==="
sed -n '35,55p' models/raw_access.py

echo ""
echo "=== double-header iteration in fallback ==="
sed -n '295,360p' services/workspace_resolver.py

echo ""
echo "=== api/search.py extract_text_from_bubble call ==="
rg -n 'extract_text_from_bubble' --type py -A 2 -B 2

echo ""
echo "=== extract_text_from_bubble variable shadowing ==="
sed -n '30,55p' utils/text_extract.py

echo ""
echo "=== optional_raw_number call sites ==="
rg -n 'optional_raw_number' --type py

echo ""
echo "=== messageCount in summaries ==="
rg -n 'messageCount' services/workspace_tabs.py -A 2 -B 2

echo ""
echo "=== bubble_timestamp_ms and model_name_from_config - `@property` or method ==="
rg -n 'def bubble_timestamp_ms\|def model_name_from_config' models/conversation.py -B 2

echo ""
echo "=== Bubble.text property ==="
rg -n 'def text\b\|plain_text' models/conversation.py -A 5

echo ""
echo "=== bubbleId WARNING flood - conversation_header_bubble_id calls in bulk loops ==="
sed -n '125,145p' services/workspace_tabs.py
sed -n '295,315p' services/workspace_resolver.py

Length of output: 20890



@clean6378-max-it — yes, all three of your corrections are accurate. Here's the precise status of each:


Your 3 Corrections — All Confirmed

1. bubble_map typing — not fully addressed ✅ you're right

determine_project_for_conversation (line 247) was updated to Mapping[str, Bubble | dict[str, Any]], but infer_invalid_workspace_aliases (line 379) still has the bare bubble_map: dict. Since infer_invalid_workspace_aliases calls determine_project_for_conversation and forwards its bubble_map, the typed guarantee breaks at that boundary — same mypy gap.

2. bubble_timestamp_ms() test coverage — not fully addressed ✅ you're right

My "zero coverage" claim was wrong. The real remaining gap is specifically that there is no test verifying _bubble_entry_timestamp_ms does not fall back to now() when the timestamp is epoch 0 — i.e., that 0 is preserved and the or-fallback is not triggered. That's a narrower but real gap.

3. thinking wrong-type warning — not fully addressed ✅ you're right

I overstated the issue. token_count and thinking_duration_ms do have isinstance guards + drift warnings. Only thinking still returns self.raw.get("thinking") with no guard for wrong types — a bool or int stored under that key would pass through silently.


Status of the Rest

Item Status
WARNING flood on projectLayouts/bubbleId warn_if_missing: bool = True param added to optional_raw_value — mechanism exists; depends on call sites passing False
Falsy-zero or-fallback bug (workspace_tabs.py:288,416) rg found no matches for that pattern — likely fixed
extract_text_from_bubble variable shadowing ✅ Fixed — uses payload variable now
api/search.py:162 HasBubbleRaw unused ✅ Resolved — Bubble satisfies the protocol, call site now correct
optional_raw_value passes bool as number ✅ Fixed — explicit isinstance(value, bool) pre-check added
Double-header iteration in fallback ✅ Fixed — now single-pass with comment
messageCount counts raw headers ✅ Documented via inline comment; not algorithmically changed (acceptable)
infer_invalid_workspace_aliases bypasses Composer.from_dict ❌ Still passes raw json.loads dict; bubble_map: dict annotation also unfixed (same as #1)
optional_raw_number dead code ❌ Still no call sites
bubble.tool_results called twice Could not confirm from output — worth a targeted check
Missing-key policy docs ("absent-by-design" vs "drift-sensitive") ❌ Still undocumented

So the remaining open items are: infer_invalid_workspace_aliases (both the bubble_map annotation and the missing Composer.from_dict call), optional_raw_number dead code, thinking wrong-type warning, epoch-0 test gap, and the missing-key policy documentation from the original "should fix" list.

🐇 (\_/)
(•ᴥ•) Good catch on all three — my initial analysis had real inaccuracies.

@bradjin8
Copy link
Copy Markdown
Collaborator Author

bradjin8 commented Jun 5, 2026

@coderabbitai, Check again if the three findings are fixed or not.

@coderabbitai
Copy link
Copy Markdown

coderabbitai Bot commented Jun 5, 2026

Tip

For best results, initiate chat on the files or code changes.

🧩 Analysis chain

🏁 Script executed:

#!/bin/bash
echo "=== 1. infer_invalid_workspace_aliases bubble_map annotation ==="
rg -n 'infer_invalid_workspace_aliases\|bubble_map\s*:' services/workspace_resolver.py -A 3 -B 1

echo ""
echo "=== determine_project_for_conversation signature ==="
rg -n 'def determine_project_for_conversation' services/workspace_resolver.py -A 8

echo ""
echo "=== 2. bubble_timestamp_ms / _bubble_entry_timestamp tests ==="
rg -n 'bubble_timestamp_ms\|_bubble_entry_timestamp\|epoch.*0\|zero.*epoch\|keeps.*0\|timestamp.*0' tests/ --type py -A 6 -B 2

echo ""
echo "=== All tests related to timestamp ==="
cat tests/test_raw_accessors.py

echo ""
echo "=== 3. thinking property current state ==="
rg -n 'def thinking\b' models/conversation.py -A 15

echo ""
echo "=== token_count and thinking_duration_ms for comparison ==="
rg -n 'def token_count\|def thinking_duration_ms' models/conversation.py -A 15

echo ""
echo "=== infer_invalid_workspace_aliases full function ==="
sed -n '370,420p' services/workspace_resolver.py

echo ""
echo "=== Check for any test files mentioning epoch 0 or timestamp zero ==="
rg -n 'epoch\|timestamp.*0\|0.*timestamp\|createdAt.*0\|0.*createdAt' tests/ --type py -A 3 -B 1

Length of output: 11896



@bradjin8 — all three findings are now fixed on this branch. Here's the precise verification:


1. bubble_map typing in infer_invalid_workspace_aliasesFixed

infer_invalid_workspace_aliases (line ~379) now carries the same typed annotation as determine_project_for_conversation:

bubble_map: Mapping[str, Bubble | dict[str, Any]],

Both call-site and callee are consistent; the mypy gap is closed.


2. bubble_timestamp_ms() epoch-0 test coverage ✅ Fixed

Two new tests close the gaps:

  • test_bubble_entry_timestamp_ms_preserves_epoch_zero — patches services.workspace_tabs.datetime, calls _bubble_entry_timestamp_ms(bubble) with createdAt=0, asserts ts == 0 and ts != sentinel_now_ms. This is the exact regression guard that was missing.
  • test_bubble_entry_timestamp_ms_falls_back_when_no_timestamp — verifies the now() fallback still fires when no timestamp key exists.

3. thinking wrong-type warning ✅ Fixed

The property now has a proper isinstance guard and drift warning:

`@property`
def thinking(self) -> str | dict[str, Any] | None:
    value = self.raw.get("thinking")
    if value is None:
        return None
    if isinstance(value, (str, dict)):
        return value
    _logger.warning(
        "Schema drift in Bubble %s: invalid type for thinking (expected str or dict, got %s)",
        self.bubble_id,
        type(value).__name__,
    )
    return None

Three tests cover it: test_bubble_thinking_silent_when_missing, test_bubble_thinking_warns_on_wrong_type (asserts WARNING fires for thinking: 42), and test_bubble_thinking_accepts_str_or_dict.

@bradjin8 bradjin8 force-pushed the feat/typed-accessor-methods branch from 709f838 to 01ea37c Compare June 5, 2026 15:18
@bradjin8 bradjin8 requested a review from wpak-ai June 5, 2026 15:22
@wpak-ai wpak-ai merged commit 937dc91 into master Jun 5, 2026
16 checks passed
@wpak-ai wpak-ai deleted the feat/typed-accessor-methods branch June 5, 2026 15:43
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.

Typed accessor methods for raw dict keys

3 participants