# Changelog — Puppeteer Bridge

## 1.9.43 — 2026-07-20 — Why some icons had overlays and others did not, plus stacked session titles

John: "why do you sometimes have overlays on your icons and sometimes not?" It was never a rule, it
was a race and two silent failures. Measured on the live windows:

1. PLACEHOLDER FAVICONS. example.com declares <link rel="icon" href="data:,">. That fetched
   "successfully" as an empty blob, so the overlay was abandoned and /favicon.ico was never tried.
   Junk hrefs are now skipped before fetching.
2. NO RETRY, EVER. If the favicon had not loaded when the overlay ran, applyAppOverlay gave up
   silently and nothing re-ran it, so that window had no overlay for its whole life. It now retries
   once shortly after, logs when it finds nothing, and takes a second pass after navigation since
   many sites inject their icon link after first paint.
3. STACKED SESSION TITLES (the reason an adopted window kept the default icon). retagSessionTitle
   stripped only a TRAILING "(session: ...)", but the old session's MutationObserver had already
   re-appended its own, so the strip missed and we produced
   "BMV080 - Adom Wiki (session: thr) (session: thr-split1-mkh7)". The window still answered to the
   PARENT's suffix, collided with it, and its branding landed unreliably. Now every occurrence is
   stripped, and all title observers share one named handle so an adopted tab can disconnect the
   previous session's observer instead of fighting it forever.

## 1.9.42 — 2026-07-20 — Stop the "event-driven" drag check from behaving like a poll

Chasing a suspected false-split, the detector proved CORRECT (an untouched 3-tab control window was
checked repeatedly and every tab reported the same window id, no split; the earlier adom-tsci splits
were real drags). But the audit exposed a worse problem: 1360 window-opened events across 10 drains
in about a minute. A busy Windows desktop fires that event for tooltips, menus and task switching,
and every drain was running a per-tab CDP check across every multi-tab session, which is exactly the
wasteful polling this design was supposed to avoid.

Two cheap guards: skip entirely when no session has 2+ tabs (a single-tab window cannot be split),
and throttle the check to at most once every 1.5s regardless of event volume. Detection latency is
unchanged in practice since a real drag is followed by more events.

## 1.9.41 — 2026-07-20 — Self-audit of the live taskbar: two icon bugs found and fixed

John: "look at your taskbar icons and audit yourself." Doing that on the real taskbar found two
defects that no amount of reasoning had surfaced:

1. An Adom app was wearing the WEB globe. classifyUrl demanded BOTH a *.adom.cloud host AND a
   /proxy/ path segment, so an app served at a bare slug host (project-manager-xxxx.adom.cloud)
   fell through to "web". Any *.adom.cloud host is now an Adom app.
2. A long-lived window still wore the plain DEFAULT tile with no category at all. Windows that
   predate the category work, or that were recovered across a bridge restart, were never
   re-evaluated. brandSweep now clears each session's cached category and re-runs the full
   refresh, so every live window converges on a correct category icon.

Also corrected a claim I made to John: the extra split sessions I could not explain came from
adom-tsci, a different window than the hybrid one he was testing, not from his drag.

## 1.9.40 — 2026-07-20 — Adopted windows are re-tagged, so they stop being invisible and colliding

John dragged a tab out of a 3-category window and got sloppy results: one new window kept the bare
Chrome icon, and a window wore the favicon of an app it had nothing to do with. One root cause
behind both, and it was careless: an ADOPTED tab kept the title of the session it came FROM. All
three windows still read "(session: hybrid)".

Consequences, both observed live: AD could never resolve the new window by "(session: <newId>)", so
brandPupWindow/overlay/jump-list refresh failed outright ("could not refresh taskbar presence") and
the window kept the default Chrome icon; and because several windows answered to the SAME suffix,
any title-based operation hit an arbitrary one, which is how a shotlog favicon landed on a window
that was not shotlog.

Fix: retagSessionTitle() strips any existing pup suffix, installs the new session's suffix, and
re-arms the MutationObserver. It runs on the adopted tab BEFORE any branding (everything downstream
resolves the window by that suffix), and re-asserts the source window's own tag since its tab set
changed underneath it.

## 1.9.39 — 2026-07-20 — The dark-teal window grows 30 percent, and the glyph keeps its margin

I had misread the last request: I grew the line drawing INTO the window's margins, which is exactly
the bad graphic design John was objecting to. Correct reading: the dark-teal window itself grows 30
percent (156 x 133 on the 244 tile, up from 120 x 102), and the glyph scales with it while keeping a
real margin inside, 16 percent on every side. Nothing touches an edge. Also fixed the stroke weight,
which the rescale had thinned to a hairline (it is now computed to land at the same visual weight as
the original regardless of the glyph box size).

Rule to keep: every drawing gets margins, at every nesting level. Tile margin around the window,
window margin around the glyph.

## 1.9.38 — 2026-07-20 — Drop the toolbar line, glyphs much bigger, real Adom mark for apps

John, still squinting at the taskbar: drop the line separating the browser chrome from the content
(it collided with the glyph and earned nothing), and make the inner glyph bigger again. Done: the
window body is a plain solid rounded rect and the category glyph is roughly 1.7x its original size,
filling the body. The tile margins that match the Adom favicon are unchanged.

Also fixed the app icon: my hand-drawn Adom mark rendered as a blob, so the icon now embeds the REAL
two-lobe mark taken straight from the shipped pup tile art, scaled to fit the window body.

## 1.9.37 — 2026-07-20 — Inner category glyphs 30 percent larger

John, after living with them: the inner glyph was too small to read at taskbar size, but the frame
margins were right because they match the Adom favicon. So the CONTENT glyph is scaled 1.3x about
the content-area centre, growing into the window's empty space; the browser frame, its toolbar line
and the tile margins are untouched. Much more legible at 24px, which is the size Windows actually
paints. README icon art regenerated to match.

## 1.9.36 — 2026-07-20 — Never leave a stale taskbar overlay after a drag-out

The drag-out adoption worked (John dragged a tab out and the new window took pup's identity), but
the window it LEFT kept showing the dragged site's favicon in its overlay even though that tab was
gone. Cause: the refresh was fire-and-forget, and a window whose tab set just changed (or a window
just created) is not findable by title for a beat, so the refresh silently no-oped.

New refreshWindowChrome(session, id) refreshes a window's whole taskbar presence together (identity
icon, favicon overlay, jump list) and RETRIES with backoff until the shell can see the window, then
logs loudly if it never can. Both sides of a drag-out use it: the adopted window and the source
window. The source also clears its cached category and overlay first, so a window that went from
mixed back to plain wiki actually repaints instead of short-circuiting on "same category".

John's rule, recorded: overlays must always be as up to date as they can be. A stale favicon is a
lie about what the window contains.

## 1.9.35 — 2026-07-20 — THE drag-out bug: AD answers in two shapes and pup only understood one

Root cause found. AD's /command replies in TWO shapes depending on path and version: the relay wraps
the payload as {output:"<json string>"}, while the loopback direct API can return the payload BARE.
pup assumed the wrapped shape everywhere, so a bare payload silently collapsed to "" and every field
read as undefined. The drag-out drain therefore saw ZERO events while an identical manual call saw
dozens, and it failed silently because zero events is a legitimate state.

Fixed with one defensive parser (adPayload) that accepts either shape, used by the watch lookup and
the event drain. This class of bug has now bitten twice (desktop_caption, desktop_ui_events), so the
parser is the single place to fix it rather than each call site.

## 1.9.34 — 2026-07-20 — Drag-out diagnostics actually deployed, plus a UIA watch leak fix

Two things, one of them a process failure worth naming. The 1.9.33 diagnostic build never reached
the desktop: the manifest was updated LOCALLY but never pushed, so the manifest URL still served
1.9.32 and bridge_install correctly reinstalled 1.9.32 while reporting "Installed". Every conclusion
drawn from that build's logs was worthless. Lesson: after `release upload`, ALWAYS `repo push` the
manifest and verify the SERVED version before installing (a bare "Installed" proves nothing).

Also fixed a real leak this hunt exposed: pup subscribed to the global window-opened UIA watch on
every bridge start and never unsubscribed, accumulating watches (w1..w4 observed live). It now
reuses an existing global window-opened watch when one is present, since all events land in one
shared ring drained by sequence number.

Carries the drag-out diagnostics: every step of detection now logs (drain counts, per-tab window
ids, adoption or the reason none happened) so a silent failure cannot hide again.

## 1.9.32 — 2026-07-20 — Fix: the drag-out event was filtered away by its own (empty) name

1.9.31 subscribed to window-opened correctly but filtered the events by window NAME
(/Chrome|Chromium/). A window fires window-opened at CREATION, before its title exists, so the
dragged-out Chrome window arrived with an EMPTY name and the one event that mattered was discarded
(verified in the live ring: the needed event was seq 32, name ""). The filter is gone: any
window-opened triggers the follow-up, which is cheap and idempotent anyway. Also drains on
pup_list_tabs. Confirmed the underlying detection is sound: after John's drag, the dragged tab
reported window origin 335,292 while its siblings shared 0,0.

## 1.9.31 — 2026-07-20 — Drag a tab out and it becomes its own pup window (event-driven, no polling)

John dragged a tab out of a pup window: Chrome made a NEW OS window, which carried none of pup's
identity (bare Chrome-for-Testing icon), and because the dragged page keeps its "(session: <id>)"
title suffix, ONE session suddenly spanned TWO windows, making AD's title-based resolution
ambiguous.

Detection is EVENT-DRIVEN, not a scan (John: scanning every second is wasteful). AD exposes a global
UIA subscription, desktop_ui_watch {event:"window-opened"}, whose events land in a 500-deep ring
drained with desktop_ui_events {sinceSeq}. pup subscribes once at boot and drains on activity it is
already doing (the 30s health tick plus every tab verb), so there is no dedicated timer: instant
while you are working, one tick when idle. Only Chrome-class window events trigger the follow-up,
and the follow-up is one cheap CDP Browser.getWindowForTarget per tab of multi-tab sessions.

Policy (option A): a tab that no longer shares its session's window is ADOPTED as its own session,
with its own sessionId, identity, CATEGORY icon, taskbar overlay and jump list. That keeps "one
session = one window" true, keeps window titles unique, and means a dragged-out digikey tab
correctly shows the globe instead of inheriting the wiki book.

## 1.9.30 — 2026-07-20 — Fix the raise that reported success while the window stayed hidden

