@m13v/s4l 1.7.5-rc.2 → 1.7.5-rc.21

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (48) hide show
  1. package/mcp/dist/index.js +571 -79
  2. package/mcp/dist/panel.html +2 -3
  3. package/mcp/dist/redditAuth.js +115 -0
  4. package/mcp/dist/runtime.js +83 -2
  5. package/mcp/dist/setup.js +142 -5
  6. package/mcp/dist/telemetry.js +13 -3
  7. package/mcp/dist/version.json +2 -2
  8. package/mcp/manifest.json +1 -1
  9. package/mcp/menubar/s4l_card.py +32 -6
  10. package/mcp/menubar/s4l_menubar.py +42 -111
  11. package/mcp/menubar/s4l_state.py +54 -187
  12. package/mcp/package.json +1 -1
  13. package/mcp/shared/onboarding-ledger.cjs +63 -4
  14. package/package.json +1 -1
  15. package/scripts/autopilot_stall_watch.py +3 -6
  16. package/scripts/browser_mutex.py +316 -0
  17. package/scripts/claude_job.py +21 -0
  18. package/scripts/config.py +118 -0
  19. package/scripts/counterparty_history.py +44 -6
  20. package/scripts/engage_github.py +7 -3
  21. package/scripts/engage_reddit.py +9 -3
  22. package/scripts/engagement_signal_feedback.py +256 -0
  23. package/scripts/learned_preferences.py +37 -14
  24. package/scripts/memory_snapshot.py +45 -0
  25. package/scripts/merge_review_queue.py +67 -3
  26. package/scripts/post_reddit.py +289 -24
  27. package/scripts/reddit_browser.py +62 -63
  28. package/scripts/reddit_browser_fetch.py +32 -0
  29. package/scripts/release-mcpb.sh +17 -0
  30. package/scripts/reset-test-machine.sh +13 -7
  31. package/scripts/scan_twitter_thread_followups.py +7 -1
  32. package/scripts/schedule_state.py +1 -1
  33. package/scripts/setup_reddit_auth.py +925 -0
  34. package/scripts/snapshot.py +96 -4
  35. package/scripts/stats.py +37 -13
  36. package/scripts/test_no_silent_fallbacks.py +281 -0
  37. package/scripts/trigger_promo_lane_test.sh +91 -0
  38. package/scripts/twitter_browser.py +183 -342
  39. package/scripts/twitter_scan.py +75 -10
  40. package/scripts/watchdog_hung_runs.py +15 -9
  41. package/skill/lib/harness-common.sh +344 -0
  42. package/skill/lib/linkedin-backend.sh +61 -161
  43. package/skill/lib/reddit-backend.sh +39 -125
  44. package/skill/lib/twitter-backend.sh +51 -273
  45. package/skill/lock.sh +57 -0
  46. package/skill/run-reddit-search-launchd.sh +4 -1
  47. package/skill/run-reddit-search.sh +51 -2
  48. package/skill/run-twitter-cycle.sh +44 -21
@@ -231,18 +231,83 @@ def _write_scan_tweets_record(rec: dict) -> None:
231
231
  pass # fail-open; shell falls back to structured_output if file is missing
232
232
 
233
233
 
