"""The site's own URL for opening a composer, used when the task is to create something new. Reachability, not fill mechanics, is what a 20-run dry sweep measured as the ceiling on scripted writes: the send script armed 20/20 and reached a composer 0/20. The fill, the commit check and the two-sided receipt are all proven; what fails is that prestage's aux navigator lands on the site's HOME page, and the composer is one more hop that it does not reliably take. Polling harder does not help, it was tried twice and the reached rate went 40% -> 0%. Every site here publishes a URL that opens its own composer. Asking for that URL is deterministic where hunting for a button is not: no selector to drift, no modal to race, no capped element list to starve. It is also cheaper, since a hit skips the aux navigation loop entirely. Deliberately narrow, because composing in the wrong place is worse than not composing at all: - only for a task that creates something TOP-LEVEL. A reply or comment belongs on the thread the user is looking at, so those keep their own target (the same rule the post-is-not-a-comment guard enforces on the composer, applied one layer earlier and one layer cheaper). - only when the task points at the bare site. Any deeper URL on that host is a specific target the user chose, and it outranks the generic composer every time. - only as a PROPOSAL. The caller navigates, then checks whether a composer actually appeared; if it did not, the normal path runs untouched. A site that changes its compose URL degrades to today's behaviour rather than stranding the run somewhere useless. """ import os import re from typing import Dict, List, Optional from urllib.parse import urlparse from typeguard import typechecked from backend.apps.agents.browser import browser_fast_path, browser_send_parse # Registrable host -> the site's own compose URL. A dynamic-key map keyed by host, matched by # suffix so www./m./mobile. variants all resolve. Adding a site is one row. # # These are the sites' own documented entry points, not scraped links: # x /compose/post opens the post dialog on a fresh page # linkedin ?shareActive=true opens the "start a post" modal on the feed # reddit /submit?type=TEXT the self-post form # gmail ?compose=new opens a compose window in the mail UI P_COMPOSE_URLS: Dict[str, str] = { "x.com": "https://x.com/compose/post", "twitter.com": "https://x.com/compose/post", "linkedin.com": "https://www.linkedin.com/feed/?shareActive=true", "reddit.com": "https://www.reddit.com/submit?type=TEXT", "mail.google.com": "https://mail.google.com/mail/u/0/#inbox?compose=new", } # Creating something new. "compose"/"draft"/"email" carry gmail, where nobody says "post". P_CREATE_RE = re.compile( r"\b(post|posting|tweet|tweeting|publish|publishing|share|sharing|compose|composing|" r"draft|drafting|write|writing|send|create|creating|start|starting|ask|asking)\b" # "open a new issue/thread" is a create; bare "open" is navigation ("open the first video"), # so only the whole phrase counts. Measured: github never reached the tier without this. r"|\bopen(ing)? a new\b", re.I) # Answering something that already exists. One of these and the target is the thread, not the site. P_RESPOND_RE = re.compile(r"\b(reply|replies|replying|comment|commenting|respond|responding|" r"answer|answering|quote|retweet|dm|message)\b", re.I) P_URL_RE = re.compile(r"https?://[^\s\"'<>)\]]+", re.I) # "go to x.com and post ..." names its site without a scheme, which is how most tasks arrive. P_BARE_HOST_RE = re.compile(r"(?:^|[\s/@(,])((?:[\w-]+\.)+[a-z]{2,})\b", re.I) @typechecked def registrable_host(url_or_host: str) -> str: """The host with any www./m./mobile. prefix removed, lowercased, port dropped. Prefix stripping is done with a real prefix check; `lstrip("www.")` would eat any leading w or dot and quietly turn `w3schools.com` into `3schools.com`.""" raw = (url_or_host or "").strip() host = urlparse(raw).netloc if "//" in raw else raw host = host.lower().split("@")[-1].split(":")[0] for prefix in ("www.", "m.", "mobile."): if host.startswith(prefix): host = host[len(prefix):] break return host @typechecked def p_table_hit(host: str) -> str: """The compose URL for this host, matching a parent domain too (`old.reddit.com` -> reddit). Exact-or-dotted-suffix only: a bare `endswith` would match `notreddit.com` against `reddit.com` and send a post to a site the user never named.""" host = registrable_host(host) if not host: return "" if host in P_COMPOSE_URLS: return P_COMPOSE_URLS[host] for known, url in P_COMPOSE_URLS.items(): if host.endswith("." + known): return url return "" @typechecked def p_names_deeper_target(task: str, host: str) -> bool: """True when the task carries a URL on this host that points somewhere more specific than its front page. That URL is the user's chosen target and must win over the generic composer.""" want = registrable_host(host) for m in P_URL_RE.finditer(human_words(task)): parsed = urlparse(m.group(0).rstrip(".,;)")) found = registrable_host(parsed.netloc) if not found or (found != want and not found.endswith("." + want)): continue if parsed.path.strip("/") or parsed.query or parsed.fragment: return True return False @typechecked def human_words(task: str) -> str: """The task minus the aux-written routing brief. A dispatched task is the user's prompt followed by a brief a model wrote about how to route it. The brief is prose, and it both quotes things of its own and uses answering words, so reading intent off the whole string reads the model's commentary as the user's request. The send script hit this first (a brief saying "do not submit it" read-only-flagged a real send). Everything reads this, refusals included. Letting the brief veto looked like the safe choice and was not: briefs routinely spell out a route ("navigate to https://x.com/home"), so the deeper-target veto fired on the model's own suggestion and silently disabled the tier on two of four sites. A brief cannot turn a post into a reply either, since the words that would say so are the user's and are still read here.""" return (task or "").split(browser_fast_path.BRIEF_MARKER, 1)[0] @typechecked def wants_top_level_compose(task: str) -> bool: """True when the task creates something new rather than answering something that exists. An intent word alone is not enough: "what is the top post on reddit" is a READ, and `post` there is a noun. Reading the verb correctly needs a model, so instead this asks for the thing every real write in this product carries and no read does, the quoted text to write. That is also the send script's own precondition, so a task this refuses is one the script would decline anyway, and the aux navigator handles it exactly as it does today.""" text = human_words(task) if not browser_send_parse.quoted_payload(text): return False return bool(P_CREATE_RE.search(text)) and not P_RESPOND_RE.search(text) @typechecked def enabled() -> bool: """On by default. This tier only ever navigates the user's browser to a page that site publishes for exactly this purpose, and the caller verifies the result before relying on it, so the failure mode is a wasted page load rather than a wrong action. The switch exists to A/B it against the aux navigator and to turn it off in the field without a rebuild.""" return os.environ.get("OSW_COMPOSE_ENTRY", "1") != "0" @typechecked def named_hosts(task: str, start_url: str) -> List[str]: """The hosts this task is about, in the order they should be trusted. The site the USER NAMED wins, and where the card happens to sit is only the fallback for a task that names none ("post this" while already on the site). That order is load-bearing for anything that has to be ON a site before it can read from it: a cold run opens on a blank search page, so trusting the card first made the first live discovery read google.com and correctly find nothing. `compose_entry_for` can afford the opposite order because a table hit already proves relevance; without a table there is nothing to filter a wrong host out. Same extraction either way, kept in one place so the two cannot drift into disagreeing about which site a task is for. The routing brief is excluded, so a model cannot redirect the post.""" out: List[str] = [] for candidate in (*[m.group(0) for m in P_URL_RE.finditer(human_words(task))], *[m.group(1) for m in P_BARE_HOST_RE.finditer(human_words(task))], start_url): host = registrable_host(candidate) if host and "." in host and host not in out: out.append(host) return out @typechecked def named_page(task: str, host: str) -> str: """The most specific page the user named on this host, else its front page. Composing often lives under a section rather than at the root: "open a new issue" on `github.com/owner/repo` is reachable from the repo and nowhere near `github.com/`, which publishes no compose link at all. Measured, so the host alone is not enough to go on.""" want = registrable_host(host) for m in P_URL_RE.finditer(human_words(task)): url = m.group(0).rstrip(".,;)") found = registrable_host(urlparse(url).netloc) if found and (found == want or found.endswith("." + want)): return url return f"https://{want}/" @typechecked def compose_entry_for(task: str, start_url: str, task_is_send: bool) -> Optional[str]: """The URL to open to reach this site's composer, or None to leave navigation alone. `start_url` is where the card already is; a host named in the task counts too, since a run that begins on a blank tab still says "go to x.com and post ...". `task_is_send` is the caller's already-computed write verdict and is REQUIRED, not defaulted, because forgetting it is silently destructive: a quote is not proof of a write, and `find the reddit post that says "..."` reads as a create to any regex short enough to be readable (`post` is a noun there). Four such phrasings each resolved to reddit's SUBMIT page in a probe, which would derail a plain read. The verdict the send script itself gates on is the right authority, so this asks for it rather than growing a second opinion that can drift.""" if not enabled() or not task_is_send or not wants_top_level_compose(task): return None # Which site to open comes from the user's words; a brief naming some other site must not # redirect the post. asked = human_words(task) named = [m.group(0) for m in P_URL_RE.finditer(asked)] named += [m.group(1) for m in P_BARE_HOST_RE.finditer(asked)] for candidate in (start_url, *named): url = p_table_hit(candidate) if not url: continue host = registrable_host(candidate) if p_names_deeper_target(task, host): return None # Already on the compose surface: navigating again would remount it and throw away a # composer that is right there. if registrable_host(start_url) == host and p_on_compose_surface(start_url, url): return None return url return None @typechecked def p_on_compose_surface(current_url: str, compose_url: str) -> bool: """True when the current URL is already this site's compose surface.""" cur, target = urlparse(current_url or ""), urlparse(compose_url or "") if not cur.netloc: return False cur_mark = (cur.path.strip("/") + "?" + cur.query + "#" + cur.fragment).lower() target_mark = (target.path.strip("/") + "?" + target.query + "#" + target.fragment).lower() for token in ("compose", "submit", "shareactive"): if token in target_mark and token in cur_mark: return True return False