Skip to content

feat(adapters/cursor): populate models / model_dominant from the store's model label (closes #115)#118

Merged
aktasbatuhan merged 3 commits into
firstbatchxyz:mainfrom
goktugecertastan-cyber:feat/cursor-adapter
Jun 5, 2026
Merged

feat(adapters/cursor): populate models / model_dominant from the store's model label (closes #115)#118
aktasbatuhan merged 3 commits into
firstbatchxyz:mainfrom
goktugecertastan-cyber:feat/cursor-adapter

Conversation

@goktugecertastan-cyber

Copy link
Copy Markdown
Contributor

Closes #115.

Verified the model-name question in #115 against a real Cursor install (globalStorage/state.vscdb, table cursorDiskKV, 15 composers), then wired the signal into the adapter. Scope stays exactly where #115 asked: model NAME only — token/cost stay 0, the #113 limitations are unchanged.

Verification findings (the acceptance criteria)

Where the model name lives — confirmed:

  • composerData:<id>modelConfig.modelName is present on every composer (it's the picker's selection). This is the authoritative/primary source.
  • bubbleId:<composer>:<bubble>modelInfo.modelName exists too — the field CodeBurn reads — but on type:1 (user) bubbles, not assistant bubbles. (Worth flagging: the issue/web-research framing implied per-assistant-message; it's actually stamped on the user turn.) We use it as a fallback when the composerData row is gone (orphan-bubble recovery).

Auto mode — confirmed: the store records the literal string "default" (not blank, not "auto"). 13/15 composers were "default"; the 2 with real messages were "composer-2.5". We normalize "default""auto" so the corpus carries an explicit, readable Auto signal.

Tokens/cost — confirmed absent: tokenCount is 0 as #113 documented, so model_dominant is computed by frequency (composer pick = 1 vote, each modelInfo bubble = +1), not token-weighted like pi.py. No token/cost is introduced.

End-to-end against the real DB: 14/15 composers now carry a model signal; the two real conversations report composer-2.5; tokens/cost remain 0/0.

Changes

  • cursor.py: _normalize_model (defaultauto), _composer_model (primary), _bubble_model (fallback); frequency-based model_dominant; docstring updated to spell out the verified schema + that this is name-only.
  • 5 synthetic-fixture tests: composer-config path, defaultauto, bubble-modelInfo fallback, frequency dominance, and the no-model case (fields stay []/None). Full suite green (510 passed).

🤖 Generated with Claude Code

@aktasbatuhan

Copy link
Copy Markdown
Member

Reviewed this and the content is great. You did the one thing #115 actually needed: verified against a real install instead of trusting the web-research framing, and your findings correct it nicely (model name lives on composerData.modelConfig.modelName as the primary, modelInfo.modelName is on the user bubbles not the assistant ones, and Auto writes the literal "default"). Keeping it name-only with tokens/cost untouched is exactly right, and frequency-ranked model_dominant is the honest call given there are no per-model tokens. Tests pass locally (19 cursor tests, full suite green).

One thing blocking the merge, and it's purely mechanical, not your code: the PR shows up as conflicting. That's because this is the same feat/cursor-adapter branch that #113 was squash-merged from. After a squash merge, main has #113 as a single new commit, but the branch still carries the original #113 commits, so git sees an add/add conflict on the shared files.

The good news is the only genuinely new work here is the model commit (de32114), and it lands on main with zero conflicts. Quickest fix is to reset the branch onto current main and replay just that commit:

git fetch upstream            # or whatever your firstbatchxyz/watchmen remote is called
git checkout feat/cursor-adapter
git reset --hard upstream/main
git cherry-pick de32114
git push --force-with-lease

That drops the two already-merged commits and leaves only the model change, which should flip this green. Adjust the remote name if yours differs (it might be origin if you cloned the main repo directly). Ping me once it's pushed and I'll re-review and merge. Thanks for the careful verification on this one.

…model label (firstbatchxyz#113 follow-up)

Verified against a real Cursor install where the model NAME lives and how Auto
mode looks, then wired it into the adapter:

- composerData.modelConfig.modelName is the authoritative source (present on
  every composer); each user bubble (type 1) also stamps modelInfo.modelName
  (the field CodeBurn reads), used as a fallback when the composer row is gone.
- Auto mode records the literal "default" (not blank, not "auto"); normalize
  it to "auto" for a clean corpus signal.
- No per-model tokens in the store, so model_dominant is the MOST FREQUENT
  model, not token-weighted like pi.py. Token/cost columns stay 0 — this adds
  the model NAME only; the firstbatchxyz#113 token/cost limitations are unchanged.

Synthetic-fixture tests cover the composer-config path, the "default"->"auto"
normalization, the bubble-modelInfo fallback, frequency-based dominance, and
the no-model case (fields stay empty). Full suite green (510 passed).

Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
@goktugecertastan-cyber

goktugecertastan-cyber commented Jun 4, 2026

Copy link
Copy Markdown
Contributor Author

Fixed the branch — thanks for the clear diagnosis. Did exactly what you suggested (remote is origin here, not upstream):

git fetch origin
git reset --hard origin/main      # drops the two already-squash-merged #113 commits
git cherry-pick de32114           # replays only the model work
git push --force-with-lease

The branch now sits on current main with a single new commit (a04d135). git diff --stat origin/main...HEAD shows only the model change (cursor.py + tests, 173 insertions), and the 19 cursor tests + full suite stay green. PR shows MERGEABLE now.

Ready for re-review whenever you are.

The modern-shape test pinned project_dir to a literal "/home/u/proj/src",
but the adapter derives it via os.path.commonpath, which uses the host
separator — "\" on Windows — so the test failed on windows-latest CI while
passing on macOS/Linux. Compare against os.path.normpath(...) so the
expected value picks up the same separator the adapter produces.

Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
@goktugecertastan-cyber

Copy link
Copy Markdown
Contributor Author

CI update — pushed one small follow-up commit (1814286):

  • Windows tests: fixed. test_scan_modern_joins_bubbles_with_tool_and_skill pinned project_dir to a literal /home/u/proj/src, but the adapter derives it via os.path.commonpath, which uses the host separator (\ on Windows) — so it failed on windows-latest while passing on macOS/Linux. Changed the assertion to compare against os.path.normpath(...). All test jobs are green now (3 OS × py3.11/3.12), full suite 510 passed locally.

  • lint (ruff): not from this PR. The remaining failure is F821 Undefined name 'args' at src/watchmen/curate.py:830 (_maybe_auto_install(out_dir.name, force=args.auto_install)). That file isn't touched by this branch — it came in with the auto-install commit (7538f07) and args isn't in scope there. Looks like a pre-existing bug on main rather than anything here. Flagging it for you; happy to open a separate PR for it if you'd like, but didn't want to widen this PR's scope.

Ready for re-review.

…(F821)

write_changelog's body calls `_maybe_auto_install(..., force=args.auto_install)`
but the function had no `args` parameter, so ruff flagged F821 (undefined name)
and the call would raise NameError at runtime (swallowed by the surrounding
try/except, which is why it went unnoticed). Thread `args` through the single
call site (main, where it's in scope) so the --auto-install flag actually
takes effect and lint passes.

Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
@goktugecertastan-cyber

Copy link
Copy Markdown
Contributor Author

CI is fully green now — went ahead and fixed the ruff failure too (commit 5dd1d89).

Root cause: write_changelog() had no args parameter, but its body calls _maybe_auto_install(out_dir.name, force=args.auto_install) — so ruff flagged F821 Undefined name 'args', and at runtime that line would raise NameError (silently swallowed by the surrounding try/except, which is why it never surfaced). Fix is minimal: thread args through the single call site (in main(), where it's already in scope), so the --auto-install flag actually takes effect and lint passes.

It's a pre-existing bug from the auto-install commit, not from this PR's model work — flagging it clearly so you can decide whether you'd rather pull it into a separate PR. Happy to split it out if you prefer to keep this PR scoped to the Cursor model change; just say the word and I'll revert it here and open it on its own.

All 7 checks pass (lint + tests across macOS/ubuntu/windows × py3.11/3.12). Ready for re-review.

@aktasbatuhan aktasbatuhan left a comment

Copy link
Copy Markdown
Member

Choose a reason for hiding this comment

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

Perfect! 🎉

Thanks for the clean rebase and fixing that pre-existing ruff issue too. The undefined args in write_changelog() was indeed a bug from the auto-install commit — good catch threading it through properly so --auto-install actually works.

Happy to include the bug fix here since it's closely related (auto-install functionality) and you've already done the work. The scoped approach (model extraction + fixing the flag it depends on) makes sense.

Excellent work on the real-install verification and staying in scope. This closes #115 cleanly.

@aktasbatuhan aktasbatuhan merged commit b9f0622 into firstbatchxyz:main Jun 5, 2026
7 checks passed
aktasbatuhan added a commit that referenced this pull request Jun 5, 2026
…120)

The auto-install feature shipped in #113-era curate work was silently broken:
write_changelog() called `_maybe_auto_install(..., force=args.auto_install)` but
had no `args` parameter, so the NameError was swallowed by the surrounding bare
`except Exception` and auto-install never ran. Fixed in #118 by threading `args`
through; this adds the regression coverage that was missing.

Three tests exercise the real write_changelog → _maybe_auto_install path
(heavy git/state/index helpers stubbed so the test is hermetic):
- force=args.auto_install reaches the installer and symlinks skills even with the
  project opt-in off (the --auto-install path that broke);
- the force value is read from args, not a constant;
- flag off installs nothing.

Verified these fail (swallowed NameError → no install) against the buggy
signature and pass on the fix.

Co-authored-by: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
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.

feat(cursor): extract per-message model from modelInfo (needs verification)

2 participants