234
+ def _revive_current_tab(url: str) -> bool:
235
+ """Side-door revive of a wedged tab: attach a FRESH CDP session to the
236
+ SAME target and stop+navigate it there, so the one-tab-reused-forever
237
+ invariant survives a hung renderer.
238
+
239
+ When goto_url times out, what died is (almost always) the tab session or
240
+ the renderer, not the daemon's browser-level connection — the daemon just
241
+ REPORTED the timeout over that very connection. Page.stopLoading and
242
+ Page.navigate are handled by the BROWSER process, and navigate is allowed
243
+ to replace a crashed/hung renderer, so a fresh session to the same target
244
+ recovers the tab in place. Finish by re-pointing the daemon at the fresh
245
+ session (set_session, deliberately WITHOUT Target.activateTarget — that
246
+ call raises/focuses the Chrome window). Returns True when the navigate
247
+ was acknowledged; False routes the caller to the new-tab last resort
248
+ (only justified when the browser's handling of the target itself is
249
+ dead). Outcome is logged either way: every real wedge doubles as an
250
+ experiment on which recovery door works."""
251
+ from browser_harness import helpers as _bh
252
+
253
+ try:
254
+ tid = None
255
+ try:
256
+ tid = (_bh.current_tab() or {}).get("targetId")
257
+ except Exception:
258
+ pass
259
+ if not tid:
260
+ infos = _bh.cdp("Target.getTargets").get("targetInfos", [])
261
+ pages = [t for t in infos if t.get("type") == "page"]
262
+ pref = [t for t in pages if (t.get("url") or "").startswith("http")]
263
+ tid = ((pref or pages) or [{}])[0].get("targetId")
264
+ if not tid:
265
+ print("[twitter_scan] revive: no page target found", file=sys.stderr)
266
+ return False
267
+ sid = _bh.cdp("Target.attachToTarget", targetId=tid, flatten=True)["sessionId"]
268
+ try:
269
+ _bh.cdp("Page.stopLoading", session_id=sid)
270
+ except Exception:
271
+ pass # stop is best-effort; navigate is the recovery
272
+ _bh.cdp("Page.navigate", session_id=sid, url=url)
273
+ _bh._send({"meta": "set_session", "session_id": sid, "target_id": tid})
274
+ print(
275
+ f"[twitter_scan] revived wedged tab in place (target={tid[:12]})",
276
+ file=sys.stderr,
277
+ )
278
+ return True
279
+ except Exception as e:
280
+ print(f"[twitter_scan] revive failed ({e})", file=sys.stderr)
281
+ return False
282
+
283
+
234
284
  def _navigate(url: str) -> None:
235
- """Reuse the existing real tab if there is one (typical cycle behavior),
236
- otherwise open one. The MCP-managed Chrome always has at least an
237
- about:blank tab from launch, but be defensive: a hung tab close between
238
- cycles can leave us with only chrome:// tabs."""
239
- real = [
240
- t for t in list_tabs(include_chrome=False)
241
- if (t.get("url") or "").startswith(("http", "about:"))
242
- ]
243
- if real:
285
+ """ALWAYS reuse the daemon's current tab; escalate through revive; create
286
+ a tab only when both fail — and then in background, so it cannot pop.
287
+
288
+ Recovery ladder (2026-07-15, after the Chrome 150 wedge storm):
289
+ 1. goto_url — plain reuse; also revives an "Aw, Snap" tab whose session
290
+ still answers. The only path 99% of navigations take.
291
+ 2. _revive_current_tab — fresh CDP session to the SAME target
292
+ (stop+navigate); recovers a wedged session/renderer in place.
293
+ 3. new_tab(background=True) — last resort for a dead target.
294
+ Target.createTarget/activateTarget are PROVEN to focus-steal (7/7
295
+ same-second harness_browser_foregrounded correlation), hence
296
+ background; the TypeError fallback covers customer boxes whose
297
+ installed harness predates the background parameter."""
298
+ try:
244
299
  goto_url(url)
245
- else:
300
+ return
301
+ except Exception as e:
302
+ print(
303
+ f"[twitter_scan] goto_url failed ({e}); trying in-place revive",
304
+ file=sys.stderr,
305
+ )
306
+ if _revive_current_tab(url):
307
+ return
308
+ try:
309
+ new_tab(url, background=True)
310
+ except TypeError:
246
311
  new_tab(url)
247
312
 
248
313
 
@@ -37,12 +37,18 @@ MAX_AGE_SEC = 45 * 60
37
37
  # whose heartbeat has gone stale means the holder is wedged mid-browser-work
38
38
  # (2026-07-13: pid 1380 hung 60+ min in Phase 2b-prep media capture during a
39
39
  # network flap, blocking every peer pipeline until the 180-min age cap).
