Skip to content

fix(person): detect custom note quota block in connect_with_person#449

Merged
stickerdaniel merged 8 commits into
stickerdaniel:mainfrom
ghul0:fix/448-custom-note-limit-detection
Jun 10, 2026
Merged

fix(person): detect custom note quota block in connect_with_person#449
stickerdaniel merged 8 commits into
stickerdaniel:mainfrom
ghul0:fix/448-custom-note-limit-detection

Conversation

@ghul0

@ghul0 ghul0 commented May 16, 2026

Copy link
Copy Markdown

Summary

Fixes #448. When LinkedIn rejects an invite-with-note because the account's free personalized-note quota is exhausted, the old code path could return misleading/generic states such as send_failed or connect_unavailable.

This PR keeps the response simple:

{
  "status": "custom_note_limit_reached",
  "message": "<raw LinkedIn Premium dialog text>",
  "note_sent": false
}

No retry hints, no synthesized explanation. The message is read directly from the LinkedIn Premium upsell dialog.

Behaviour changes

  • New status custom_note_limit_reached on connect_with_person with note_sent: false.
  • Removed can_send_without_note; callers only get the status plus LinkedIn's own message text.
  • note_sent now tracks delivery of the note, not textarea fill. Any failure path leaves it false.
  • Existing statuses (connected, accepted, send_failed, connect_unavailable, etc.) are unchanged.

Detection signal

Locale-independent: the upsell renders a dialog whose body links to /premium/.... The detector waits for dialog[open] a[href*="/premium/"] or [role="dialog"] a[href*="/premium/"], then reads the surrounding dialog text directly from LinkedIn.

Implementation

  • _get_premium_upsell_message waits for the Premium link and returns the raw dialog innerText / textContent.
  • _submit_invite_dialog returns (submitted, note_sent, note_limit_message).
  • Detection covers:
    1. Add note opening the Premium upsell before a textarea appears,
    2. filling the note failing because the upsell replaced the editor,
    3. submit being intercepted by the upsell.
  • _probe_invite_note_limit handles no-anchor profiles by opening the deeplink only as a non-submitting probe and returning the raw dialog text if LinkedIn shows the upsell. It never clicks Send.
  • Public tools/person.py docstring updated.

Testing

Latest head: d11bda3.

  • GitHub CI pending/running after latest push.
  • Local: uv run --python 3.13.9 ruff check . clean.
  • Local: uv run --python 3.13.9 ruff format --check . clean.
  • Local: uv run --python 3.13.9 ty check clean.
  • Targeted local tests: 34 passed for tests/test_scraping.py::TestConnectWithPerson and tests/test_tools.py::TestPersonTool.
  • Local integration branch with PR feat(network): add get_pending_invitations tool #447 merged: 72 passed for connect/person, network tool, and link metadata coverage.
  • Full local suite still has one pre-existing environment-sensitive failure in tests/test_browser_security.py::test_harden_linkedin_tree_noop_outside_linkedin on this machine (umask=002 parent-dir mode); unaffected suite passes when that single known test is deselected.

Synthetic prompt

When connect_with_person hits LinkedIn's Premium upsell because personalized invite-note quota is exhausted, return status: custom_note_limit_reached, note_sent: false, and set message to the raw dialog text from LinkedIn. Do not add retry hints such as can_send_without_note. Detect the upsell via a locale-independent /premium/ link in the dialog, read the surrounding dialog text directly, cover Add-note/fill/submit interception cases, and keep the no-anchor deeplink path probe-only so it never submits an invite.

Generated with Claude Opus 4.7

Closes #448

@ghul0 ghul0 force-pushed the fix/448-custom-note-limit-detection branch from d11bda3 to 21a9e3e Compare May 17, 2026 06:36
@stickerdaniel

Copy link
Copy Markdown
Owner

@greptileai review

@greptile-apps

greptile-apps Bot commented May 20, 2026

Copy link
Copy Markdown
Contributor

Greptile Summary

This PR adds detection for LinkedIn Premium note-quota blocks in the connection flow. It changes:

  • Detects Premium upsell dialogs by looking for /premium/ links inside open dialogs.
  • Returns custom_note_limit_reached with LinkedIn's raw dialog text when invite notes are blocked.
  • Updates invite submission so note_sent reflects note delivery rather than textarea fill.
  • Adds a non-submitting deeplink probe for note-quota detection when no invite anchor is visible.
  • Updates the person tool docs and tests for the new status.

Confidence Score: 5/5

This looks safe to merge.

  • No blocking issues found in the changed code.

Reviews (5): Last reviewed commit: "Merge branch 'main' into fix/448-custom-..." | Re-trigger Greptile

Comment thread linkedin_mcp_server/scraping/extractor.py
@stickerdaniel

