← Commit history

Annotate docs: Multi default and keys; gates refuse to run over other threads' windows

John Lauer ·1962bc1559 ·15d ago ·parent 1c42ac3
3 files changed +113−6
docs/screenshots-and-recording.md+1−1
@@ -29,4 +29,4 @@ ffmpeg -i take.mp4 -vf "fps=12,scale=800:-1" loop.gif  ## Annotate -Annotate puts a drawing layer over a page: pen, arrows, boxes and text. Start it from a tile's jump list (Annotate this page), from the pen in pup's browser toolbar, with Alt+Shift+A, or by an AI calling `pup_annotate`. When you click Done, the marked-up region is captured, copied to your clipboard, and sent to the AI thread that owns the window, with a thin caption naming the thread, page and time. The drawing stays visible, so the AI's next screenshot shows it too.+Annotate puts a drawing layer over a page: pen, arrows, boxes and text. Start it from a tile's jump list (Annotate this page), from the pen in pup's browser toolbar, with Alt+Shift+A, or by an AI calling `pup_annotate`. It starts in **Multi** mode: draw as many shapes as you like (each in the next color), then press **Enter** or click Done. **Space** switches to **Single** mode, where each shape is captured the moment you lift the mouse (the quick red circle); **M** and **S** pick a mode directly and **Esc** cancels. A hint under the toolbar shows the keys. When you finish, the marked-up region is captured, copied to your clipboard, and sent to the AI thread that owns the window, with a thin caption naming the thread, page and time. The drawing stays visible, so the AI's next screenshot shows it too.
tools/foreground-proof.sh+13−5
@@ -36,12 +36,20 @@ t=[x for x in w if not x.get('minimized') and x.get('kind')=='normal' and x.get( print(t[0].get('hwnd') if t else '')"); fi [ -n "$PIN" ] || { echo "no non-pup window to pin on $BOX"; exit 2; } echo "== pin window $PIN in front, force a cold lane =="-# every thread's windows, by name: the "everyone" close only clears the caller's own (measured), and a-# leftover window with the harness's name owned by another thread makes the open fail with not_your_window-for W in $(timeout 60 $AB pup_status '{}' 2>/dev/null | python3 -c "+# 2.0.700: NEVER touch another thread's windows. This used to close every window on the box with takeover, then+# force-kill the browser; on 2026-10-05 it closed another thread's adom-parts-search window on arav-rog for good.+# The gate needs a cold browser, which means ending every window in it, so a box where another thread has+# windows is not a valid box for it: refuse (INVALID, exit 2) and name them. Only the gate's own leftovers close.+SESS=$(timeout 60 $AB pup_status '{}' 2>/dev/null)+OTHERS=$(printf '%s' "$SESS" | python3 -c " import sys,json-try: print(' '.join(s.get('sessionId') for s in json.load(sys.stdin).get('sessions') or []))-except Exception: pass"); do timeout 60 $AB pup_close_window "{\"window\":\"$W\",\"takeover\":true,\"reason\":\"foreground proof: clear every pup window first\"}" >/dev/null 2>&1; done+try: print(' '.join(s.get('sessionId')+'('+str(s.get('owner'))+')' for s in json.load(sys.stdin).get('sessions') or [] if s.get('owner')!='pup-foreground-proof'))+except Exception: print('?unreadable')")+if [ -n "$OTHERS" ]; then echo "FOREGROUND PROOF: INVALID - other threads have pup windows on $BOX: $OTHERS. The gate restarts the browser and would close them; run it on a box with no other pup windows."; exit 2; fi+for W in $(printf '%s' "$SESS" | python3 -c "+import sys,json+try: print(' '.join(s.get('sessionId') for s in json.load(sys.stdin).get('sessions') or [] if s.get('owner')=='pup-foreground-proof'))+except Exception: pass"); do timeout 60 $AB pup_close_window "{\"window\":\"$W\",\"reason\":\"foreground proof: clear the gate's own leftover windows\"}" >/dev/null 2>&1; done cat > "$S/cold.ps1" <<'PS' Get-CimInstance Win32_Process -Filter "Name='chrome.exe'" | Where-Object { $_.CommandLine -match 'pup-profiles' } | ForEach-Object { Stop-Process -Id $_.ProcessId -Force -ErrorAction SilentlyContinue } Start-Sleep -Seconds 2; "cold"
tools/jumplist-ralph.py+99
@@ -27,6 +27,13 @@ def b64(o): return base64.b64encode(json.dumps(o, separators=(',', ':')).encode(  def main():     ap = argparse.ArgumentParser(); ap.add_argument('--target', required=True); a = ap.parse_args(); t = a.target+    # 2.0.700: this gate fires the real "Close ALL Adom Pup windows" task and closes every window between checks+    # (pup_close everyone), which takes other threads' windows with it. A box where another thread has windows is+    # not a valid box: refuse (INVALID, exit 2) and name them, as tools/foreground-proof.sh does.+    others = [f"{s.get('sessionId')}({s.get('owner')})" for s in (ab(t, 'pup_status', {}).get('sessions') or []) if s.get('owner') not in (THREAD, 'user-taskbar-menu')]+    if others:+        print(f"JUMPLIST RALPH: INVALID - other threads have pup windows on {t}: {' '.join(others)}. This gate closes every pup window; run it on a box with no other pup windows.")+        sys.exit(2)     exe = ps(t, "Get-ChildItem (Join-Path $env:LOCALAPPDATA 'Adom Bridge\\bridges-cache\\puppeteer') -Filter *.exe | Select-Object -First 1 -ExpandProperty FullName", 'jump-list ralph: locate the pup exe')     shim = ps(t, "Join-Path $env:LOCALAPPDATA 'Adom Bridge\\ab-taskrun.exe'", 'jump-list ralph: locate the taskrun shim')     print(f'exe={exe}\nshim={shim}')@@ -48,8 +55,100 @@ def main():     lau = (ab(t, 'pup_list_windows', {}).get('sessions') or [{}])     lau = next((s.get('lastAgentUpdate') or {} for s in lau if s.get('sessionId') == sid), {})     check('annotate overlay armed', e.get('result') is True, f'({out}; lastAgentUpdate={lau.get("verb")})')+    # 2.0.444 (John: "'annotate this page' on the fusion taskbar icon did nothing"): a HUMAN caller's+    # annotate must bring the window to the screen, not arm a pen on a window parked at the bottom.+    wi = ab(t, 'pup_window_info', {'window': sid})+    fg = ab(t, 'desktop_get_foreground', {})+    fgh = fg.get('hwnd') or (fg.get('window') or {}).get('hwnd')+    idle = (ab(t, 'desktop_user_idle_ms', {}) or {}); idle_ms = idle.get('idleMs') if isinstance(idle.get('idleMs'), int) else (json.loads(idle.get('output') or '{}').get('idleMs') if isinstance(idle.get('output'), str) else None)+    raised_ok = bool(wi.get('foreground')) and (fgh is None or fgh == r.get('hwnd'))+    elevated = ''+    if not raised_ok:+        probe = ab(t, 'desktop_bring_to_front', {'hwnd': r.get('hwnd'), 'reason': 'jump-list ralph: read why Windows refused the Annotate raise'})+        if 'INTEGRITY' in (probe.get('error') or '').upper(): elevated = (json.loads(ab(t, 'desktop_get_foreground', {}).get('output') or '{}').get('title') or '?')+    if not raised_ok and elevated:+        print(f'  SKIP annotate RAISED the window: the foreground belongs to an elevated process ({elevated!r}); Windows refuses a foreground switch away from it for every process, ab included')+    elif not raised_ok and isinstance(idle_ms, int) and idle_ms < 200_000:+        # Windows refuses a foreground switch while the user is active (foreground lock, ~200 s after input); a+        # scripted raise from shell_execute cannot win that, by design. The click path (shim from the jump list) can.+        print(f'  SKIP annotate RAISED the window: someone used this box {idle_ms // 1000}s ago (foreground lock active); the raise refusal is Windows, not pup')+    else:+        check('annotate RAISED the window (user caller)', raised_ok, f'(session.foreground={wi.get("foreground")}, os fg hwnd={fgh}, window hwnd={r.get("hwnd")}, idleMs={idle_ms})')     ab(t, 'pup_eval', {'window': sid, 'expr': 'window.__pupAnnotate && window.__pupAnnotate.exit && window.__pupAnnotate.exit(); true'}) +    # 1b. The Adom TOOLBAR pen (2.0.453): worker.js sends only the tab URL and caller user-toolbar; pup must+    # resolve the window from the URL (John, 2026-09-10: after a wake the pen did nothing).+    print('\n== Toolbar pen by _tabUrl (worker.js payload)')+    tabs = ab(t, 'pup_list_tabs', {'window': sid}); cur = ((tabs.get('tabs') or [{}])[0].get('url')) or WIKI+    body = json.dumps({'command': 'pup_annotate', 'args': {'_tabUrl': cur, 'tool': 'pen', 'caller': {'aiThread': 'user-toolbar'}}})+    out = ps(t, "$pf = Join-Path $env:LOCALAPPDATA 'Adom Bridge\\pup-bridge-port.json'; $port = (Get-Content $pf -Raw | ConvertFrom-Json).port; (Invoke-WebRequest -Uri ('http://127.0.0.1:' + $port + '/command') -Method Post -ContentType application/json -Headers @{'x-adom-caller-thread'='user-toolbar'} -Body '" + body.replace("'", "''") + "' -UseBasicParsing).Content", 'jump-list ralph: the toolbar pen payload (_tabUrl, user-toolbar)')+    time.sleep(2); e = ab(t, 'pup_eval', {'window': sid, 'expr': '!!window.__pupAnnotate'})+    started = ('started\\":true' in str(out)) or ('"started":true' in str(out))   # the reply is an escaped JSON envelope+    check('toolbar pen resolved the window by _tabUrl and armed', e.get('result') is True and started, f'(url={cur[:60]}; started={started}; reply={str(out)[:400]})')+    # 1b2. Esc closes the overlay (2.0.457, John: "esc key should let me turn it back off"): a real key event.+    ab(t, 'pup_press_key', {'window': sid, 'key': 'Escape'}); time.sleep(1)+    e2 = ab(t, 'pup_eval', {'window': sid, 'expr': '!window.__pupAnnotate'})+    check('Esc closed the annotation overlay', e2.get('result') is True, f'(overlay gone={e2.get("result")})')+    ab(t, 'pup_eval', {'window': sid, 'expr': 'window.__pupAnnotate && window.__pupAnnotate.exit && window.__pupAnnotate.exit(); true'})+    # 1b3. Focus emulation follows the active tab (2.0.458, John: touchpad scrolling stuttering with 21 tabs).+    # Every attached tab used to carry it, so no background tab was ever timer-throttled or discardable.+    print('\n== Focus emulation is not left on every tab')+    ab(t, 'pup_open_tab', {'window': sid, 'url': WIKI + '/releases'}); time.sleep(3)+    rows = (ab(t, 'pup_list_tabs', {'window': sid}).get('tabs') or [])   # list_tabs resolves the SESSION, so it does not perturb the state it reports+    emulated = [x.get('tabId') for x in rows if x.get('focusEmulated')]+    active = [x.get('tabId') for x in rows if x.get('active')]+    check('only the active tab is focus-emulated', len(rows) >= 2 and emulated == active, f'(tabs={len(rows)}, emulated={emulated}, active={active})')+    ab(t, 'pup_close', {'all': True, 'everyone': True}); time.sleep(1)+    r = ab(t, 'pup_open_window', {'window': 'jl-ralph', 'url': WIKI, 'owner': THREAD}, timeout=150); sid = r.get('sessionId') or 'jl-ralph'; time.sleep(6)++    # 1b4. A press must never be left down (2.0.460, John: "i still can't scroll with my touchpad on ANY of your+    # pup windows... i also can't change tabs"). A release that failed mid-click left Chrome treating every later+    # mouse event as one drag. The next mouse verb on the tab has to finish that click.+    print('\n== A stuck mouse button is released before the next mouse verb')+    ab(t, 'pup_eval', {'window': sid, 'expr': "window.__ups = 0; window.__downs = 0; addEventListener('pointerdown', () => window.__downs++, true); addEventListener('pointerup', () => window.__ups++, true); true"})+    ab(t, 'pup_input_dispatch', {'window': sid, 'type': 'mousePressed', 'x': 40, 'y': 300, 'button': 'left'})   # a press with no release, exactly what a timed-out release leaves behind+    d1 = ab(t, 'pup_eval', {'window': sid, 'expr': 'window.__downs + "/" + window.__ups'})+    ab(t, 'pup_click', {'window': sid, 'x': 60, 'y': 320})   # any later mouse verb must release it first+    d2 = ab(t, 'pup_eval', {'window': sid, 'expr': 'window.__downs + "/" + window.__ups'})+    ups = int(str(d2.get('result') or '0/0').split('/')[1]); downs = int(str(d2.get('result') or '0/0').split('/')[0])+    check('the orphaned press was released', ups >= downs and ups >= 2, f'(down/up after the orphan press: {d1.get("result")}, after the next click: {d2.get("result")})')++    # 1b5. Every open tells the calling AI what it is spending (2.0.462, John: "if the ai knew how much ram it+    # was using, that might have helped the ai decide... you should be automatically telling it what % of ram+    # was even left on the box").+    print('\n== Memory is reported back to the caller')+    rt = ab(t, 'pup_open_tab', {'window': sid, 'url': WIKI + '/releases'}); time.sleep(2)+    m = rt.get('memory') or {}+    ok_mem = isinstance(m.get('boxFreePct'), int) and isinstance(m.get('boxTotalMb'), int) and m.get('boxTotalMb') > 0 and isinstance(m.get('_hint'), str) and str(m.get('boxFreePct')) in m.get('_hint', '')+    check('open_tab reports box RAM and what a tab costs', ok_mem, f"(freePct={m.get('boxFreePct')}, browserMb={m.get('browserMb')}, avgMbPerTab={m.get('avgMbPerTab')}, tabs={m.get('tabsOnThisBrowser')})")+    lw = ab(t, 'pup_list_windows', {}).get('memory') or {}+    check('pup_list_windows reports it too', isinstance(lw.get('boxFreePct'), int), f"(freePct={lw.get('boxFreePct')})")++    # 1b6. An idle browser is released (2.0.463, John: "if pup has no windows open, why is it even using 1.2gb+    # across 8 processes?"). Set the grace to 1 min, close everything, and it must be gone within ~90 s.+    print('\n== An idle browser is released')+    ab(t, 'pup_configure', {'idleBrowserQuitMin': '1'})+    ab(t, 'pup_close', {'all': True, 'everyone': True}); time.sleep(2)+    gone = False+    for _ in range(10):+        time.sleep(15)+        st = ab(t, 'pup_status', {})+        m = st.get('memory') or {}+        if not m.get('browserMb'): gone = True; break+    check('the browser with no windows was closed', gone, '(pup_status no longer reports a browser for the profile)')+    ab(t, 'pup_configure', {'idleBrowserQuitMin': '10'})+    r = ab(t, 'pup_open_window', {'window': 'jl-ralph', 'url': WIKI, 'owner': THREAD}, timeout=150); sid = r.get('sessionId') or 'jl-ralph'; time.sleep(6)++    # 1c. A tab the USER opens in a pup window (window.open from the page, outside pup) is adopted by that+    # window's session within a health tick (2.0.453).+    print('\n== User-opened tab adoption')+    before = len(ab(t, 'pup_list_tabs', {'window': sid}).get('tabs') or [])+    ab(t, 'pup_eval', {'window': sid, 'expr': "window.open('https://wiki.adom.inc/adom/pup-bridge/releases', '_blank'); true"})+    time.sleep(3); after = ab(t, 'pup_list_tabs', {'window': sid}).get('tabs') or []+    check('user-opened tab adopted into the window session', len(after) == before + 1 and any('releases' in (x.get('url') or '') for x in after), f'(tabs {before} -> {len(after)})')+    ab(t, 'pup_close', {'all': True, 'everyone': True}); time.sleep(1)+    r = ab(t, 'pup_open_window', {'window': 'jl-ralph', 'url': WIKI, 'owner': THREAD}, timeout=150); sid = r.get('sessionId') or 'jl-ralph'; time.sleep(6)+     # 2. Switch to public view / logged-in view     print('\n== Switch to public view'); out = fire('pup_wiki_set_view', {'sessionId': sid, 'view': 'public'}, 'jump-list ralph: public view task'); time.sleep(6)     u = ab(t, 'pup_eval', {'window': sid, 'expr': 'location.href'}).get('result') or ''