Root cause of "the toggle closed my window and never brought it back". osRaiseSessionWindow resolved
the browser PID as `_detachedPid || process().pid`, so for a DETACHED launch (every Windows pup
browser) it ran PowerShell SetForegroundWindow against the LAUNCHER pid's MainWindowHandle, which is
a different window than the session's. PowerShell dutifully reported "focused" each time while the
real window stayed hidden and unfindable. The proven pup_raise_os_window handler resolves the pid
as `process()` ONLY, which is null when detached, so it SKIPS PowerShell and relies on the CDP
place+maximize+bringToFront that actually works. The helper now matches it exactly. Verified live:
the same window that ignored seven in-code raises came back instantly via the verb's path.

## 1.9.29 — 2026-07-20 — CRITICAL: the wiki toggle destroyed tabs and could leave NO window

John clicked the jump-list login toggle on a 5-tab research window. The window closed, the captions
fired, and nothing came back. Two separate bugs, both severe:

1. TAB LOSS. The toggle relaunched only the ACTIVE tab's url and closed the rest, silently
   destroying every other tab in the window. It now snapshots ALL tab urls plus which one was
   active BEFORE teardown, relaunches on the active url, restores the siblings in order, and
   re-activates the tab the user was on.
2. NO WINDOW RETURNED. The relaunch succeeded (session alive, page at 0,0, logged in) but the
   window never became visible to the shell: the identity stamp logged "No visible window with
   title containing (session: ...)" and the user was left with nothing at all. The toggle now
   VERIFIES the window is findable after the raise, retries heal+raise with backoff, and if it
   still cannot be seen says so in an on-screen caption instead of reporting success.

A switch that cannot show its window is a failed switch. Reporting ok:true while the user stares at
an empty desktop is the worst possible outcome and is what this release exists to prevent.

## 1.9.28 — 2026-07-20 — Category re-stamp retries, and stamp failures are loud

The 1.9.27 acid test flipped the AUMID to .mixed but the window stamp silently returned false once
and the tile stayed latched (the category path stamped exactly once, unlike the auth path which
retries). Stamp failures now log their verdict (response error or not-applied raw), and the
category-change re-stamp retries with backoff like the auth path.

## 1.9.27 — 2026-07-20 — Live tile repaints via category-carrying appIds (AD's measured fix)

AD measured every repaint mechanism on Win11 (WIN11_TASKBAR_ICON.md Part 7): the taskbar tile is
baked when the button is created and never re-read - WM_SETICON, DeleteTab/AddTab, SHChangeNotify,
FRAMECHANGED, RedrawWindow and the TaskbarCreated broadcast all leave it unchanged. The only live
repaint is making the shell create a NEW button, which a DIFFERENT appId does (~1s, no flicker).
So the per-session appId now carries the category: Adom.Pup.<sid>.<category>. A category flip is
register-new, stamp, unregister-old; the jump list re-attaches to the new AUMID automatically.
Registrations are keyed by the full appId (a Set - state is baked into the id). Session teardown
captures the current appId before the session record is deleted so cleanup cannot leak. Tradeoff,
by Windows design: pins bind to one AUMID, so pins do not follow category flips on per-session
windows; grouped mode keeps the stable pinnable Adom.Pup.

## 1.9.26 — 2026-07-20 — Signed-in icon applies on the FIRST stamp (Windows latches button icons)

Proven live: ex-wikiin stamped and registered icon=wiki-in, yet its taskbar button kept the plain
book. Windows latches a button's icon at window creation; later in-place re-stamps often do not
repaint. Fix: a session launching on the authed wiki profile is authed-intent, so _wikiAuthed is
seeded at session creation, BEFORE the first identity stamp - the first icon is already
book-plus-person. The post-open auth probe still corrects the flag and tab glyph if the ~30-day
session expired. Remaining known gap (AD ask): a category flip on a LIVE window (wiki to mixed by
adding a tab) re-stamps correctly but the button may hold the old icon until the window recreates;
needs an AD-side force-refresh of the taskbar tile.

## 1.9.25 — 2026-07-20 — Signed-in wiki icon is now unmistakably different (book + person)

John called it: the outline-vs-solid book distinction technically existed but read identical at
taskbar size, which is the same as not existing. The signed-in wiki icon is now the book plus a
SOLID PERSON BUST in the lower content area (a signed-in reader) - a genuinely different silhouette
that survives 24px and 16px. Logged-out stays the plain book. Lesson recorded: a fill change alone
is not a state indicator at taskbar sizes; change the silhouette.

## 1.9.24 — 2026-07-20 — Category icons v3: solid window body, knockout glyphs, real margins

John: the content glyph was nearly touching the frame edges (no margins, bad graphic design), so
make the browser window SOLID and put the line drawings inside it. Done: the window body is solid
dark teal with a teal toolbar separator, the category glyph is a teal knockout sized to about 55
percent of the content area with two-plus grid units of margin on every side. Reads clearly at 16px.

## 1.9.23 — 2026-07-20 — Category icons v2: browser window first, category as its content

John's design correction, and the right semantic: pup windows ARE Chrome windows, so every category
icon now shares the same browser-window line drawing (frame + toolbar, the 80%) and the CATEGORY is
what is inside the window (the 20%): open book = wiki public, solid book = wiki signed in, the ADOM
MARK = an Adom app (the terminal glyph made no sense for apps), globe = web, two overlapping panes =
mixed. Dark teal glyphs on the teal tile, 24x24 house grid, BMP frames.

## 1.9.22 — 2026-07-20 — Category icon updates the moment the category changes

Adding a digikey tab to a wiki window should flip its icon to "mixed" immediately, but the base
icon re-stamp sat behind the 10-minute brand debounce, so category flips lagged. updateTabCountBadge
(which already runs on every tab open/close/switch/navigate) now compares the window's category to
the last stamped one and re-stamps at once when it changed. The debounce still suppresses redundant
same-icon re-stamps.

## 1.9.21 — 2026-07-20 — Dark teal glyphs (family-consistent) + the white-icon disease was Explorer's cache

Two things. (1) John: the category glyphs should be DARK TEAL, not white - the original pup tile
draws its Adom mark and browser glyph in #003d40 on the teal, so white broke the family. All five
regenerated with #003d40 glyphs (BMP frames kept). (2) The white-taskbar-icon mystery from 1.9.19
and 1.9.20 was neither the ICO format nor pup: after the AD downgrade/upgrade thrash, EXPLORER's
own icon cache was poisoned - every icon change reported success while the shell painted its white
fallback, even for the old proven icon with no relaunch set. An Explorer restart rebuilds the cache.
Diagnostic for the future: when stamps report applied but the button paints the generic white page,
suspect the SHELL's cache, not your code - isolate by stamping the known-good icon; if that is white
too, restart Explorer.

## 1.9.20 — 2026-07-20 — Category icons paint now: ICO frames must be BMP, not PNG