Copy link
Copy Markdown
Owner

@greptileai review

ghul0 and others added 6 commits May 21, 2026 02:24
Closes stickerdaniel#448.

When LinkedIn shows the Premium upsell modal because the free
personalized invitation-note quota is exhausted, the previous code
path silently funnelled the failure through `send_failed` while
reporting `note_sent: true` (the note had been written into the
textarea but was never actually delivered).

Changes:

- `_submit_invite_dialog` now returns a three-tuple
  `(submitted, note_sent, note_limit_blocked)`. `note_sent` reports
  *delivery* rather than *textarea fill*, so it is False on every
  failure path including the Premium-modal interception. The dialog
  cleanup contract is unchanged — callers still must not dismiss the
  dialog themselves.

- New `_detect_premium_upsell_modal` helper. The signal is
  locale-independent: the upsell modal renders a `[role="dialog"]`
  whose primary anchor href contains `/premium/`. This matches the
  project's existing rule of keying on stable URL fragments rather
  than localized text (mirrors the `/preload/custom-invite/` gate
  used elsewhere in this module).

- `_connection_result` accepts an optional `can_send_without_note`
  keyword that is surfaced on the response only when set, so existing
  status payloads keep their current shape.

- `connect_with_person` returns a new `custom_note_limit_reached`
  status when `_submit_invite_dialog` reports the upsell interception.
  The response carries `note_sent: False` and
  `can_send_without_note: True`, giving the calling LLM enough
  structure to retry with `note=None`.

- README-visible status list and the public docstring in
  `tools/person.py` updated with the new status and its semantics.

- New `test_connect_with_person_custom_note_limit_reached` covers the
  tool-level contract; existing connect tests remain green.
LinkedIn can swap the invite dialog for the Premium upsell at submit
time, leaving the original primary button detached or pointer-event
covered. _click_dialog_primary_button returns False, the keyboard
fallback also fails, and the not-sent branch dismissed the dialog
without reading the upsell text, so the caller returned
connect_unavailable instead of custom_note_limit_reached with
LinkedIn's raw quota message.

Move the upsell probe ahead of the dismiss, matching the three other
Premium-detection checkpoints in _submit_invite_dialog. A new test
covers the submit-click failure path.

Refs stickerdaniel#448
@stickerdaniel stickerdaniel force-pushed the fix/448-custom-note-limit-detection branch from 6598add to 978ad34 Compare May 21, 2026 00:46
@ghul0 ghul0 marked this pull request as ready for review May 25, 2026 09:52
Comment thread linkedin_mcp_server/scraping/extractor.py
Comment on lines +1861 to +1865
if btn_count >= 3:
try:
await buttons.nth(btn_count - 2).click()
except Exception:
logger.debug("Could not open invite note editor", exc_info=True)

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

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

P1 Probe skips two-button gate

When the no-anchor probe opens LinkedIn's current two-button invite gate, this guard skips the only safe Add a note button because btn_count is 2. The quota upsell can appear only after that button is clicked, so _probe_invite_note_limit() returns None and connect_with_person() reports connect_unavailable instead of custom_note_limit_reached for the path this PR is trying to cover. The submit path already handles this same two-button gate with btn_count >= 2; the probe should mirror that while still avoiding the primary send button.

Suggested change
if btn_count >= 3:
try:
await buttons.nth(btn_count - 2).click()
except Exception:
logger.debug("Could not open invite note editor", exc_info=True)
if btn_count >= 2:
try:
await buttons.nth(btn_count - 2).click()
except Exception:
logger.debug("Could not open invite note editor", exc_info=True)
Prompt To Fix With AI
This is a comment left during a code review.
Path: linkedin_mcp_server/scraping/extractor.py
Line: 1861-1865

Comment:
**Probe skips two-button gate**

When the no-anchor probe opens LinkedIn's current two-button invite gate, this guard skips the only safe `Add a note` button because `btn_count` is `2`. The quota upsell can appear only after that button is clicked, so `_probe_invite_note_limit()` returns `None` and `connect_with_person()` reports `connect_unavailable` instead of `custom_note_limit_reached` for the path this PR is trying to cover. The submit path already handles this same two-button gate with `btn_count >= 2`; the probe should mirror that while still avoiding the primary send button.

```suggestion
        if btn_count >= 2:
            try:
                await buttons.nth(btn_count - 2).click()
            except Exception:
                logger.debug("Could not open invite note editor", exc_info=True)
```

How can I resolve this? If you propose a fix, please make it concise.

@stickerdaniel stickerdaniel merged commit df4a6e2 into stickerdaniel:main Jun 10, 2026
6 checks passed
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

[BUG] connect_with_person reports note_sent=true and send_failed when free invite-note limit is reached

2 participants