40
- # Thresholds: WARN at 30 min stale (log-only). KILL at 90 min stale — above
41
- # the gen phase's documented 60-min worst-case ceiling, so a legit long hold
42
- # with no python browser invocations can't be false-killed — and ONLY when
43
- # peers are actually queued waiting (an unwanted lock harms nobody). The
44
- # 2026-07-12 lock.sh signal fix makes the TERM safe: the holder releases its
45
- # locks and exits instead of continuing lock-protected work.
40
+ # Thresholds: WARN at 10 min stale (log-only). KILL at 15 min stale, and ONLY
41
+ # when peers are actually queued waiting (an unwanted lock harms nobody).
42
+ # Retuned 2026-07-14 from 30/90 min: the original 90 was sized around a
43
+ # "documented 60-min gen phase ceiling" that run logs disprove (media capture
44
+ # is ~2 min; the long pole is lock WAIT, which doesn't hold the lock), and
45
+ # during the Chrome 150 crash-storm the 90-min heal lost the race against a
46
+ # roughly-hourly wedge cadence, leaving the pipeline down all night
47
+ # (2026-07-13). Heartbeat is touched on attach and every page network event,
48
+ # so any healthy browser hold stays fresh; 15 min of zero activity while
49
+ # peers queue is a wedge. The 2026-07-12 lock.sh signal fix makes the TERM
50
+ # safe: the holder releases its locks and exits instead of continuing
51
+ # lock-protected work.
46
52
  # ONLY the heartbeat-instrumented platforms. linkedin-browser holders write no
47
53
  # heartbeat (the linkedin pipeline drives Chrome via the bh harness, not a
48
54
  # shared python lib), so on pid-file age alone a healthy long linkedin run
@@ -52,9 +58,9 @@ BROWSER_LOCKS_WATCHED = (
52
58
  "/tmp/social-autoposter-twitter-browser.lock",
53
59
  "/tmp/social-autoposter-reddit-browser.lock",
54
60
  )
55
- HB_WARN_SEC = 30 * 60
56
- HB_KILL_SEC = 90 * 60
57
- HB_REWARN_SEC = 30 * 60 # re-log the warning at most this often per holder
61
+ HB_WARN_SEC = 10 * 60
62
+ HB_KILL_SEC = 15 * 60
63
+ HB_REWARN_SEC = 10 * 60 # re-log the warning at most this often per holder
58
64
  # Per-script cap overrides for pipelines that legitimately run longer than
59
65
  # the 45 min global (stats.py over ~4-5k posts + rate-limit sleeps).
60
66
  # Key is (script_file, platform_or_None). Lookup order: (script, platform),