The 1.9.19 category icons applied cleanly (AD reported the stamp and registration OK) but the
taskbar painted the generic white-page fallback. Root cause: the ICOs were written with
PNG-compressed frames (PIL's default), and the shell path that resolves a window's
RelaunchIconResource cannot parse them, so it fell back. AD's own HICON loading handles PNG frames,
which is why every API response said success while the button stayed white. Regenerated all five
category icons with classic BMP frames (bitmap_format="bmp"; pure-BMP verified, no PNG signatures)
under new filenames per the icon-cache rule. The old hand-built adom-pup-browser3.ico that always
painted is BMP-frame, which corroborates the diagnosis.

## 1.9.19 — 2026-07-20 — One session one window, category icons, and no emoji anywhere

Three things, in the order John asked for them.

1. ONE SESSION = ONE OS WINDOW. browser.newPage() opens a TAB in whatever window a profile already
   has, so two sessions sharing the adom-wiki-authed jar collapsed into a single window. A window
   title reflects only its ACTIVE tab, so AD could not resolve "the window titled (session: <id>)"
   for the background one, and every per-window operation keyed on that title silently failed:
   icon stamp, taskbar overlay, flash, jump list. That is why the login icon would not repaint
   after a view toggle. Sessions now get their own window via CDP Target.createTarget
   {newWindow:true}, same cookie jar, separate window.

2. CATEGORY ICONS. A pup window's icon now says what KIND of thing it is showing. Each is the teal
   Adom tile carrying one monochrome #e6edf3 glyph on the 24x24 house grid, per the brand icon law
   (custom hand-drawn, single colour, no gradients, no shadows, no emoji):
     wiki      open book, outline    Adom wiki, public
     wiki-in   open book, SOLID      Adom wiki, signed in
     app       window + prompt       an Adom app (localhost or a cloud slug proxy URL)
     web       globe                 anything else on the internet
     mixed     stacked windows       tabs spanning more than one category
   Solid vs hollow is the one difference that survives Windows painting the button at 24px; a
   padlock turned to mush there. Mixed content gets its own icon rather than picking a winner.

3. NO EMOJI. The brand rule is absolute ("no emoji, anywhere"), and the bridge was full of them.
   Tab titles now lead with a monochrome geometric TEXT glyph, U+25CF solid for signed in and
   U+25CB hollow for public, mirroring the icons. Jump-list titles dropped their emoji entirely
   (the task already carries a real icon), and captions are plain words. The title strip regex
   clears both the new glyphs and legacy emoji so old windows heal instead of stacking prefixes.

Also reverted the green overlay plate from 1.9.16 to 1.9.18: the category icon carries login state
now, so tinting the favicon plate was a redundant and off-law second colour signal.

## 1.9.18 — 2026-07-20 — Fix: the green plate never painted (overlay ran before auth was known)

The 1.9.16/1.9.17 plate colour was correct but never appeared. Ordering bug in pup_open_window:
applyAppOverlay paints the overlay EARLY, and `session._wikiAuthed` is only assigned a few lines
LATER from the auth probe - so the plate was always composited in the logged-out colour, even for a
window opened with wikiView:"authed". (The log tell: the per-session AUMID re-registers with
icon=auth AFTER the stamps, proving the state landed late.) Now the overlay is repainted once the
auth state is recorded.

## 1.9.17 — 2026-07-20 — Use the Adom brand green (#3fb950), not an ad-hoc one

John caught it: the signed-in green was #16a34a, which is Tailwind's green-600, not an Adom colour.
The design system defines `--green: #3fb950` and reserves it for "success only" - which is exactly
what "signed in" is - so both the overlay plate and the main-icon glyph now use that token. Nothing
else about the indicator changed. (The logged-out plate stays #0b2325, the pre-existing dark plate.)

## 1.9.16 — 2026-07-20 — Wiki login state = the favicon's PLATE COLOUR (John's idea, the right one)

John: "why don't you just use a different background than the background you place onto the favicon
in the overlay?" Exactly right, and better than everything I tried. The overlay already composites
each site favicon onto a dark rounded plate (#0b2325), so that backdrop was free real estate:
  dark plate  = logged out / public / any non-Adom site (unchanged)
  GREEN plate = signed in to the Adom wiki
The favicon KEEPS the slot - you still see which site the active tab is on - and the plate is a far
bigger colour target at 16px than any glyph tucked into the 24px base icon. Cache key now includes
the plate state, the tooltip says "Adom wiki: SIGNED IN", and a login-state flip repaints the
overlay. .ico favicons (which sharp cannot decode) fall back to drawing the green plate alone
rather than losing the signal.

The 1.9.14 main-icon variant (solid green browser glyph) stays as a second, redundant signal for
Alt-Tab and the flyout header, where taskbar overlays are not painted.

## 1.9.15 — 2026-07-20 — Retry the icon re-stamp (the flip silently lost a race)

1.9.14 tracked login state correctly but the taskbar icon did not visibly change. Cause: after a
view toggle the window is BRAND NEW, and AD resolves it by the "(session: <id>)" title suffix that
pup injects only once the relaunched page loads. The single immediate re-stamp fired first and AD
answered "No visible window with title containing (session: ...)" - visible in the log as a
[badge] FAILED with no matching [identity] stamped line. Now retried with backoff until the title
exists, and the failure is logged loudly instead of passing silently.

## 1.9.14 — 2026-07-19 — Login state on the MAIN icon; overlay stays favicons (reverted 1.9.13)

REVERTED 1.9.13's overlay-slot padlock. John was explicit: the overlay is for FAVICONS ("i LOVE
seeing those favicons in that overlay") and the login indicator belongs on the MAIN icon. That was
his instruction from the start and I should not have moved it. The overlay is favicon-only again,
exactly as it was in 1.9.0.

The main-icon indicator is now what he actually asked for - the lower-right browser-window glyph
FILLED IN SOLID when signed in - with one deviation for legibility: it fills with signed-in GREEN
rather than dark. Rendered at true 24px (what Windows actually paints), a dark fill blended into
the glyph's own dark stroke; the colour flip is what survives downscaling. Geometry is identical
between the two icons so they register perfectly and only the fill changes.
  adom-pup-browser3.ico      - browser-window glyph, teal/hollow = logged OUT / non-wiki window
  adom-pup-wiki-authed2.ico  - same glyph FILLED SOLID GREEN     = signed in to the Adom wiki
Also dropped the 1.9.12 padlock variant: changing SHAPE is exactly the detail that dies at 24px.

## 1.9.13 — 2026-07-19 — Login state moves to the OVERLAY slot (readable at real taskbar size)

Honest follow-up to 1.9.12: the base-icon corner glyph (browser outline vs padlock) works
mechanically, but a full-res capture of the real taskbar showed pup buttons render at ~24px, so
that corner glyph is only ~7px — too small to be the "good human indicator" John asked for.
Moved the signal to the OVERLAY slot, which Windows paints ~16px bottom-right and designs for
exactly this:
  GREEN plate + closed padlock = signed in to the Adom wiki
  SLATE plate + open padlock   = logged out / public view
Colour carries it at 16px where shape cannot. The slot is free on wiki windows because the favicon
that normally rides there is the ADOM LOGO — redundant with the pup base icon, which is already
Adom. NON-Adom sites (ti.com, digikey) keep their favicon, which genuinely says where you are.
The 1.9.12 base-icon swap stays as a second signal for Alt-Tab and the flyout header, where
overlays are not painted. Tooltip spells it out: "Adom wiki: SIGNED IN / public - logged out".

## 1.9.12 — 2026-07-19 — The taskbar ICON now shows Adom-wiki login state

John: "can you show an indicator in the pup icon whether the adom wiki is logged in or not? maybe
that browser window icon gets filled in solid". Done, as a MAIN-ICON swap (the overlay slot stays
the active tab's favicon, which is more useful):
  adom-pup-browser3.ico     — browser-window OUTLINE glyph = logged OUT / non-wiki window
  adom-pup-wiki-authed1.ico — SOLID PADLOCK glyph          = signed in to the Adom wiki
Same art otherwise, only the lower-right corner glyph differs. Solid-vs-outline is deliberate: a
mass/contrast change is what survives downscaling to 16-32px (fine shape detail does not), and the
padlock carries the meaning. Rendered from SVG at 16/24/32/48/64/128/256.

Plumbing: session._wikiAuthed tracks the state and setWikiAuthedState() re-stamps the window
identity AND re-registers the per-session AUMID when it flips (so Alt-Tab and the flyout header
follow too). Two cache traps handled — the identity cacheKey now includes the state, and the AUMID
registry is a Map of appId->state instead of a Set, because both cache by key/path and would
otherwise pin the stale tile. State is set on open, on the view toggle (both directions), and on
the navigate auth re-check, so a window that logs in mid-session updates its icon on its own.

## 1.9.11 — 2026-07-19 — OS captions live, jump-list header branded, in-page toast retired

AD shipped all four asks; pup now consumes them.
- **desktop_caption on the loopback API (AD 1.9.144).** The earlier silent no-op was a wider AD hole
  than "relay-only": ~90 inline verbs matched only their BARE names, and the CLI strips the
  `desktop_` prefix client-side — which is exactly why a CLI test passed while pup's identical call
  did nothing. Both spellings work now. Fixed pup's arg name too: it is `expiresInMs`, NOT `duration`.
- **In-page toast RETIRED.** With a real OS surface available, pup should not inject DOM into the
  user's page. Captions are also the only feedback that survives the window teardown mid-switch.
- **Jump-list header icon (AD 1.9.141).** Not a registry fix — there is no ApplicationIcon value on
  AUMID keys and the registry cannot brand the header at all. The header obeys the window's Relaunch*
  TRIO, and Windows ignores RelaunchIconResource unless RelaunchCommand is ALSO set. stampPupIdentity
  now passes relaunchCommand (a real `pup_focus_window` for that session, carrying a
  foregroundReason since the raise gate demands one, with Windows-argv-escaped quotes).
- **Jump-list cmd flash (AD 1.9.143)** needed no pup change (AD interposes a GUI-subsystem shim);
  pup re-sets jump lists at every open/navigate, so pre-1.9.143 lists heal automatically.
- **Log rotation:** answered, bounded. Per-spawn `<name>.log` truncates on respawn; the appended
  `<name>.ad.log` is hard-capped (trimmed to newest 64 KB at 128 KB). No janitor needed on pup's side.

## 1.9.10 — 2026-07-19 — In-page toast feedback (desktop_caption is relay-only) + branding race fix

Root-caused why the 1.9.9 captions never appeared: AD's loopback direct API — the path bridges use
via chrome.adCommand — does NOT expose desktop_caption (confirmed live: "'desktop_caption' is not a
built-in desktop verb"; the relay path has it, which is why a CLI test showed one). Filed with AD.
pup now renders its own IN-PAGE toast (wikiToast — a fixed-position Adom-teal pill injected into the
page, auto-fade): "Switching…" into the old page at click time, and the result ("🔓 Adom wiki: now
the LOGGED-IN view (John Lauer)" / "📖 …now the PUBLIC (logged-out) view") into the relaunched page
right where the user is looking. The AD caption call stays (now logged, not silent) and lights up
the moment AD ships direct-API captions. Also fixed the [badge] FAILED race: the toggle branded the
window before the title suffix existed — branding now waits 1.5s like every other path.

## 1.9.9 — 2026-07-19 — FIX: clicking the jump-list task did nothing (Windows argv quoting)

THE bug that made the whole feature look broken. The jump-list task line is parsed by the CLI's
argv parser, which STRIPS unescaped double quotes — so the raw JSON args arrived as
{sessionId:wiki-demo,view:authed} and the CLI died instantly with "Invalid JSON args: key must be a
string at line 1 column 2" (exit 1). The user saw a 50ms cmd flash and nothing else. Reproduced and
fixed on the real command line: the JSON is now wrapped in quotes with inner quotes
backslash-escaped so argv delivers it intact as one argument.

Also added an on-screen AD caption (desktop_caption) so a click gives INSTANT feedback during the
few-second relaunch: "🔓 Switching to the LOGGED-IN view of the Adom wiki…" on click, replaced on
completion by "🔓 Adom wiki: now the LOGGED-IN view (<user>)" / "📖 …now the PUBLIC (logged-out)
view", or a prompt to do the one-time sign-in.

TESTING LESSON (baked into pup-bridge-debug): I had only tested the VERB directly via the CLI,
which bypasses the exact path a click takes. The bug lived entirely in that untested segment. Test
the ACTUAL entry point the user hits, not a convenient proxy for it.

## 1.9.8 — 2026-07-19 — Wiki toggle: snappy click, non-blocking raise, self-correcting glyph

Follow-up to 1.9.7's foreground raise, from ralph-testing it live. Three fixes: (1) the OS raise now
uses async PowerShell (runPsHiddenPromise) instead of sync — the sync call blocked pup's whole event
loop for up to 8s, stalling every other request. (2) pup_wiki_set_view now RESPONDS the instant
the window is raised and runs branding/overlay/jump-list/auth-recheck in the BACKGROUND, so a
jump-list click doesn't wait ~30s on cosmetics (that stall was likely why the switch felt like it
"never worked"). (3) The tab glyph is set optimistically on switch and then self-corrects via a
delayed auth re-check — fixes a logged-in window (/auth/me=200) briefly mislabeling "📖 Public"
because the first auth check raced the page load.

## 1.9.7 — 2026-07-19 — Wiki toggle now REALLY pops to the foreground when you click it

The taskbar wiki view-toggle worked functionally (verified: it relaunches the window under the
authed profile, /auth/me=200, tab title flips to "🔓 Logged in") but the user couldn't SEE it: the
callback only did a CDP maximize, so the relaunched window came back behind whatever they were
looking at. Now pup_wiki_set_view does a real OS SetForegroundWindow raise (extracted into a
reusable osRaiseSessionWindow helper from the proven pup_raise_os_window path — CDP place +
clear WS_EX_NOACTIVATE + SetForegroundWindow, with a WMI child-PID fallback) plus a taskbar flash
in case Windows' foreground lock denies the raise. Clicking the jump-list task now unmistakably
brings the switched window to the front. (The ~50ms cmd flash on click is AD's console-subsystem
CLI launcher — tracked separately for an AD CREATE_NO_WINDOW fix.)

## 1.9.6 — 2026-07-19 — Kill the ~60s respawn loop (crash-poison recovery) + jump-list task icon

Two fixes. (1) THE BIG ONE: pup was respawning every ~60s for hours (AD's lifecycle log showed
spawn-with-no-reap = pup self-crashing, not AD killing it). Root cause: recoverSessions() reconnects
to Chromes from ~/.adom/pup-sessions/*.json on boot; a NATIVE crash during recovery (puppeteer.connect
/ a CDP call) kills the process before the per-session try/catch can delete the file, so the same
poison file re-crashes every boot forever (in-process try/catch and the uncaughtException handler
can't stop a native crash). Fix: a persistent circuit breaker — a recover-attempt marker is written
BEFORE the risky ops; a file that still carries an incomplete attempt next boot is quarantined
(deleted), a clean recovery clears it. One strike quarantines, so any poison file self-heals in a
single crash. (2) The taskbar jump-list toggle now carries the Adom Pup icon (iconPath) instead of
Windows' generic blank-document glyph.

## 1.9.5 — 2026-07-19 — Name the Adom wiki in the jump-list toggle (was too generic)

The taskbar toggle read "🔓 Switch to logged-in view" — generic enough to look like it'd apply to
any site (John's catch). Now it leads with the wiki: "🔓 Adom wiki: switch to logged-in view" /
"📖 Adom wiki: switch to public view", with descriptions that spell out logged-in = private
source/drafts/owner cards vs public = what the world sees. Self-describing at a glance.

## 1.9.4 — 2026-07-19 — Fix the concurrent-launch wedge + liveness/recovery hints

Root-caused a live cold-start wedge: after a bridge restart, two near-simultaneous
pup_open_window calls for the SAME profile each spawned their own detached Chrome onto the
same --user-data-dir, collided on Chrome's SingletonLock, and mutually taskkilled each other's
process — so NONE came up and the bridge looked hung for minutes (verbs timed out, AD LED dim).
Fix: `_launchInFlight` dedup — the first launch per profile registers a Promise and concurrent
callers JOIN it instead of racing a second Chrome. Also added AI-facing recovery hints so other
threads don't relearn this: pup_readiness now explains that a status probe does NOT respawn a
down bridge (only a window/tab verb does) and warns against spamming opens during "bridge_starting";
the pup SKILL and dev-skill gained a liveness/restart/wedge troubleshooting section (the AD LED +
browser.bridgeRunning are truth; bridge_list status:"running" is not liveness).

## 1.9.3 — 2026-07-19 — Per-window wiki view toggle in the taskbar jump list (#208 shipped)

AD 1.9.139+ shipped #208 (desktop_set_window_jumplist). Every WIKI pup window now carries a
taskbar RIGHT-CLICK task to flip between the logged-in and public view — "🔓 Switch to logged-in
view" on a public window, "📖 Switch to public view" on an authed one. The jump list attaches to
the per-session AUMID (registry-only, from 1.9.2), so it's a PER-WINDOW menu. Clicking it runs
AD's bundled CLI → the new pup_wiki_set_view verb, which relaunches THAT window under the
other cookie jar (profile = userDataDir is fixed at launch, so a view switch is a relaunch) and
brings it on-screen (a user click, so foregrounding is warranted). The menu is added/cleared on
open and on every navigation (drops off when a window leaves Adom); split mode only (grouped mode
shares one button); self-gates on older AD. If the authed jar hasn't done the one-time SSO yet,
the response flags wikiLoginNeeded and the user logs in once in that window.

## 1.9.2 — 2026-07-19 — Register per-session AUMIDs (#207 shipped): Adom icon in alt-tab + jumplist

AD 1.9.136+ shipped #207 (desktop_register_app_identity {shortcut:false} + unregister). pup now
REGISTERS each split-mode per-session AUMID (Adom.Pup.<sessionId>) registry-only at window create,
so Alt-Tab and the taskbar right-click jumplist header resolve the Adom icon/name instead of
falling back to the chrome.exe icon. Unregistered on session close (no registry cruft, no Start
Menu spam). Self-gates on older AD. The base "Adom.Pup" stays the shortcut-registered pinnable
launcher; grouped mode already uses it.

## 1.9.1 — 2026-07-19 — Wiki mode label is now WORDS, and updates after login

John saw a bare 📖 emoji and had no idea what it meant; the logged-in window showed nothing at
all (glyph checked auth at open, before login completed). Fixed: the tab title now leads with a
clear WORD label — "🔓 Logged in · <title>" or "📖 Public · <title>" — and it re-checks auth +
refreshes on every Adom navigation, so a window that logs in mid-session flips to "🔓 Logged in".

## 1.9.0 — 2026-07-19 — Favicons win the overlay (John: more useful than counts)

The taskbar overlay now shows the ACTIVE tab's site favicon for ANY tab count — you always see
what the window is showing (ti.com, mouser, …). The tab count moves to the hover tooltip
("Adom Pup — 3 tabs (icon = active tab)"). Overlay follows tab switches (updates on
pup_switch_tab). Fallback to a plain count badge only when the active page serves no favicon.
Grouped-mode dual counter unchanged.

## 1.8.99 — 2026-07-19 — Auth gating is Adom-wide; overlay = site favicon (1 tab) or count (2+)

- Authed treatment now gates on ANY adom.inc host (isAdomUrl), not just wiki.adom.inc — other
  Adom properties get the logged-in view + hints too. NON-Adom sites (ti.com, digikey.com,
  adom.cloud proxies) get NOTHING: no auth routing, hints, or glyph.
- Overlay rule resolved (John's 3-vendor-tabs question): 1 tab → wear THAT site's favicon on a
  dark plate (you see it's ti.com); 2+ tabs → show the tab COUNT (no single favicon represents a
  multi-site window; hover tooltip carries the detail). No stacking — the 32px slot can't.

## 1.8.98 — 2026-07-19 — Profiles live OUTSIDE the bridge cache (persistence actually persists)

Root-cause fix behind "the wiki login didn't survive": PROFILES_DIR/SESSIONS_DIR were inside the
bridge dir (bridges-cache\puppeteer), which AD CLOBBERS on every bridge_install — so every pup
update wiped every persistent profile, including the wiki-auth session. Moved both to the stable
~/.adom zone (pup-profiles / pup-sessions) with a one-time best-effort migration. Now "log in
once" genuinely lasts across updates, restarts, and reboots — for the wiki-auth vault and any
user profile. (The dev-skill "persistent profile = automatic 30-day storage" claim was only true
once this landed; a restart_bridge never clobbered, a bridge_install always did, which masked it.)

## 1.8.97 — 2026-07-19 — Gentle in-tab wiki mode indicator

The AI picks the wiki view, so the user needs to SEE which one. A leading glyph on the TAB TITLE
shows it: 🔓 = logged-in view, 📖 = public/anonymous. (The native tab-strip empty space isn't
reachable by the bridge; the tab title is, and it's where the user's already looking.) Idempotent
+ MutationObserver so the SPA can't wipe it; doesn't fight the session-suffix injector.

## 1.8.96 — 2026-07-19 — Wiki authed: shared login across ALL threads, own window each; sharper hints

Proven live: a brand-new thread (own sessionId) opening wikiView:"authed" is authenticated by
reusing the one-time login — each thread gets its OWN window, they share only the vault cookie jar.
- Routing fix: wikiView:"authed" now ALWAYS routes to the reserved adom-wiki-authed vault profile
  (previously only when the thread passed no profile of its own) — so the single login serves
  every thread regardless of what profile they'd otherwise use.
- Hints rewritten around the 80% case: PUBLIC is the default (logged out, what the world sees);
  wikiView:"authed" is the opt-in logged-in view; a thread can't self-login (it's the user's SSO)
  but ONE login unlocks authed for EVERY thread for ~30 days. Survives bridge restarts/reboots.

## 1.8.95 — 2026-07-19 — Suppress the browser sign-in / work-profile prompt (CfT DOES have it)

Correction to a load-bearing wrong assumption: Chrome for Testing shows the same "Sign in to
Chromium? Set up a work profile" dialog that poisoned Aditya — it is NOT immune (proven live).
pup now launches CfT with browser sign-in and sync DISABLED
(--allow-browser-signin=false --disable-sync --disable-features=SigninIntercept,
DiceWebSigninInterception,ProfilePicker,ForceSignInReauth,SyncPromoAfterSignin --disable-signin-promo),
so the dialog never appears for any user. Site logins (e.g. the wiki session cookie) are plain
cookies and unaffected. The auth-URL steering hint stays as backup. Dev skill corrected.

## 1.8.94 — 2026-07-19 — Wiki dual-view: single-window, hint-only (no two-window nag)

Reverted the "show BOTH views" push — double-reloading the same page in two windows is annoying
for the reload-100x loop. Now ONE window is normal; hints just make the AI AWARE it can switch
this window to wikiView:"authed" (or log the user in once) when a task needs the logged-in angle.
The deliberate public-vs-private visibility check flips the SAME window between views.

## 1.8.93 — 2026-07-19 — Wiki dual-view: public (default) + logged-in (wikiView:"authed")

pup can now open a wiki.adom.inc page from EITHER viewpoint, and hints teach the AI to show BOTH
so the user can verify publish visibility ("did private source leak to the public?").
- Default open = PUBLIC/anonymous (fresh profile, logged out — what the world sees).
- `pup_open_window {url, wikiView:"authed"}` routes to a reserved PERSISTENT profile whose
  on-disk cookie jar holds the 30-day SSO session. First use returns `wikiLoginNeeded:true` with
  a hint to raise the window for a one-time hydrogen.adom.inc SSO; thereafter it stays logged in.
- No token→cookie shortcut exists (wiki auth is cookie-based SSO; the container CLI bearer is a
  separate credential) — the persistent profile IS the mechanism, so no manual cookie
  capture/cache/reinject is needed; the 30-day cookie lives in the profile and re-auth only fires
  on real expiry (a logged-out check catches it). Dual-view hints on every wiki open; the
  pup-adom-wiki skill documents the verify-visibility workflow.

## 1.8.92 — 2026-07-19 — App-favicon overlay: a pup window WEARS the app that drives it

John's standard: every Adom app already serves a favicon (<link rel="icon">, monochrome white
mark per adom-ui-design; the canonical webview identity per hd-tab-icons) — so pup now
auto-fetches the loaded page's favicon and wears it in the taskbar overlay, composited onto a
dark rounded plate (white marks need dark backing). "That pup window with the shotlog icon IS my
shotlog thread." Explicit overrides: pup_open_window {appIconB64, appName}. An app-looking
URL (localhost / *.adom.cloud proxy) serving NO favicon gets a 🎨 reprimand hint teaching the
standard (adom-ui-design + app-creator). When an app icon is worn it owns the overlay slot and
the tab count moves to the hover tooltip; split-mode count badges remain for icon-less windows;
grouped mode's dual counter is unchanged. Favicon refreshes on navigate.

## 1.8.90 — 2026-07-18 — Grouped counter: rounded-rect plates

Bare digits were hard to read without a background; rounded rectangles fit digits tighter than
circles so the type stays large. Dark rect / white digit = windows; teal rect / dark digit =
total tabs.

## 1.8.89 — 2026-07-18 — Circle-less counter: windows digit white

The dark windows digit vanished on the dark taskbar (the circles had been doing the contrast
work). Windows count is now WHITE with a dark halo; tabs stays teal. Both bright, still
color-distinguished.

## 1.8.88 — 2026-07-18 — Circle-less grouped counter (trial)

Grouped dual counter drops the circles: bold bare digits (windows dark, tabs teal) with a
hairline contrast halo, ~25% taller than the circled variant allowed.

## 1.8.87 — 2026-07-18 — Grouped dual-counter digits enlarged again

Digits in the grouped top-row counters bumped another ~12% (14px single-digit windows, 13px
single-digit tabs) for taskbar legibility.

## 1.8.86 — 2026-07-18 — Grouped dual counter: both numbers on the top row

Per John: windows (dark) and total tabs (teal) now sit side by side across the TOP of the
overlay instead of diagonally.

## 1.8.85 — 2026-07-18 — Badge digits 15% bigger

Per John: badge numbers bumped ~15% across the static split badges and the grouped dual counter.

## 1.8.84 — 2026-07-18 — Grouped-mode dual counter (windows + total tabs); flat badges

- Grouped mode's single button now shows TWO counters: window count (dark circle, upper-right)
  and total tab count (light teal circle, dark text, lower-left). Rendered at runtime via sharp
  from an SVG template (combos are dynamic), cached per pair; falls back to the static
  windows-count badge if sharp is unavailable. Single-window grouped shows a plain tab count.
- All badge circles are now FLAT — no white outline (per John), split badges regenerated.

## 1.8.83 — 2026-07-18 — Lighter tab-count badge

Per John: the badge circle no longer fills the whole overlay slot — ~60% size anchored
upper-right, hairline translucent outline instead of the heavy white ring. (Overlay icons are
passed as base64, not paths, so no icon-cache filename dance needed for these.)

## 1.8.82 — 2026-07-18 — pup_configure: taskbar grouping is now a user preference

John's insight from the AUMID grouping bug: BOTH behaviors are legitimate preferences. New
`pup_configure {taskbarGrouping: "split"|"grouped"}` (persisted in ~/.adom/pup-settings.json,
survives bridge updates):
- `split` (default): per-session AUMID — one taskbar button per window, each badged with its own
  tab count.
- `grouped`: shared AUMID — all pup windows stack under one "Adom Pup" button whose badge shows
  the WINDOW count.
Switching re-stamps live windows immediately (brand sweep) and refreshes badges. AIs are told to
ask the user before changing it.

## 1.8.81 — 2026-07-18 — Per-session AUMID: one taskbar button per window again

The shared Adom.Pup AUMID made Windows GROUP all pup windows into a single taskbar button —
destroying per-window differentiation and rendering the tab-count overlay wrong (it showed
whichever window last updated). Identity is now stamped as `Adom.Pup.<sessionId>` per window:
each session gets its own button with its own accurate badge. The per-window
RelaunchIconResource/DisplayNameResource (already set by AD's verb) brand each unregistered
per-session AUMID; the registered plain "Adom.Pup" remains the pinnable Start Menu launcher.
The brand sweep re-stamps live windows on deploy, splitting existing groups immediately.

## 1.8.80 — 2026-07-18 — Tab-count overlay badge (differentiate pup windows at a glance)

John: an 8-tab review window and a 1-tab window are indistinguishable teal tiles. The overlay
slot (freed by the identity takeover) now shows the window's TAB COUNT for ≥2 tabs — dark
circle, white digit, 9+ cap, tooltip "Adom Pup — N tabs". No badge = 1 tab, so many-tab windows
pop out. Updated on open_tab / close_tab / popup auto-attach; only on identity-capable AD
(≥1.9.134) — older AD keeps the brand badge in that slot.

## 1.8.79 — 2026-07-18 — Brand sweep: no more mixed icon generations

Identity is per-HWND and stamped at birth, so windows born across rebrands/updates left the
taskbar showing MIXED icon generations (plus dead-session ghosts inflating the count). New
`brandSweep` re-asserts each live session's title suffix (title churn can make a window
un-resolvable) and re-stamps it to the CURRENT identity — at startup (post-reattach) and after
every pup_rescan. Any future icon change now propagates to already-open windows within
seconds of the bridge update instead of never.

## 1.8.78 — 2026-07-18 — AD-version awareness: warn when the raise gate is bypassed

Every AD capability gap self-gates silently (identity → badge, bottom/force → PS fallback) EXCEPT
one: AD <1.9.124 intercepts pup_raise_os_window natively, so pup's foreground gate silently
does not exist there — a policy hole, not cosmetics. pup now probes the AD version at startup and,
on affected boxes: logs a warning, turns the status chip yellow with an update-AD note, exposes
`adVersion` + `raiseGateBypassed` on /status, and appends an upgrade advisory to open-response
hints. Boxes on current AD see nothing.

## 1.8.77 — 2026-07-18 — pup owns its own provisioning: warmup ALWAYS ensures CfT

John: "isn't that your decision? why would you need AD to change anything?" Correct — the
skip-CfT-when-Chrome-exists policy was the dead installed-browser-first doctrine living on in
pup's OWN warmup(). Now warmup (runs at every bridge spawn = AD install/update + machine start)
background-fetches CfT whenever it isn't cached, regardless of installed browsers — so a fresh
box provisions pup's browser at INSTALL time and the first "open in pup" never waits. Low-disk
guard unchanged. AD's job ends at "pup is installed"; no AD policy involved (issue #204
withdrawn — resolved pup-side; AD's v1.9.76 prewarmer is redundant for pup and the install
singleton dedupes if both fire).

## 1.8.76 — 2026-07-18 — Icon glyph tucked into the corner (clear gap from the leaves)

1.8.75's browser glyph touched the dark leaf and blended into it. The glyph is now smaller and
tucked into the lower-right corner with clear teal space on all sides. Ships as
`adom-pup-browser3.ico` (new filename per the icon-cache rule); v2 art removed.

## 1.8.75 — 2026-07-18 — Identity icon simplified per John: line-art browser glyph, no white

1.8.74's plated badge was overdone. The icon is now the favicon with a MINIMAL line-art browser
window in the leaf dark-teal (#003d40) inlaid lower-right — teal fill, dark outline + title-bar
line, zero white. Ships as `adom-pup-browser2.ico` (new filename per the icon-cache rule); the
v1 badge art is removed. Master SVG `adom-pup-browser2.svg`; future home remains the brand-icons
wiki repo.

## 1.8.74 — 2026-07-18 — New pup identity icon: favicon + browser-window inlay

The Adom Pup icon is now the Adom favicon with a browser-window glyph inlaid in the lower-right
(dark plate, white window frame, teal screen) — the icon itself says "this is an Adom browser
window." Ships as `icons/adom-pup-browser.ico` (NEW filename per the icon-cache rule; the old
adom-pup.ico is removed). Master art `adom-pup-browser.svg` lives in src/ for now; future home is
the Adom brand-icons wiki repo so the mark is globally reusable beyond pup.

## 1.8.73 — 2026-07-18 — Adom Pup window identity: full icon/name takeover (AD ≥1.9.134)

pup windows now read "Adom Pup" with the Adom icon in the taskbar, alt-tab, and hover — no Chrome
logo, no CfT "Test" tile. Built on AD 1.9.134's verbs (shipped to pup's spec after the 2026-07-18
prototype):

- **Self-registration at bridge start** (`desktop_register_app_identity`, loopback direct-API):
  HKCU AUMID `Adom.Pup` + a Start Menu "Adom Pup" shortcut whose PIN launches a REAL managed pup
  window via adom-desktop-cli (the pinning trap, solved). The bridge owns its branding: the icon
  ships in this release zip (`icons/adom-pup.ico`; art changes require a NEW filename — Windows
  caches decoded icons by path).
- **Per-window stamp at CREATE** (`desktop_set_window_identity` with `cacheKey`), not just at
  park — identity is per-HWND, and stamping late would leave the window reading "Chrome for
  Testing" through its whole load. Re-stamped at park/heal/tab hot paths.
- **Overlay badge demoted to fallback**: on AD <1.9.134 (unknown verbs → self-gating) the teal
  overlay badge continues exactly as before; on capable AD the full identity replaces it.
  `desktop_taskbar` remains in use for flash/progress.
- Mechanical root (from the prototype): icon HICONs are process-owned USER objects — AD holds
  them in a process-lifetime cache, which is why AD-core is the only correct owner.

## 1.8.72 — 2026-07-18 — THE DEMARCATION GATE: no silent branded fallback, ever

1.8.71's "converge by the second session" still ran the FIRST session on branded Chrome — and the
first session is exactly the Aditya session. Now an unpinned new-window open REQUIRES cached CfT:
if absent, pup kicks the ~150 MB install in the background and returns the pollable
chrome_for_testing_installing (poll pup_readiness, retry) instead of silently driving the
user's browser. The installed Chrome/Edge is reachable ONLY by explicit pup_use pin (hints
carry the identity-bleed warning). Existing/live sessions exempt. Companion AD ask filed
(issue #204): flip prewarm_cft auto→always so install-time prewarm erases the first-use wait.

## 1.8.71 — 2026-07-18 — CfT-first hybrid pick order (#202) + auth-URL steering (the Aditya incident)

- **Pick order flipped to: cached CfT → installed Chrome → Edge → CfT-download-last-resort.**
  Chrome for Testing is the automation-first binary with no profile/sync/sign-in machinery — the
  branded browsers' identity features bleed into pup's "anonymous" windows (the documented
  sign-in → profile-creation → consolidation → lost-user cascade). No first-open stall anywhere:
  a branded-fallback launch kicks a ONE-TIME background CfT install, so every box converges to
  deterministic CfT by its second session. `pup_use` still pins explicitly.
- **Codec caveat retired with evidence:** CfT (real Chrome build) decodes H.264/AAC/MP3/VP9
  ("probably" via canPlayType on a live box) — the gap was legacy Chromium's, not CfT's. Only
  Widevine DRM playback is out of scope.
- **Auth-URL steering:** opening/navigating to a known auth/login URL (Google, Microsoft, Okta,
  Auth0, GitHub, AWS, Apple, Salesforce, Atlassian, OpenAI, generic /oauth2/authorize) returns a
  🔑 hint: pup is the anonymous sandbox — sign the USER in via abe (their real browser) or AD-core
  desktop_open_url (plain native launch) instead; continue in pup only for throwaway accounts.

## 1.8.70 — 2026-07-15 — THE FOREGROUND GATE (abe parity): reason required, recorded, auditable

Real incident: a thread background-opened a window then raised it 2 minutes later; the raise verb
required no reason, recorded nothing, and didn't even log — forensics needed the focus-latch
timestamp. pup now gates foregrounding the way abe does (dual key, one call):

- `pup_open_window {foreground:true}` WITHOUT `foregroundReason` → the open succeeds but the
  window stays BACKGROUND, with `foregroundDenied:'foreground_reason_required'` + a hint (abe
  semantics: withhold the foregrounding, not the work). With a reason (min 10 chars): granted.
  Also accepts abe's arg name `userRequestedForeground`.
- `pup_raise_os_window`/`pup_focus_window` WITHOUT `foregroundReason` → REFUSED with
  `errorCode: foreground_reason_required` and the contract in the hint.
- Every granted foreground is logged (`[foreground] ... GRANTED — reason: ...`), recorded in
  `lastAgentUpdate` (`FOREGROUND: <reason>`), and surfaced as `lastForeground {ts, verb, reason}`
  in session status — "who put this window on my screen and why" is now one query.
- pup_describe teaches the whole contract, including return-to-background after the show-me.

## 1.8.69 — 2026-07-15 — Sized opens keep their x/y (multi-monitor placement works); window-management granted

- The sized-open background re-assert was still the LEGACY PS park, which moves the window to 0,0
  sized-to-monitor — silently clobbering the caller's x/y/w/h (every sized open snapped back to
  the primary monitor's origin, making second-monitor placement impossible). It is now AD
  `state:'bottom' force:false` — z-order only, geometry untouched, user-held windows respected.
- `window-management` added to the permissive default grants so `getScreenDetails()` works inside
  pup pages (it used to hang forever on a CDP-suppressed permission prompt) — pup can now
  enumerate the REAL monitor layout from Chrome itself. Chrome sees monitors AD's
  `desktop_list_monitors` misses (ADOMGPU reports 1 monitor via AD but isExtended:true in Chrome;
  AD gap to file).

## 1.8.68 — 2026-07-15 — Park AFTER the page settles: the paint-raise happens invisibly off-screen

Even with 1.8.67's re-asserts, the raise-then-re-bottom left a visible blip: another thread's
window (adom/wiki) briefly popped over John's work between its first paint and the re-assert.
Root: the park placed the window on-screen BEFORE the navigation settled, so the paint-raise was
visible. Now launchSession waits for the page to settle (load event, capped 2.5s, skipped if
already complete) while the window is still OFF-SCREEN, and only then parks (bottom → place →
z-confirm). The first paint's raise hits an invisible off-screen window; the window's first
visible frame is full-size, z-bottomed, z-confirmed. One late re-assert (3s) remains for
late paints. The open response still awaits the full verdict.

## 1.8.67 — 2026-07-15 — Re-assert bottom after the open's navigation (the post-park raise)

Even with 1.8.66's z-confirmed park, the w3 batch sat on top with honest 'backgrounded' verdicts:
the open's FIRST cross-origin nav (file:// welcome → https) swaps renderer processes and Chromium
re-shows the window at first paint — raising it — and heavy sites paint AFTER the park's z-confirm
passed (the real goto runs concurrently with the park). A later same-window navigate does NOT
raise (tested), and neither does the badge (tested). Fix: re-assert z-bottom right after the
open's goto AND once more at the page 'load' event, plus after open_tab's new-tab first paint.
All re-asserts use force:false so a window the user is actively holding foreground is never
yanked. Verified: bottoming sticks once the page settles (8s watch, no spontaneous re-raise).

## 1.8.66 — 2026-07-15 — PLACEMENT DOCTRINE: verify the z-order, revert if unconfirmed

1.8.65's "fix" was worse: AD desktop_set_window_bounds ALSO raises the window (despite
restore:false), and desktop_list_monitors reports logical px while set_window_bounds takes
physical — five windows on top of the user's work AND undersized. Placement is rebuilt around
distrust:

- Geometry via CDP plain bounds only (left/top/width/height, normal state), sized from the page's
  own screen.availWidth/Height with retries. No CDP maximize (raises), no AD bounds (raises).
- After placement: RE-BOTTOM via AD state:bottom force, then CONFIRM THE Z-ORDER via AD
  desktop_list_windows — pup's window must be strictly below at least one non-pup window and not
  topmost. Retried once; if still unconfirmed, the window is moved BACK OFF-SCREEN and the verdict
  is park-failed. A window that might be covering the user never stays on-screen.

## 1.8.65 — 2026-07-15 — NEVER use CDP maximize: it RAISES the window (the covering-windows breach)

John caught five "backgrounded" windows stacked at z-1..3 directly under his editor — covering his
work. Root cause, latent since 1.8.55: CDP `Browser.setWindowBounds {windowState:'maximized'}` is
ShowWindow(SW_MAXIMIZE) in Chromium, which RAISES the window to the top of the z-order (no focus
steal, so every focus-based check passed while the windows sat on top). The earlier
"small-then-grow" sighting was the same raise making the staging visible.

- Placement is now AD `desktop_set_window_bounds {restore:false}` (MoveWindow only: no activation,
  NO z-order change) sized to the primary monitor from AD `desktop_list_monitors` (cached 10 min).
  The window ends visually full-screen, still at the z-bottom where force-bottom put it.
- The park threads the hwnd from the bottoming step into placement (no re-lookup in the common path).
- Dev-skill dead-end added: never use CDP windowState:'maximized' for background placement.

## 1.8.64 — 2026-07-15 — Background windows appear already-maximized (no more small-then-grow)

John: "why when you open a new window is it smaller than my screen and then i see it grow larger?"
Because the park staged the window ON-screen at 1200×800 before maximizing, and a z-bottom window
is visible wherever the user's windows don't cover — so the small stage showed, then the grow.
Now the park stages the window JUST past the LEFT EDGE of the primary monitor (fully invisible but
adjacent, so the OS maximize targets that monitor — maximizing from deep off-screen produces
garbage bounds, tested) and only then maximizes: the window's first visible frame is full-size.
Position-verify and self-heal thresholds tightened from -5000 to -1000 so a window stuck at the
-1310 staging spot reads as failed/heal-worthy instead of healthy.

## 1.8.63 — 2026-07-15 — force:true adopted (AD 1.9.115): the launch park is one AD call

AD 1.9.115 shipped the follow-up: `desktop_set_window_state {state:'bottom', force:true}` bottoms
the window EVEN IF it is currently foreground (verified on ADOMGPU: sentToBack:true despite
wasForeground:true). pup now always passes force:true — the launch park is a single AD call in
the common case. The legacy PS fallback is RETAINED but auto-obsoletes: it only fires on AD
1.9.114 (force ignored, skip-on-foreground) or older (no state:'bottom' at all); delete it once
the fleet is ≥1.9.115. It was kept deliberately — removing it while AdomLapper was still on
1.9.114 would have regressed the post-restart foreground-reassignment case on John's primary box.

## 1.8.61 — 2026-07-14 — 100% direct-API window management (AD shipped state:'bottom')

AD 1.9.114 shipped pup's feature request: `desktop_set_window_state {state:'bottom'}` — native
z-order send-to-back, background drive, title-resolved, returns `{sentToBack, wasForeground}`.
The park now uses it, deleting pup's LAST PowerShell from the open path (one AD call replaces
find_window + the P/Invoke SetWindowPos script). Legacy PS fallback kept for desktops on older AD.

## 1.8.60 — 2026-07-14 — Launch park bottoms UNCONDITIONALLY (escape hatch removed)

"Pup window is foreground at park time" is NOT a user signal: Windows reassigns the foreground to
a fresh window when the previous foreground window was just destroyed (e.g. a bridge-update
restart), which force-foregrounded windows the user never clicked (nxp.com, another thread's
cpconn). The user-click escape hatch is gone; a user who clicked during the ~2s launch gap clicks
once more and the window stays (nothing ever re-parks).

## 1.8.59 — 2026-07-14 — HARD RULE: never move a window on-screen until z-bottom is CONFIRMED

Under a 3-parallel-open load, one window's hwnd lookup lagged, the z-bottom step failed, and the
park moved the window on-screen anyway — an un-bottomed window lands on TOP of the user's work
(the nxp.com pop; no focus stolen, but visually covering, which is just as forbidden).

- The park now retries hwnd-resolve + z-bottom up to 8× (~2.5s); if 'bottomed' is never confirmed,
  the window STAYS off-screen and the verdict is an honest `park-failed` — invisible beats popping.
- `park-failed` windows self-heal opportunistically: any pup_status / list query retries the
  heal, on top of the existing reattach / open_tab / switch_tab touchpoints.
- 1.8.58: badge calls check AD's real result + log `[badge] applied/FAILED`; the tab-path debounce
  arms only on success (one silent failure no longer means a bare icon for 10 minutes).

## 1.8.54 → 1.8.57 — 2026-07-14 — Honest click signal; OS maximize; focus-latch fix; self-heal stranded windows

- **1.8.54:** `GetForegroundWindow` replaces `document.hasFocus()` for user-click detection —
  hasFocus LIES for CDP-created windows (false-fired `user-foreground` on every open).
- **1.8.55:** park places via CDP `windowState:'maximized'` (OS-true full size) instead of pixel
  math from a screen probe that could fail mid-navigation (shipped 1600×900 windows once).
- **1.8.56:** flash `pending/cleared` focus latch fixed — the hook now also injects into the
  CURRENT document (`evaluateOnNewDocument` alone only fires on the NEXT navigation, so clicked
  windows kept reporting `pending`).
- **1.8.57 (the adom-shotlog fix):** a window created under a build whose park failed sat
  stranded off-screen forever — taskbar click "showed" an invisible window, badge gone. pup now
  **self-heals**: cheap CDP bounds check at reattach / `open_tab` / `switch_tab`; only if actually
  off-screen, re-park + re-badge (never touches a healthy or user-foreground window). Badge also
  re-applies (debounced 10 min) on the tab hot path so long-lived shared windows stay badged
  across bridge restarts. Anonymous window CREATORS now get an owner-tip in the open response
  (an ownerless long-lived window has zero cross-thread protection). Enriched `pup_describe`
  entries for `open_tab`/`switch_tab` (return shape, updateNote, background/click contract).

## 1.8.53 — 2026-07-14 — Direct-API park: CDP + AD verbs replace the PowerShell window-hunt

John caught a parallel-open window (catch-2) still stuck OFF-SCREEN while the bridge claimed
"backgrounded" — the PS park moved the wrong window and its focus-only self-check passed anyway.
And he asked the right question: "why are you using powershell? that's heavy. why aren't you just
calling direct api's?" Rebuilt the park on direct APIs:

- **Geometry via CDP `Browser.setWindowBounds`** — addresses the EXACT window by CDP windowId, so
  parking the wrong window is impossible. ~10ms, no process spawn.
- **Window lookup via AD `desktop_find_window`** (native, title-suffix match) — no EnumWindows PS,
  no pid guessing.
- **The park runs where the window is BORN** (right after `newPage()` + welcome page in
  launchSession) — no wait loop at all; the `await` IS the event. `tryDetachedLaunch` just marks
  `_needsPark` (reused windows are never re-parked).
- **Verification checks POSITION** (CDP `getWindowBounds`), not just focus — the exact lie catch-2
  told is now detected as `park-failed`.
- **One residual Win32 call**: z-bottom (`osSendHwndToBottom`, single P/Invoke) — CDP has no
  z-order and AD has no send-to-back verb yet (AD issue filed). Runs while the window is still
  off-screen so its latency is invisible.
- **Badge fires the instant the park lands** (title suffix already set) instead of blind
  1.5s/3.5s timers — the teal Adom favicon appears with the window, not seconds later.

## 1.8.52 — 2026-07-14 — Close the launch gap: a click during the first ~2s now shows the window

With 1.8.51, a taskbar click during the launch gap (window still at its off-screen coords, park PS
still starting) activated an invisible off-screen window — the user saw nothing for ~2s. Now the park
checks the foreground first, and the check is UNAMBIGUOUS because `--no-startup-window` means Chrome
cannot raise itself: pup-window-foreground can only be the user. If the user clicked during the gap,
the park places the window on-screen at the TOP full-size and leaves them focused on it
(`user-foreground`); otherwise it parks to z-bottom as usual. Either way it exits — still no loops.

## 1.8.51 — 2026-07-14 — `--no-startup-window`: eliminate the focus steal at the SOURCE (no more war)

John proved a taskbar click within the first couple seconds still got yanked (the 1.8.50 stability
watch cannot tell an early user click from a Chrome self-raise — no watch can). Fix: make the war
impossible instead of fighting it better.

- **Background launches use `--no-startup-window`.** Windows only grants foreground rights to a
  process at launch; with this flag the browser starts with NO window, and when pup creates the
  window via CDP moments later the browser is a background process — the OS itself DENIES it the
  foreground. No steal can happen.
- **Therefore: no stability watch, no timers, no re-parks — deleted.** The park is now: wait for
  the window to exist → hand focus back if somehow taken (normally a no-op) → one SetWindowPos to
  z-bottom → verify → EXIT. A user click at ANY moment — 200ms in or 2 minutes in — has nothing
  running against it.
- Removed the duplicate post-open background re-assert (now only after an explicit sized open,
  where CDP setWindowBounds can re-activate; guarded by a foreground check).

## 1.8.50 — 2026-07-14 — One event-driven watchdog replaces the blind-timer park (John: "waiting 1.2s is for the birds")

The launch park no longer fires at guessed delays. `osBackgroundWatchdog` = ONE hidden PS process:
1. WAIT for the real window to EXIST (EnumWindows @50ms, up to 4s) — react to the event, not a timer.
2. The moment it exists: hand focus back (only if Chrome actually took it), THEN park on-screen at
   z-bottom (deactivate-first = no pop). Once.
3. Stability watch: if Chrome re-grabs the foreground during startup, hand back + re-park within 50ms;
   exit after 500ms of calm (typically <1s total).
4. Exit. Afterwards NOTHING re-parks — a user's taskbar click brings the window up full-size, forever.
The open response awaits the watchdog's SELF-VERIFIED verdict, so `background` is the real outcome,
never a guess mid-park.

## 1.8.49 — 2026-07-14 — Kill the input-timing heuristic (it false-fired whenever the user was typing)

Opening windows while John was actively using the laptop: 2 of 3 came up `user-foreground`, small
(centered 80%) instead of parked full-size. Root cause: the "did the user click it?" check used
`GetLastInputInfo`, which measures input ANYWHERE on the system — so Chrome's startup self-raise +
the user typing in another app read as "user grabbed the pup window."

- **Launch passes (300/700/1200ms) now ALWAYS park — no user detection.** Within ~1.2s of launch the
  taskbar button barely exists; a foreground pup window is Chrome's self-raise. After the launch
  window, pup NEVER auto-re-parks, so a user click is never fought — no heuristics needed.
- **Late re-parks (post-resize) keep a simple foreground check only:** after startup Chrome doesn't
  self-raise, so pup-is-foreground = the user took it → leave it exactly as they have it.
- **Removed the centered-80% "show" placement** — windows are always full-monitor size; the user's
  own click is what brings them up (restoring exactly what Windows restores).

## 1.8.48 — 2026-07-14 — Background = on-screen z-bottom (minimize reverted: it broke screenshots)

1.8.47's minimize broke screenshots: a minimized window reports `visibilityState: hidden`, so
`Page.captureScreenshot` hangs (~27s then errors) even with occlusion disabled. Reverted to the
on-screen-occluded park (which keeps rendering → screenshots work), now correct on all the pieces:

- **Real window handle** via `EnumWindows` (1.8.46) — no more phantom-helper-window misses.
- **Deactivate-then-park order** (1.8.46) — hand focus back while off-screen, THEN move on-screen at
  z-bottom, so it never flashes on top (no pop).
- **Short re-park window** (~1.2s: 300/700/1200ms) — just long enough to counter Chrome's startup
  foreground-grab. After that pup NEVER re-parks, so a user click is not fought. Within the window,
  `GetLastInputInfo` distinguishes a real click (→ bring on-screen + latch off further passes) from
  Chrome self-raising (→ re-park).

## 1.8.47 — 2026-07-14 — Background = MINIMIZE (no more re-park loop fighting the user's clicks)

The on-screen-z-bottom park needed a re-park retry loop to counter Chrome's startup foreground-grab,
and that loop fought the user: click a window to view it and pup yanked it back. Rebuilt the background
primitive around **minimize**, which removes the whole conflict:

- **Minimizing hands the foreground back to the user automatically** (no foreground-lock fight, no
  AttachThreadInput) — no focus steal, and no visible pop (the window launched off-screen).
- **The restore rectangle is set on-screen** (`SetWindowPlacement`), so a taskbar click restores the
  window centered + visible — **and it STAYS**, because there is NO on-screen re-park loop to yank it.
- **Screenshots still work** because Chrome already launches with occlusion detection disabled
  (`--disable-features=CalculateNativeWinOcclusion` + `--disable-backgrounding-occluded-windows`), so a
  minimized window keeps compositing frames.
- The only late passes are safety re-minimizes that RESPECT a user restore: the instant the window is
  un-minimized by a real click (physical input), pup latches `_userForeground` and never touches it again.
- `pup_lower_os_window` / post-resize re-park also minimize now. `raise` restores via CDP as before.

## 1.8.46 — 2026-07-14 — Resolve the REAL browser window (the background park was targeting a phantom)

**The bug behind "I click a pup window and it never shows."** The background park resolved the window via
`Get-Process(pid).MainWindowHandle`, which on some boxes (John's laptop) returns a **hidden Chrome helper
window**, not the real browser window. So `SetWindowPos` moved a phantom, the real window stayed at its
off-screen launch coords (`screenX -21333`), and the "backgrounded" self-check passed against the phantom —
a **false positive**. Clicking the taskbar activated an off-screen window = nothing visible. Proven by
contrast: CDP `Browser.setWindowBounds` (which `raise` uses) put the same window at `0,0 1708×1068` first try.

- **Resolve the real window by ENUMERATION** (`EnumWindows`): the largest VISIBLE top-level
  `Chrome_WidgetWin_1` owned by the browser's pid (or its children). Replaces the flaky `MainWindowHandle`
  in `osBackgroundWindowByPid` AND `osClearFlashByActivate`. The park now actually moves the real window
  on-screen at z-bottom, so a taskbar click shows it — and the self-verify is honest.

## 1.8.45 — 2026-07-14 — User↔AI window balance + real flash clear + programmatic flash state

Three related fixes, all from John's live testing. Governing rule now written into `pup-bridge-dev`
("The user↔AI balance"): pup windows are background AI tasks that must not disturb the user, AND real
windows the human must be able to click, SEE, and use normally — both at once.

- **A user click now actually SHOWS the window.** v1.8.44 made the re-park respect a deliberate user
  raise, but it *bailed without moving the window on-screen* — so clicking a still-off-screen window
  activated an invisible window (user saw nothing). Now the user-raise path brings the window ON-SCREEN
  (centered ~80%, `SWP_SHOWWINDOW`, HWND_TOP) and leaves it foreground. Background↔foreground handoff.
- **`pup_alert_window {stop}` truly clears the flash.** `FLASHW_STOP` only stops the *pulse*; the
  Win11 attention tint persists until the window is ACTIVATED. New `osClearFlashByActivate` clears it by
  activating the window OFF-SCREEN (invisible → tint clears) then handing focus back — no pop, no focus
  steal. Reports a real `cleared` / `already-foreground`, never a fire-and-forget guess.
- **Programmatic flash state.** `getSessionInfo.flash` = `none|pending|cleared` (from `_flashState` vs an
  injected `window.__pupFocusTs` focus latch) — answers "which windows are still flashing?" without a
  screenshot.

## 1.8.44 — 2026-07-14 — Respect a deliberate user raise (click-to-watch no longer forced back)

A user clicking a background pup window's taskbar button to WATCH a page load saw it snap
back to the background. Cause: the open-time re-park passes (600ms/1.4s/2.5s) that counter
Chrome's startup self-raise ran `SetWindowPos HWND_BOTTOM` **unconditionally**, so a click
landing inside that 2.5s window got yanked back.

- **`osBackgroundWindowByPid(..., respectUser)`** now distinguishes a real user raise from
  Chrome self-raising: if the pup window is foreground AND `GetLastInputInfo` shows a physical
  input within the last ~1.5s (Chrome's self-raise is programmatic — no input), it returns
  `"user-foreground"` and skips the park.
- The retry driver **latches** on that (`_userForeground`) and **cancels the remaining passes**
  — once you take the window, pup never fights you for it. The post-resize re-park honors the
  same latch. The initial off-screen park is unaffected (runs with `respectUser=0`; you can't
  have clicked an off-screen window).

## 1.8.35 → 1.8.43 — 2026-07-13/14 — Flash routes through AD; taskbar badge; snappy flash; cleanup nudge

- **Taskbar flash + clear route through AD-core** (`desktop_flash_window`) instead of pup's own
  `FlashWindowEx` — one mechanism, native, reliable. `pup_alert_window {stop}` now returns
  AD's real `applied` result (no more fire-and-forget `cleared:true` lies). Snappy: flashes on
  open before the page finishes loading.
- **Adom favicon badge** overlaid on the pup taskbar button via AD's `desktop_taskbar` overlay.
- **`🧹 CLEANUP` hint** in the open response + `pup_status` when windows are ≥30m old or ≥5 open.

## 1.8.24 → 1.8.34 — 2026-07-13 — Background-by-default, actually correct (no pop / no cover / no focus steal / no cmd flash)

The long-standing "pup window pops up, covers my work, steals my typing" problem, solved properly and
made deterministic + self-verifying. **The full technique + the dead-ends are documented in the
`pup-bridge-dev` skill ("Background-by-default without stealing focus") — read that before touching window
placement; do NOT re-derive it.** Summary:
- **Launch OFF-SCREEN** (`--window-position=-32000,-32000`, not `--start-maximized`) so it never visually
  pops; CDP still renders it (screenshots work).
- **Park on-screen at z-bottom** sized to the monitor (`SetWindowPos HWND_BOTTOM SWP_NOACTIVATE`) → behind
  the user's windows (invisible) but taskbar-clickable.
- **Hand keyboard focus back with `AttachThreadInput`** (snapshot the user's window pre-launch; attach
  input threads to bypass the foreground lock; `SetForegroundWindow` back to it; re-run over ~2.5s as the
  browser re-grabs during startup). Deterministic `backgrounded` instead of a flaky focus steal.
- **All window-management PowerShell runs HIDDEN** (`execFileSync` + `windowsHide` + `-WindowStyle Hidden`
  via `runPsHidden`), so no cmd/terminal window ever flashes.
- **Self-verifying:** every open re-reads `GetForegroundWindow()` and reports a `background` field
  (`backgrounded`/`still-foreground`). navigate/reload/switch_tab no longer re-raise a background window.
- Dead-ends rejected (all worse): start-maximized+lower (visible pop), bare `SetForegroundWindow` (lock),
  minimize-and-leave (breaks screenshots), `WS_EX_NOACTIVATE` (breaks taskbar-click + Chrome self-activates),
  minimize-loop (flicker), pup-side localhost probe (AD already reprimands localhost on failure).

## 1.8.23 — 2026-07-10 — Agent-update signalling (auto-flash), keystroke-safe raise, render_html

Field report from an agent that staged a 4-tab task and lost track of which window it was updating.
All five items shipped (AD relays verb args verbatim → no AD-side change):

- **Auto-flash on agent updates.** Mutating verbs (`open_window`/`open_tab`/`navigate`/`reload`/`back`/
  `forward`/`switch_tab`/`click`/`type`/`press_key`/`input_dispatch`/`eval`/`evaluate`) flash the
  window's taskbar via real `FlashWindowEx` (`FLASHW_TRAY|FLASHW_TIMERNOFG`) so the user notices the
  agent touched it — **never steals focus**, **self-suppresses while foreground**, **clears on click**.
  Debounced: one flash per window per ~5s burst. Suppress per-call with `{silent:true}`, treat a read as
  read-only with `pup_eval {readOnly:true}`, or silence a whole session with
  `pup_open_window {autoFlash:false}`.
- **`updateNote` + `lastAgentUpdate`.** Pass `updateNote` on any mutating verb; pup records
  `{tabId,verb,note,ts}` and surfaces it top-level **and per-tab** in `pup_list_tabs` /
  `list_windows` / status — answers "which tab did the agent just change?".
- **Raise no longer leaks keystrokes.** `pup_raise_os_window`/`focus_window` blur the page's active
  element before `bringToFront()`. Docs steer agents to `pup_alert_window` (or the auto-flash) for
  "I updated this", reserving raise for an explicit show-me.
- **`pup_open_tab` returns `tabId` + `tabCount`** explicitly (no more diffing `list_tabs`).
- **New `pup_render_html {sessionId,html,waitUntil?,updateNote?,silent?}`** — renders a local HTML
  string via `page.setContent` (UTF-8-correct; a `data:` URL mojibakes accented chars without a charset).
- Note: `desktop_screenshot_window titleContains/sessionId` is an **AD-core** verb — routed to AD, not
  pup. The `fullPage` nested-scroll `nestedScroll:true` warning shipped in 1.8.21.

## 1.6.0 → 1.8.17 — 2026-07 — Generic browser, background-by-default, cross-thread ownership, bridge→AD notify

Consolidated summary of the arc since 1.1.0 (per-release detail lives in the wiki Releases tab):

- **Became a generic, non-profile browser.** Removed all native-real-browser drive code — driving the
  user's real signed-in Chrome/Edge is now the Adom browser **extension's** `nbrowser_*` job. Every pup
  session is a fresh isolated Chrome/Edge process. Installed-browser-first: Chrome → Edge → cached CfT,
  spawn-verified with fallthrough; Chrome for Testing only as a last resort.
- **Loopback bind** (`ADOM_BIND_HOST`, 127.0.0.1) so no Windows Firewall prompt.
- **Background by default → hard guarantee (1.8.17):** windows never steal focus; sizing/positioning no
  longer foregrounds either. `foreground:true` is the only way to show a window.
- **Window ownership (1.8.16):** sessions carry an advisory `owner`; declared cross-thread navigate/close
  is refused (`session_owned_by_another_thread`), anonymous grabs are reprimanded — stops AI threads
  stealing each other's windows. `pup_status`/`list_windows` expose `owner` + `ageMinutes`.
- **Chrome install drives the user through the UAC (1.8.12–1.8.15):** on a locked-down box, escalates to
  an elevated install and — via AD ≥1.9.84's direct-API — fires a native "click YES" toast from the
  bridge itself, pinging the attended device (cross-AD `target`) and re-arming if it expires. Degrades
  cleanly on older AD (raise UAC once + relay hint, never spam).
- **Missing verbs + catalog completeness (1.8.8–1.8.11):** added pup_click/type/press_key/back/
  forward/evaluate and documented 13 working-but-unadvertised verbs in `bridge.json` + `pup_describe`.
- **Title-suffix resilience (1.8.17):** persistent `evaluateOnNewDocument` injector so AD's title-based
  raise/lower window lookup stops flaking right after a navigation.
- **Skillpack restructure:** public `pup` + `pup-bridge-dev` + `pup-bridge-publish` as
  `skills/<name>/SKILL.md` (`user-invocable:false` on the dev/publish pair).

## 1.1.0 — 2026-06-29 — Cold-start self-heal + ownership extraction

**Why:** On a fresh PC (Hydrogen Desktop bundles Bridge; the installer pre-installs neither
Node nor Chrome for Testing), the first "open … in pup" failed with a generic
"Failed to launch Chrome after 3 attempts" + a developer-only string ("run npx puppeteer browsers
install chrome") — no `errorCode`, no auto-install, no actionable hint. The AI couldn't recover.

**Fixed — the bridge now self-heals Chrome for Testing (CfT):**
- **`src/chrome.js`** (new) — detect (validates the *actual* executable, not just "some dir exists"),
  auto-install programmatically via the bundled `@puppeteer/browsers` (no npx), pinned to the exact
  buildId this puppeteer expects, with download-progress + install-state dedupe + a readiness snapshot.
- **`pup_open_window`** — a cold-start gate (CfT path only; native-drive skips it) that detects a
  missing CfT, kicks off a NON-BLOCKING background install, and returns a structured
  `{ errorCode:"chrome_for_testing_installing", installing:true, statusVerb:"pup_readiness",
  _hint:"poll pup_readiness then retry" }` — instead of blindly spawning a missing binary 3× then
  throwing. On install failure → `chrome_install_failed`/`chrome_download_failed` with a real hint
  (no more silent continue-to-doomed-launch).
- **`pup_readiness`** (new verb) — `{ ready, chromeForTestingInstalled, installing,
  installProgressPct, ... }`. Poll until `ready:true`.
- **`pup_prewarm`** (new verb) — install CfT WITHOUT opening a window (warm before first use);
  `{wait:true}` blocks.
- **Status chip** — `/status` now emits `led`/`summary`/`tooltip` and tells the TRUTH: yellow
  "Installing Chrome N%" / "Chrome not installed", red on install failure, green when ready (was always
  green, even with no Chrome). `/status` + `/health` both carry `version` + `chrome` readiness.
- **Deep launch throw** is now Chrome-aware (structured `chrome_for_testing_not_installed` when CfT is
  genuinely missing vs a memory/locked-profile failure when it's present).

**Hygiene:**
- `BRIDGE_VERSION` ↔ `bridge.json` version lockstep fixed (was 1.0.14 vs 1.0.16).
- `bridge.json`: `statusVerb:"pup_readiness"` + `updateManifestUrl` so AD auto-updates from this
  page; new verbs registered in `verbs[]` + `pup_describe`.

**Ownership:** extracted from `adom-bridge/plugins/puppeteer` into this standalone wiki repo
(`wiki.adom.inc/adom/pup-bridge`), mirroring the cloud-owned kicad/fusion model.
Bridge keeps a bundled SEED for first-run/offline and auto-pulls newer versions from here via
`updateManifestUrl`.
