@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.
- package/mcp/dist/index.js +571 -79
- package/mcp/dist/panel.html +2 -3
- package/mcp/dist/redditAuth.js +115 -0
- package/mcp/dist/runtime.js +83 -2
- package/mcp/dist/setup.js +142 -5
- package/mcp/dist/telemetry.js +13 -3
- package/mcp/dist/version.json +2 -2
- package/mcp/manifest.json +1 -1
- package/mcp/menubar/s4l_card.py +32 -6
- package/mcp/menubar/s4l_menubar.py +42 -111
- package/mcp/menubar/s4l_state.py +54 -187
- package/mcp/package.json +1 -1
- package/mcp/shared/onboarding-ledger.cjs +63 -4
- package/package.json +1 -1
- package/scripts/autopilot_stall_watch.py +3 -6
- package/scripts/browser_mutex.py +316 -0
- package/scripts/claude_job.py +21 -0
- package/scripts/config.py +118 -0
- package/scripts/counterparty_history.py +44 -6
- package/scripts/engage_github.py +7 -3
- package/scripts/engage_reddit.py +9 -3
- package/scripts/engagement_signal_feedback.py +256 -0
- package/scripts/learned_preferences.py +37 -14
- package/scripts/memory_snapshot.py +45 -0
- package/scripts/merge_review_queue.py +67 -3
- package/scripts/post_reddit.py +289 -24
- package/scripts/reddit_browser.py +62 -63
- package/scripts/reddit_browser_fetch.py +32 -0
- package/scripts/release-mcpb.sh +17 -0
- package/scripts/reset-test-machine.sh +13 -7
- package/scripts/scan_twitter_thread_followups.py +7 -1
- package/scripts/schedule_state.py +1 -1
- package/scripts/setup_reddit_auth.py +925 -0
- package/scripts/snapshot.py +96 -4
- package/scripts/stats.py +37 -13
- package/scripts/test_no_silent_fallbacks.py +281 -0
- package/scripts/trigger_promo_lane_test.sh +91 -0
- package/scripts/twitter_browser.py +183 -342
- package/scripts/twitter_scan.py +75 -10
- package/scripts/watchdog_hung_runs.py +15 -9
- package/skill/lib/harness-common.sh +344 -0
- package/skill/lib/linkedin-backend.sh +61 -161
- package/skill/lib/reddit-backend.sh +39 -125
- package/skill/lib/twitter-backend.sh +51 -273
- package/skill/lock.sh +57 -0
- package/skill/run-reddit-search-launchd.sh +4 -1
- package/skill/run-reddit-search.sh +51 -2
- package/skill/run-twitter-cycle.sh +44 -21
package/scripts/twitter_scan.py
CHANGED
|
@@ -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
|
-
"""
|
|
236
|
-
|
|
237
|
-
|
|
238
|
-
|
|
239
|
-
|
|
240
|
-
|
|
241
|
-
|
|
242
|
-
|
|
243
|
-
|
|
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
|
-
|
|
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
|
|
41
|
-
#
|
|
42
|
-
#
|
|
43
|
-
#
|
|
44
|
-
#
|
|
45
|
-
#
|
|
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 =
|
|
56
|
-
HB_KILL_SEC =
|
|
57
|
-
HB_REWARN_SEC =
|
|
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
|
+
}
|