@@ -0,0 +1,344 @@
1
+ #!/bin/bash
2
+ # harness-common.sh - the shared engine behind every per-platform browser
3
+ # backend (twitter-backend.sh, reddit-backend.sh, linkedin-backend.sh).
4
+ #
5
+ # Born 2026-07-14 from the three near-duplicate backends. Every hard-won fix
6
+ # used to land in ONE backend and drift: the CDP wedge detector, the two-strike
7
+ # tolerance, the focus-safe launcher, the occlusion flags, and the repo-relative
8
+ # script paths all existed only on the twitter side for weeks. This file owns
9
+ # the machinery once; a backend supplies configuration plus platform hooks.
10
+ #
11
+ # Contract: a backend sources this file, then defines its public
12
+ # ensure_<platform>_browser_for_backend() as a thin wrapper that calls
13
+ # hc_ensure_browser with HC_* set as env-prefixed assignments on the call
14
+ # (NOT as file-level globals, so sourcing two backends in one process can
15
+ # never cross-wire their configs):
16
+ #
17
+ # ensure_reddit_browser_for_backend() {
18
+ # HC_PLATFORM=reddit HC_PORT=9557 ... hc_ensure_browser
19
+ # }
20
+ #
21
+ # HC_* knobs (required unless a default is shown):
22
+ # HC_PLATFORM - twitter|reddit|linkedin; log labels + wedge marker prefix
23
+ # HC_PORT - CDP port (9555/9556/9557)
24
+ # HC_PROFILE_DIR - Chrome profile dir; also the exact-dir pgrep reap match
25
+ # HC_DEFAULT_URL - http://127.0.0.1:<port>; ownership test vs external/BYO
26
+ # HC_CDP_URL - actual CDP url (backend passes its <PLATFORM>_CDP_URL)
27
+ # HC_LAUNCH_URL - first-tab url (default about:blank)
28
+ # HC_WINDOW_POS - macOS offscreen window position (x,y)
29
+ # HC_WINDOW_SIZE - macOS window size (w,h)
30
+ # HC_EXTRA_FLAGS - extra platform Chrome flags, ONE STRING, split on
31
+ # whitespace on purpose (none of our flags carry spaces)
32
+ # HC_HEALTH_FILE - readiness-verdict sink (default
33
+ # skill/logs/cdp-health-<platform>.json; twitter passes
34
+ # the legacy cdp-health.json that memory_snapshot.py reads)
35
+ # HC_WEDGE_MARKER - stderr marker prefix (default <platform>_cdp_wedge;
36
+ # twitter's resolves to the load-bearing twitter_cdp_wedge
37
+ # that bin/server.js greps - do not rename)
38
+ # Hooks (optional FUNCTION NAMES, invoked only when declared):
39
+ # HC_PRE_LAUNCH_HOOK - after the external-Chrome bail, before probe/launch.
40
+ # Nonzero rc propagates (linkedin's whole-run lock
41
+ # returns the reserved skip code 78 through here).
42
+ # HC_POST_LAUNCH_HOOK - after the browser is confirmed up (freshly launched
43
+ # OR already up), before tab cleanup. Best effort;
44
+ # failures never fail the ensure (twitter session
45
+ # restore).
46
+ # HC_EXTERNAL_OK_HOOK - on the externally-managed-Chrome happy path, before
47
+ # returning (twitter session restore on AppMaker).
48
+ # HC_POST_CLEANUP_HOOK - after tab cleanup; its rc IS the ensure rc
49
+ # (linkedin logout detect-gate aborts the fire).
50
+ #
51
+ # Everything here must stay bash-3.2-safe (launchd's shell): no arrays that
52
+ # might expand empty under set -u, no ${var,,}, no associative arrays.
53
+
54
+ # Repo root for the helper scripts this file shells out to, resolved from this
55
+ # file's own location (skill/lib/ -> two up), honoring S4L_REPO_DIR when the
56
+ # caller sets it. $HOME/social-autoposter hardcodes ran code OUTSIDE the
57
+ # managed package on customer boxes and silently no-op'd where that directory
58
+ # didn't exist (S4L-4H triage 2026-07-12).
59
+ _BH_REPO_DIR="${_BH_REPO_DIR:-${S4L_REPO_DIR:-$(cd "$(dirname "${BASH_SOURCE[0]}")/../.." && pwd)}}"
60
+
61
+ _hc_ts() { date -u +%Y-%m-%dT%H:%M:%SZ; }
62
+
63
+ _hc_run_hook() {
64
+ # $1 = hook function name (may be empty/undeclared -> no-op success).
65
+ [ -n "${1:-}" ] || return 0
66
+ declare -F "$1" >/dev/null 2>&1 || return 0
67
+ "$1"
68
+ }
69
+
70
+ _resolve_chrome_bin() {
71
+ # Auto-detect Chrome/Chromium so the same script launches the harness on
72
+ # macOS dev boxes AND Linux VMs. Override with BH_CHROME_BIN.
73
+ if [ -n "${BH_CHROME_BIN:-}" ] && [ -x "$BH_CHROME_BIN" ]; then
74
+ echo "$BH_CHROME_BIN"; return 0
75
+ fi
76
+ for _p in \
77
+ "/Applications/Google Chrome.app/Contents/MacOS/Google Chrome" \
78
+ "/Applications/Chromium.app/Contents/MacOS/Chromium" \
79
+ "/usr/bin/google-chrome" "/usr/bin/google-chrome-stable" \
80
+ "/usr/bin/chromium" "/usr/bin/chromium-browser" "/snap/bin/chromium"
81
+ do
82
+ if [ -x "$_p" ]; then echo "$_p"; return 0; fi
83
+ done
84
+ for _n in google-chrome google-chrome-stable chromium chromium-browser; do
85
+ _which=$(command -v "$_n" 2>/dev/null) && [ -n "$_which" ] && { echo "$_which"; return 0; }
86
+ done
87
+ echo ""; return 1
88
+ }
89
+
90
+ hc_cdp_ready() {
91
+ # Real CDP readiness: complete an actual connect_over_cdp handshake against
92
+ # the harness. /json/version alone is a liveness probe that a WEDGED Chrome
93
+ # still passes (S4L-4H); see scripts/cdp_ready_check.py (which degrades to
94
+ # an http-only probe when playwright is not importable, so a bare-python3
95
+ # platform never false-wedges on a ModuleNotFoundError). Prints the probe's
96
+ # one-line JSON verdict on stdout; exit status is the verdict.
97
+ "${S4L_PYTHON:-python3}" "$_BH_REPO_DIR/scripts/cdp_ready_check.py" \
98
+ "${1:?cdp url}" 8000 2>/dev/null
99
+ }
100
+
101
+ hc_record_health() {
102
+ # Persist the latest readiness verdict where memory_snapshot.py picks it up
103
+ # (cdp_health block on the per-minute heartbeat sample). $1 = action tag,
104
+ # $2 = the probe's JSON verdict (may be empty). Best effort, never fails
105
+ # the caller.
106
+ local _f="${HC_HEALTH_FILE:-$_BH_REPO_DIR/skill/logs/cdp-health-${HC_PLATFORM:-unknown}.json}"
107
+ printf '{"ts":"%s","url":"%s","action":"%s","verdict":%s}\n' \
108
+ "$(_hc_ts)" \
109
+ "${HC_CDP_URL:-${HC_DEFAULT_URL:-}}" \
110
+ "$1" \
111
+ "${2:-null}" \
112
+ > "$_f" 2>/dev/null || true
113
+ }
114
+
115
+ hc_cleanup_tabs() {
116
+ # Close every CDP "page" tab except one. $1 = port, $2 = log label.
117
+ # Delegated to a standalone Python script because bash 3.2 (what launchd
118
+ # uses) cannot parse a nested heredoc inside a function body inside a
119
+ # sourced file (broke every launchd-fired twitter script 2026-05-14).
120
+ #
121
+ # Health-check gate: 10s timeout + ONE retry (a busy Chrome blows a 2s
122
+ # probe, cleanup silently skips, and orphan tabs pile up; 2026-05-16).
123
+ # Log skips so they are not silent.
124
+ local _port="${1:?port}" _label="${2:-harness}"
125
+ local _probe="curl -sf --max-time 10 -o /dev/null http://127.0.0.1:${_port}/json/version"
126
+ if ! $_probe 2>/dev/null; then
127
+ sleep 1
128
+ if ! $_probe 2>/dev/null; then
129
+ echo "[$(_hc_ts)] cleanup_harness_tabs: SKIPPED (${_label} CDP /json/version unreachable after 10s+retry)" >&2
130
+ return 0
131
+ fi
132
+ fi
133
+ BH_CLEANUP_PORT="$_port" python3 "$_BH_REPO_DIR/scripts/cleanup_harness_tabs.py" 2>/dev/null || true
134
+ }
135
+
136
+ _hc_reap_profile_owners() {
137
+ # Kill any Chrome holding EXACTLY this profile dir (trailing space in the
138
+ # pattern keeps browser-harness from matching browser-harness-linkedin),
139
+ # then clear the Singleton* files so the next launch binds our port instead
140
+ # of handing off via Chrome's SingletonLock and looping "failed to start
141
+ # within 12s" (2026-06-03; 8h twitter outage root cause).
142
+ local _prof="${1:?profile dir}"
143
+ local _pids
144
+ _pids=$(pgrep -f -- "--user-data-dir=$_prof " 2>/dev/null || true)
145
+ if [ -n "$_pids" ]; then
146
+ kill $_pids 2>/dev/null || true
147
+ sleep 2
148
+ _pids=$(pgrep -f -- "--user-data-dir=$_prof " 2>/dev/null || true)
149
+ [ -n "$_pids" ] && { kill -9 $_pids 2>/dev/null || true; sleep 1; }
150
+ fi
151
+ rm -f "$_prof/SingletonLock" "$_prof/SingletonSocket" "$_prof/SingletonCookie" 2>/dev/null || true
152
+ }
153
+
154
+ hc_ensure_browser() {
155
+ local _plat="${HC_PLATFORM:?HC_PLATFORM}" _port="${HC_PORT:?HC_PORT}"
156
+ local _prof="${HC_PROFILE_DIR:?HC_PROFILE_DIR}"
157
+ local _def_url="${HC_DEFAULT_URL:?HC_DEFAULT_URL}"
158
+ local _cdp_url="${HC_CDP_URL:-$_def_url}"
159
+ local _marker="${HC_WEDGE_MARKER:-${_plat}_cdp_wedge}"
160
+
161
+ # AppMaker / BYO Chrome: the platform CDP URL points at something other
162
+ # than our default harness URL. Don't touch that browser (it is the host's
163
+ # to restart); probe it, verify the handshake, and bail.
164
+ if [ "$_cdp_url" != "$_def_url" ]; then
165
+ if curl -sf --max-time 2 -o /dev/null "${_cdp_url}/json/version" 2>/dev/null; then
166
+ local _ext_verdict
167
+ if ! _ext_verdict=$(hc_cdp_ready "$_cdp_url"); then
168
+ echo "[$(_hc_ts)] ERROR: external Chrome at ${_cdp_url} is WEDGED (/json/version answers but the CDP handshake never completes). Host must restart it (AppMaker /opt/startup.sh, etc)." >&2
169
+ echo "${_marker}: detected url=${_cdp_url} action=none-external" >&2
170
+ hc_record_health external_wedged "$_ext_verdict"
171
+ return 1
172
+ fi
173
+ hc_record_health ok-external "$_ext_verdict"
174
+ echo "[$(_hc_ts)] Using externally-managed Chrome at ${_cdp_url} (skipping harness launch + tab cleanup)" >&2
175
+ _hc_run_hook "${HC_EXTERNAL_OK_HOOK:-}" || true
176
+ return 0
177
+ fi
178
+ echo "[$(_hc_ts)] ERROR: ${_plat} CDP url ${_cdp_url} not reachable. External Chrome must be managed by host." >&2
179
+ return 1
180
+ fi
181
+
182
+ # Platform pre-launch gate (linkedin's one-driver-per-Chrome lock). A
183
+ # nonzero rc propagates untouched so the reserved skip code 78 reaches the
184
+ # caller's parent shell.
185
+ _hc_run_hook "${HC_PRE_LAUNCH_HOOK:-}" || return $?
186
+
187
+ # Probe + launch harness Chrome if needed. Two-stage probe: /json/version
188
+ # (liveness) then a real CDP handshake (readiness). A wedged Chrome passes
189
+ # the first and fails the second; handing it downstream made every attach
190
+ # eat Playwright's 180s default while holding the browser lock (S4L-4H).
191
+ local _need_launch=0 _launch_reason="" _ready_verdict=""
192
+ if ! curl -sf --max-time 2 -o /dev/null "${_def_url}/json/version" 2>/dev/null; then
193
+ _need_launch=1; _launch_reason="http_down"
194
+ echo "[$(_hc_ts)] ${_plat} harness Chrome down on port ${_port}, launching..." >&2
195
+ else
196
+ # TWO-STRIKE gate (2026-07-13): one failed handshake is NOT proof of a
197
+ # wedge. Under heavy system load a perfectly healthy Chrome blows the
198
+ # 8s attach budget, and a single-failure kill produced 3 false
199
+ # wedge-kills in one day. First failure: stamp a strike file and
200
+ # proceed WITHOUT killing (a truly wedged Chrome makes the cycle's own
201
+ # pre-flight fail loudly right after). Second consecutive failure
202
+ # within 30 minutes: genuinely wedged, kill + relaunch. A passing
203
+ # check clears any stale strike.
204
+ local _strike="/tmp/s4l_cdp_wedge_strike_${_port}"
205
+ if ! _ready_verdict=$(hc_cdp_ready "$_def_url"); then
206
+ if [ -z "$(find "$_strike" -mmin -30 2>/dev/null)" ]; then
207
+ touch "$_strike"
208
+ echo "[$(_hc_ts)] ${_plat} harness Chrome failed the CDP handshake check (${_ready_verdict:-no verdict}); first strike, NOT killing (loaded-machine tolerance). Second consecutive failure within 30m triggers the wedge heal." >&2
209
+ echo "${_marker}: detected url=${_def_url} action=first_strike" >&2
210
+ hc_record_health wedge_first_strike "$_ready_verdict"
211
+ # Fall through WITHOUT killing: post-launch hook + tab cleanup
212
+ # below still run.
213
+ else
214
+ rm -f "$_strike" 2>/dev/null || true
215
+ _need_launch=1; _launch_reason="cdp_wedge"
216
+ echo "[$(_hc_ts)] ${_plat} harness Chrome WEDGED on port ${_port} (second consecutive handshake failure: ${_ready_verdict:-no verdict}); killing and relaunching..." >&2
217
+ # Machine-greppable marker (same stderr-marker convention as
218
+ # twitter_access_gate; bin/server.js parses the twitter one).
219
+ echo "${_marker}: detected url=${_def_url} action=restart" >&2
220
+ hc_record_health wedge_restart "$_ready_verdict"
221
+ _hc_reap_profile_owners "$_prof"
222
+ fi
223
+ else
224
+ # Healthy handshake: clear any earlier strike so isolated blips
225
+ # never accumulate into a kill.
226
+ rm -f "$_strike" 2>/dev/null || true
227
+ fi
228
+ fi
229
+
230
+ if [ "$_need_launch" = 1 ]; then
231
+ local _chrome_bin
232
+ _chrome_bin=$(_resolve_chrome_bin)
233
+ if [ -z "$_chrome_bin" ]; then
234
+ echo "[$(_hc_ts)] ERROR: no Chrome/Chromium binary found. Set BH_CHROME_BIN." >&2
235
+ return 1
236
+ fi
237
+ # Dated relaunch stamp for central observability: memory_snapshot.py
238
+ # counts these (chrome_relaunches block) so a kill-respawn loop is
239
+ # visible per-install without depending on a foreground observer.
240
+ # chrome= carries the binary version at launch time: a version change
241
+ # between consecutive lines marks exactly when a silent auto-update
242
+ # went live (how the 2026-07-12 Chrome 150 crash-storm onset was
243
+ # dated: update on disk 07-10, active only at the 07-12 reboot).
244
+ local _chrome_ver
245
+ _chrome_ver=$("$_chrome_bin" --version 2>/dev/null | grep -oE '[0-9]+(\.[0-9]+)+' | head -1)
246
+ echo "$(_hc_ts) port=${_port} reason=${_launch_reason} chrome=${_chrome_ver:-unknown}" \
247
+ >> "$_BH_REPO_DIR/skill/logs/chrome-relaunch-events.log" 2>/dev/null || true
248
+ # On Linux + no display, run headless. On root, add --no-sandbox.
249
+ # Window-position/size only meaningful on macOS multi-monitor; skip
250
+ # elsewhere so we don't hide the window off-screen on single-display
251
+ # Linux VMs. Built as a plain string (word-split on purpose) because
252
+ # an empty array expansion trips bash 3.2 under set -u.
253
+ local _os_extra=""
254
+ case "$(uname -s)" in
255
+ Linux)
256
+ _os_extra="--no-sandbox --disable-dev-shm-usage"
257
+ if [ -z "${DISPLAY:-}" ] && [ -z "${WAYLAND_DISPLAY:-}" ]; then
258
+ _os_extra="$_os_extra --headless=new --disable-gpu"
259
+ fi
260
+ ;;
261
+ Darwin)
262
+ _os_extra="--window-position=${HC_WINDOW_POS:-2000,-1032} --window-size=${HC_WINDOW_SIZE:-1024,1013}"
263
+ ;;
264
+ esac
265
+ # Self-heal: reap any stale Chrome holding THIS profile dir but not
266
+ # answering CDP on our port, else the relaunch hands off via the
267
+ # SingletonLock and loops "failed to start within 12s".
268
+ local _stale_pids
269
+ _stale_pids=$(pgrep -f -- "--user-data-dir=$_prof " 2>/dev/null || true)
270
+ if [ -n "$_stale_pids" ] && ! curl -sf --max-time 2 -o /dev/null "${_def_url}/json/version" 2>/dev/null; then
271
+ echo "[$(_hc_ts)] CDP down but Chrome still holds $_prof (pids: $(echo $_stale_pids | tr '\n' ' ')); reaping stale profile owner before relaunch" >&2
272
+ _hc_reap_profile_owners "$_prof"
273
+ fi
274
+ # Spawn via the SHARED launcher (skill/lib/browser-launch.sh): clean-
275
+ # exit stamp (no Aw-Snap restore), `open -n -g` on macOS (no focus
276
+ # steal, out of this job's pgroup), setsid exec fallback elsewhere.
277
+ # The occlusion/backgrounding flags matter for every platform: the
278
+ # window sits offscreen, and without them Chrome stops laying out
279
+ # SPA-rendered content, so elements measure 0x0 and coordinate clicks
280
+ # become impossible (2026-07-03).
281
+ # shellcheck disable=SC1091
282
+ source "$(cd "$(dirname "${BASH_SOURCE[0]}")" && pwd)/browser-launch.sh"
283
+ # shellcheck disable=SC2086
284
+ # --disable-hang-monitor: standard automation flag. Without it Chrome's
285
+ # hang monitor kills renderers that stop responding while CDP-driven
286
+ # scans hammer them under system load — the leading explanation for the
287
+ # 2026-07-14/15 EXC_BREAKPOINT (deliberate internal abort) crash dumps
288
+ # arriving ~once per cycle with zero jetsam/OOM evidence. Playwright
289
+ # and Puppeteer both launch with it for exactly this reason.
290
+ launch_harness_chrome "$_chrome_bin" "$_prof" \
291
+ --remote-debugging-port="$_port" \
292
+ --user-data-dir="$_prof" \
293
+ --no-first-run --no-default-browser-check \
294
+ --disable-hang-monitor \
295
+ --disable-features=ChromeWhatsNewUI,CalculateNativeWinOcclusion \
296
+ --disable-backgrounding-occluded-windows \
297
+ ${HC_EXTRA_FLAGS:-} \
298
+ ${_os_extra} \
299
+ "${HC_LAUNCH_URL:-about:blank}"
300
+ for _i in 1 2 3 4 5 6 7 8 9 10 11 12; do
301
+ curl -sf --max-time 2 -o /dev/null "${_def_url}/json/version" 2>/dev/null && break
302
+ sleep 1
303
+ done
304
+ if ! curl -sf --max-time 2 -o /dev/null "${_def_url}/json/version" 2>/dev/null; then
305
+ echo "[$(_hc_ts)] ERROR: ${_plat} harness Chrome failed to start within 12s" >&2
306
+ hc_record_health launch_failed null
307
+ return 1
308
+ fi
309
+ # Verify the fresh Chrome actually completes a CDP handshake before
310
+ # declaring victory; a relaunch that comes up wedged again should fail
311
+ # the cycle loudly, not feed 180s hangs downstream.
312
+ local _post_verdict
313
+ if ! _post_verdict=$(hc_cdp_ready "$_def_url"); then
314
+ echo "[$(_hc_ts)] ERROR: ${_plat} harness Chrome answers HTTP after relaunch but the CDP handshake is STILL failing: ${_post_verdict:-no verdict}" >&2
315
+ echo "${_marker}: detected url=${_def_url} action=relaunch_failed" >&2
316
+ hc_record_health relaunch_failed "$_post_verdict"
317
+ return 1
318
+ fi
319
+ hc_record_health relaunched "$_post_verdict"
320
+ echo "[$(_hc_ts)] ${_plat} harness Chrome up on port ${_port}" >&2
321
+ else
322
+ hc_record_health ok "${_ready_verdict:-null}"
323
+ fi
324
+
325
+ # Platform post-launch hook (twitter session restore). Runs on both the
326
+ # freshly-launched and already-up paths so a mid-life logout heals. Best
327
+ # effort; never blocks the cycle on failure.
328
+ _hc_run_hook "${HC_POST_LAUNCH_HOOK:-}" || true
329
+
330
+ # Always close leftover tabs from prior runs. Safe under the caller's
331
+ # browser-lock serialization.
332
+ hc_cleanup_tabs "$_port" "${_plat}-harness"
333
+
334
+ # Platform post-cleanup gate (linkedin logout detect-gate). Its rc IS the
335
+ # ensure rc, so a confirmed logout aborts this fire.
336
+ _hc_run_hook "${HC_POST_CLEANUP_HOOK:-}"
337
+ }
338
+
339
+ hc_defer_if_foreign() {
340
+ # Harness Chrome accepts multiple concurrent CDP clients on the same
341
+ # profile, so a foreign MCP wrapper cannot cause SingletonLock contention.
342
+ # Always return 1 (do not defer).
343
+ return 1
344
+ }