← back to Kickbacks Ai Upstream
src/adapters/codex/block.asset.js
559 lines
(function () {
"use strict";
// Injected as the first statement of Codex's ThinkingShimmer entry:
// function v(e){ e=(<THIS IIFE>)||e; ... }
// It ALWAYS returns undefined → `e = undefined || e` → Codex's component
// runs completely untouched. We do NOT swap e.message / return a JSX
// element: any mutation of (or substitution into) Codex's React-Compiler
// tree is torn out on the next reconcile (the finnicky no-render). Instead,
// exactly like the proven Claude Code block, we read Codex's thinking row
// READ-ONLY and paint the ad in OUR OWN element appended to <body>, OUTSIDE
// React's roots. Rendering is pure DOM (no network) so it works even though
// Codex's webview CSP blocks the loopback fetch (telemetry/billing only).
// Any throw is swallowed and undefined returned — Codex must never break.
try {
if (window.__vibeAdsCodexBoot) return undefined; // one bootstrap / webview
window.__vibeAdsCodexBoot = 1;
} catch (e) { return undefined; }
try {
var AD = __VIBE_ADS_AD__;
// Ad identity for the live /ad poll's change detection (see pollAd).
// Patch-time bake carries no adId; the first poll response fills it in.
var AD_ID = "";
var CLICKURL = __VIBE_ADS_CLICKURL__;
var CLICKTOKEN = __VIBE_ADS_CLICKTOKEN__;
var CORR = __VIBE_ADS_CORR__, DEBUG = __VIBE_ADS_DEBUG__;
var PORT = __VIBE_ADS_PORT__, LBTOKEN = __VIBE_ADS_LBTOKEN__;
var BASE = __VIBE_ADS_BASE__ ||
("http://127.0.0.1:" + PORT + "/vibe-ads/" + LBTOKEN);
var GRACE_MS = 1500;
function esc(s) {
return String(s).replace(/[&<>"]/g, function (c) {
return { "&": "&", "<": "<", ">": ">", '"': """ }[c];
});
}
function ell(f) { return ["", " .", " ..", " ..."][f % 4]; }
function elp(ms) { return (ms / 1000).toFixed(1) + "s"; }
function dlog(evt, data) {
if (!DEBUG) return;
try {
var o = { evt: evt, corr: CORR, t: "codex" };
if (data) for (var k in data) o[k] = data[k];
fetch(BASE + "/log", { method: "POST", keepalive: true,
headers: { "content-type": "application/json" },
body: JSON.stringify(o) }).catch(function () {});
} catch (e) {}
}
function ping(kind) {
try {
fetch(BASE + "/" + kind, { method: "POST", keepalive: true })
.catch(function () {});
} catch (e) {}
}
function newEventUuid() {
try {
if (typeof crypto !== "undefined"
&& typeof crypto.randomUUID === "function") {
return crypto.randomUUID();
}
} catch (e) { /* fall through */ }
try {
return "xxxxxxxx-xxxx-4xxx-yxxx-xxxxxxxxxxxx".replace(/[xy]/g,
function (c) {
var r = Math.random() * 16 | 0;
var v = c === "x" ? r : (r & 0x3 | 0x8);
return v.toString(16);
});
} catch (e) { return "evt-" + Date.now() + "-" + Math.random(); }
}
dlog("codex.boot", { href: String(location.href).slice(0, 80) });
// ---- W3 view-time accumulator (mirrors claude-code/block.asset.js) -----
// An ad must accumulate THRESHOLD_MS of cumulative visible time on the
// codex_overlay surface before it counts as a billable view. Threshold
// is server-overridable via /v1/portfolio.view_threshold_seconds, baked
// into the block as __VIBE_ADS_VIEW_THRESHOLD_MS__ (fallback 15s). Pure
// best-effort: any throw is swallowed.
var THRESHOLD_MS = (typeof __VIBE_ADS_VIEW_THRESHOLD_MS__ === "number"
&& __VIBE_ADS_VIEW_THRESHOLD_MS__ > 0)
? __VIBE_ADS_VIEW_THRESHOLD_MS__ : 15000;
var TICK_MS = 5000;
// MAX_SESSION_MS billing cap. Pinned to THRESHOLD_MS (was a hard 5000)
// so the 15s `view_threshold_met` fires FIRST and becomes the billing
// path; the moment it fires the mutex (errorImpressionCount===0 check)
// suppresses error_impression for the rest of the session. Previously
// cap(5s) < threshold(15s) forced EVERY codex view down the
// error_impression path — and because cooldown_view_seconds is also 5s
// (and the codex block never got the c22c9aa client-side
// view_tick/error_impression sync), a long-lived or backgrounded codex
// panel billed a phantom error_impression (tier_error_impression_micros,
// ~$0.01) roughly every 5s indefinitely, up to the per-user daily cap.
// With cap===threshold, error_impression is now only a genuine fallback
// (fires solely if threshold_met somehow didn't). See fixes #1/#3 below.
var MAX_SESSION_MS = THRESHOLD_MS;
var SESSION_NONCE = (function () {
try {
return (Math.random().toString(36).slice(2)
+ Math.random().toString(36).slice(2)).slice(0, 16);
} catch (e) { return "s" + Date.now(); }
})();
var _vt = Object.create(null);
function vtKey(adId, surface) { return surface + ":" + adId; }
function viewShow(adId, surface) {
if (!adId) return;
var k = vtKey(adId, surface);
var s = _vt[k];
if (!s) {
// Sticky sessionStartedAt — repeat viewShow() with same nonce does
// NOT restart the baseline. `paused`/`pausedAt` carry fix #3 (freeze
// while the webview is hidden). The session is ENDED (not merely
// hidden) when the overlay drops at idle — see viewEnd / dropOverlay.
_vt[k] = { adId: adId, surface: surface,
sessionNonce: SESSION_NONCE,
sessionStartedAt: Date.now(),
lastTickMs: 0, thresholdMet: false,
errorImpressionCount: 0,
paused: false, pausedAt: 0 };
}
}
// Fix #1: END a view session outright (used when the overlay drops at
// idle). The old viewHide was a no-op, so once viewShow created a session
// the 250ms viewTick kept accumulating elapsed time FOREVER — even after
// the ad left the screen — which is exactly the 31-minute "stuck" codex
// session that spammed error_impression. Removing the record stops the
// accumulator; a later paint() calls viewShow() again for a FRESH session.
function viewEnd(adId, surface) {
try { delete _vt[vtKey(adId, surface)]; } catch (e) {}
}
function viewHide(_adId, _surface) {
// No-op kept for call-site compatibility; real teardown is viewEnd().
}
function viewMaybeEmit(s) {
// Fix #3: a hidden Codex webview must NOT accrue view-time. (The Claude
// adapter deliberately keeps counting when hidden under its absolute-
// epoch model; Codex does the opposite — a backgrounded panel was the
// other half of the phantom-billing loop.) While document.hidden we
// freeze: mark paused (stamping pausedAt once) and emit nothing. On the
// next visible poll we shift sessionStartedAt forward by the hidden gap
// so off-screen time is excluded from elapsed, then resume mid-session
// (lastTickMs / errorImpressionCount untouched, so cadence continues).
var hidden = false;
try { hidden = (typeof document.hidden === "boolean") && document.hidden; }
catch (e) { hidden = false; }
if (hidden) {
if (!s.paused) { s.paused = true; s.pausedAt = Date.now(); }
return;
}
if (s.paused) {
s.sessionStartedAt += Math.max(0, Date.now() - (s.pausedAt || Date.now()));
s.paused = false; s.pausedAt = 0;
}
var elapsed = Math.max(0, Date.now() - s.sessionStartedAt);
var tickFired = false;
while (elapsed - s.lastTickMs >= TICK_MS) {
s.lastTickMs += TICK_MS;
tickFired = true;
var tickEventUuid = newEventUuid();
var q = "?surface=" + encodeURIComponent(s.surface)
+ "&ad=" + encodeURIComponent(s.adId)
+ "&visible_ms=" + s.lastTickMs
+ "&session=" + encodeURIComponent(s.sessionNonce)
+ "&event_uuid=" + encodeURIComponent(tickEventUuid);
ping("view_tick" + q);
}
// Mutex: threshold_met fires once per session, and only when no
// error_impression has fired yet. With cap===threshold (fix #2) the
// threshold check runs first at elapsed>=THRESHOLD_MS and wins, so
// threshold_met is now the PRIMARY billing path for codex (it used to
// never fire because the 5s cap tripped error_impression first).
if (!s.thresholdMet && s.errorImpressionCount === 0
&& elapsed >= THRESHOLD_MS) {
s.thresholdMet = true;
var thresholdEventUuid = newEventUuid();
var q2 = "?surface=" + encodeURIComponent(s.surface)
+ "&ad=" + encodeURIComponent(s.adId)
+ "&visible_ms=" + elapsed
+ "&threshold_ms=" + THRESHOLD_MS
+ "&session=" + encodeURIComponent(s.sessionNonce)
+ "&event_uuid=" + encodeURIComponent(thresholdEventUuid);
dlog("codex.view.threshold_met", { adId: s.adId, surface: s.surface,
visibleMs: elapsed, eventUuid: thresholdEventUuid });
ping("view_threshold_met" + q2);
}
var nextFireAt = (s.errorImpressionCount + 1) * MAX_SESSION_MS;
if (!tickFired && !s.thresholdMet && elapsed >= nextFireAt) {
s.errorImpressionCount += 1;
var errorEventUuid = newEventUuid();
var q3 = "?surface=" + encodeURIComponent(s.surface)
+ "&ad=" + encodeURIComponent(s.adId)
+ "&visible_ms=" + elapsed
+ "&max_session_ms=" + MAX_SESSION_MS
+ "&fire=" + s.errorImpressionCount
+ "&session=" + encodeURIComponent(s.sessionNonce)
+ "&event_uuid=" + encodeURIComponent(errorEventUuid);
dlog("codex.view.error_impression", { adId: s.adId,
surface: s.surface, visibleMs: elapsed,
fire: s.errorImpressionCount, eventUuid: errorEventUuid });
ping("error_impression" + q3);
}
}
function viewTick() {
try {
for (var k in _vt) viewMaybeEmit(_vt[k]);
} catch (e) {}
}
setInterval(viewTick, 250);
// Click-threshold companion: snapshot the running visible_ms for an
// ad+surface so the extension loopback can apply a floor (clicks
// before X ms of cumulative visible time get logged but not billed).
function viewVisibleMsNow(adId, surface) {
try {
var s = _vt[vtKey(adId, surface)];
if (!s) return 0;
return Math.max(0, Date.now() - s.sessionStartedAt);
} catch (e) { return 0; }
}
// Hidden-webview accounting (fix #3): unlike the Claude adapter, a hidden
// Codex panel does NOT keep counting — viewMaybeEmit pauses the
// accumulator while document.hidden is true and resumes (shifting the
// baseline past the hidden gap) when it's shown again. The 250ms poll
// observes the visibility flip within a tick, so no separate
// visibilitychange listener is needed.
var FG = "var(--vscode-foreground,currentColor)";
var DIM = "var(--vscode-descriptionForeground,currentColor)";
var FAV = '<svg width="13" height="13" viewBox="0 0 13 13" ' +
'aria-hidden="true" style="vertical-align:middle;border-radius:3px;' +
'flex:0 0 auto"><rect width="13" height="13" rx="3" fill="#188a45"/>' +
'<text x="6.5" y="9.6" font-size="9" font-family="monospace" ' +
'font-weight="700" text-anchor="middle" fill="#fff">K</text></svg>';
function buildAd(dots, elapsed) {
var href = /^https?:\/\//i.test(CLICKURL || "") ? esc(CLICKURL) : "#";
// Favicon lives INSIDE the anchor (matches CC's adapter shape) so a
// probe like `[data-vibe-ads-ad] svg` finds it — the row 08 / 09
// adHasFavicon check used to fail because the SVG was a sibling of
// the anchor, not a descendant. Visual is unchanged: favicon stays
// immediately left of the ad text, just now part of the link
// (clicking the favicon now navigates, which is the desired UX).
var A = '<a href="' + href + '" target="_blank" rel="noopener noreferrer" ' +
'data-vibe-ads-ad="1" style="display:inline-flex;align-items:center;' +
'gap:7px;color:' + FG +
';text-decoration:underline;overflow:hidden;text-overflow:ellipsis">' +
FAV + esc(AD) + '<span style="display:inline-block;width:3ch;' +
'text-align:left;white-space:pre">' + esc(dots) + "</span></a>";
var left = '<span style="display:flex;align-items:center;gap:7px;' +
'color:' + FG + ';min-width:0">' + A + "</span>";
var right = '<span style="font-size:11px;color:' + DIM +
';flex:0 0 auto;margin-left:auto;padding-left:24px;' +
'font-variant-numeric:tabular-nums">' + esc(elapsed) + "</span>";
return '<span style="display:flex;align-items:center;width:100%;' +
'box-sizing:border-box;padding:0 4px;justify-content:flex-start;' +
'white-space:nowrap">' + left + right + "</span>";
}
// Locate Codex's thinking-shimmer line READ-ONLY. The entry component `v`
// renders its text span with the stable class combo
// `text-size-chat … select-none truncate` (from the chunk:
// o=s("text-size-chat leading-[1.5] select-none truncate", className)).
// Require ALL THREE tokens so nothing else in the chat ever matches —
// and since we only READ its rect (never mutate it) a mis-match is at
// worst a cosmetic mispaint, never a prime-directive violation.
//
// ALSO require `loading-shimmer` (the live sweep marker) and a non-zero
// bounding rect. Codex 26.x keeps the post-turn "Thinking 1.2s" summary
// chip in the chat history with the SAME `text-size-chat truncate
// select-none` class combo and textContent "Thinking 1.2s", but as a
// display:none / hidden element — it does NOT carry `loading-shimmer-*`.
// Without the extra anchors the old predicate matched the static history
// chip and `isThinkingRow()` returned true forever — overlay never
// released at idle (row 03 regression) and stuck to turn 1 on multi-
// turn prompts (row 08, "glued to turn 1").
function findRow() {
var els = document.querySelectorAll(
'[class*="text-size-chat"][class*="truncate"]');
for (var i = 0; i < els.length; i++) {
var el = els[i];
if (el.nodeType !== 1) continue;
var c = " " + (el.className || "") + " ";
if (c.indexOf("select-none") === -1) continue;
if (c.indexOf("loading-shimmer") === -1) continue;
var r = el.getBoundingClientRect && el.getBoundingClientRect();
if (!r || (!r.width && !r.height)) continue;
// Honour visibility / opacity. Codex 26.x keeps the live shimmer
// element mounted briefly with `visibility:hidden` between the
// text-stream-end and the React unmount, so the row is technically
// present (rect non-zero, class trio still on) but invisible —
// exactly the post-stream state row 03 / codexBusy treat as
// "idle." Without this guard the overlay keeps painting on a
// hidden shimmer and isAnimating() catches our dot-cycle frames.
try {
var cs = window.getComputedStyle && window.getComputedStyle(el);
if (cs) {
if (cs.visibility === "hidden" || cs.display === "none") continue;
if (parseFloat(cs.opacity || "1") < 0.05) continue;
}
} catch (e) { /* jsdom / detached frame: fall through, pass */ }
return el;
}
return null;
}
// §4.4 gate, DOM-driven: show ONLY when the row is Codex's GENERIC
// "Thinking" placeholder. A real tool/approval/reasoning status renders
// different text (e.g. "Reading …", "Running …") → we yield (no overlay).
//
// Codex 26.x renders the shimmer as TWO stacked spans (base + sweep
// highlight overlay) so textContent concatenates to "ThinkingThinking".
// The previous /^thinking\b/i predicate failed against that string
// because `\b` requires a non-word char after "thinking" and the next
// char (`T`) is a word char — that was the regression causing rows
// 02 / 08 / 09 sawAd:false. Drop the word-boundary and rely on the
// length cap + the class-trio anchor in findRow().
function isThinkingRow(el) {
if (!el) return false;
var t = (el.textContent || "").trim();
// Empty was previously treated as "shimmer mounted, text incoming"
// but it's ALSO the post-completion state where Codex unmounts
// the text but leaves the wrapper span. That ambiguity made the
// ad linger past streaming (row 03 idle-release regression).
// Require an explicit "thinking" prefix; the transient empty
// window is brief enough that the GRACE_MS=1500 idle timer
// handles re-paint on subsequent turns without leaking idle paint.
if (!t) return false;
// accept "Thinking", "ThinkingThinking" (duplicated-span case),
// "Thinking…", "Thinking 1.2s" — all start with "thinking".
var lc = t.toLowerCase();
return lc.length <= 32 && lc.indexOf("thinking") === 0;
}
function surfaceBg(el) {
// The overlay sits on top of the "Thinking" row, so a transparent
// bg would let the underlying verb text bleed through. We need a
// colour, and it must match the surrounding chat-panel surface.
// The old strategy (first non-transparent ancestor) grabbed
// intermediate wrappers — chat bubble surfaces, hover tints —
// which differ in shade from the visible panel and made the ad
// look like a patch. Preferred chain:
// 1. nearest SCROLLABLE ancestor's bg (== the visible panel)
// 2. document.body's computed bg (== VS Code's webview theme)
// 3. CSS var fallback (theme-aware even when computed values
// aren't resolvable, e.g. detached frames)
try {
var n = el, hops = 0;
while (n && n.nodeType === 1 && hops++ < 20) {
var cs = window.getComputedStyle(n) || {};
var ov = cs.overflowY || cs.overflow;
if (ov === "auto" || ov === "scroll") {
var bg = cs.backgroundColor;
if (bg && bg !== "transparent" && bg !== "rgba(0, 0, 0, 0)")
return bg;
break; // found the panel; its bg is transparent
}
n = n.parentElement;
}
var bodyBg = (window.getComputedStyle(document.body) || {})
.backgroundColor;
if (bodyBg && bodyBg !== "transparent"
&& bodyBg !== "rgba(0, 0, 0, 0)") return bodyBg;
} catch (e) {}
return "var(--vscode-sideBar-background,"
+ "var(--vscode-editor-background,#1e1e1e))";
}
// Click-out: the anchor's real http(s) href is what the VS Code webview
// host opens externally (CSP-exempt) — the only click that survives
// Codex's `default-src 'none'`. Do NOT preventDefault. The ping is
// best-effort billing (revived by the Codex connect-src patch).
document.addEventListener("click", function (ev) {
var el = ev.target;
while (el && el !== document) {
if (el.getAttribute && el.getAttribute("data-vibe-ads-ad")) {
var vms = viewVisibleMsNow(AD, "codex_overlay");
var clickEventUuid = newEventUuid();
dlog("codex.click", { ct: CLICKTOKEN, visibleMs: vms,
eventUuid: clickEventUuid });
// `ad=` is the attribution CLAIM (parity with the CC block): the
// host's recent-ads registry resolves a click that lands during
// the ≤10s /ad poll lag after a rotation to the creative actually
// on screen instead of the freshly-rotated one.
ping("click?ct=" + encodeURIComponent(CLICKTOKEN) +
"&corr=" + encodeURIComponent(CORR) +
"&surface=codex_overlay" +
"&visible_ms=" + vms +
"&ad=" + encodeURIComponent(AD) +
"&event_uuid=" + encodeURIComponent(clickEventUuid));
return;
}
el = el.parentNode;
}
}, true);
var overlay = null, lastRow = null, lastSeenMs = 0, _rect = "";
var t0 = 0, frameN = 0, _shown = false, _sent = false;
function ensureOverlay(row) {
if (overlay && overlay.parentNode) return overlay;
overlay = document.createElement("div");
overlay.setAttribute("data-vibe-ads", "codex");
overlay.style.cssText =
"position:fixed;z-index:2147483646;pointer-events:auto;" +
"display:flex;align-items:center;box-sizing:border-box;" +
"overflow:hidden;white-space:nowrap;visibility:hidden;background:" +
surfaceBg(row);
try { (document.body || document.documentElement).appendChild(overlay); }
catch (e) {}
return overlay;
}
function placeOverlay(row) {
try {
var r = row.getBoundingClientRect();
if (r && (r.width || r.height || r.top || r.left)) {
var key = r.left + "," + r.top + "," + r.width + "," + r.height;
if (key !== _rect) {
_rect = key;
overlay.style.left = r.left + "px";
overlay.style.top = r.top + "px";
overlay.style.minWidth = r.width + "px";
overlay.style.height = r.height + "px";
overlay.style.visibility = "visible";
}
}
} catch (e) {}
}
function dropOverlay() {
// Fix #1: END the view session (was a no-op viewHide) so the
// accumulator stops the instant the ad leaves the screen at idle,
// instead of ticking off-screen forever. _sent reset below means the
// next turn opens a fresh impression session.
try { viewEnd(AD, "codex_overlay"); } catch (e) {}
try { if (overlay && overlay.parentNode)
overlay.parentNode.removeChild(overlay); } catch (e) {}
overlay = null; lastRow = null; _rect = ""; _shown = false; _sent = false;
}
function paint(row) {
var now = Date.now();
if (!t0) t0 = now;
lastRow = row; lastSeenMs = now;
frameN++;
if (!_shown) { _shown = true;
dlog("codex.show", { cls: String(row.className || "").slice(0, 60) }); }
if (!_sent) { _sent = true;
ping("impression_rendered?surface=codex_overlay&ad="
+ encodeURIComponent(AD)
+ "&event_uuid=" + encodeURIComponent(newEventUuid()));
var vis = (typeof document.hidden === "undefined")
? true : !document.hidden;
if (vis) {
ping("impression_viewable?surface=codex_overlay&ad="
+ encodeURIComponent(AD)
+ "&event_uuid=" + encodeURIComponent(newEventUuid()));
}
}
try { viewShow(AD, "codex_overlay"); } catch (e) {}
var o = ensureOverlay(row);
placeOverlay(row);
var html = buildAd(ell(Math.floor(frameN / 3)), elp(now - t0));
if (o.innerHTML !== html) o.innerHTML = html; // OUR node only
}
function frame() {
try {
if (overlay && lastRow && lastRow.isConnected) placeOverlay(lastRow);
} catch (e) {}
try { window.requestAnimationFrame(frame); }
catch (e) { setTimeout(frame, 16); }
}
try { window.requestAnimationFrame(frame); }
catch (e) { setTimeout(frame, 16); }
// ---- Live ad refresh (parity with claude-code/block.asset.js pollAd) --
// The baked __VIBE_ADS_AD__ used to be FROZEN for the life of the webview:
// rotation re-patched the bundle on disk but a running Codex panel never
// re-read it, so users sat on one creative — or the pre-inventory
// "Your ad here" placeholder — until a full reload (the "frozen ads on
// codex" complaint, 2026-06-11). Poll the loopback /ad every 10s:
// • changed payload → adopt AD/AD_ID/CLICKURL and RESET all view-time
// sessions + impression flags, so the old creative's accumulated time
// never bills against the new one (fresh session, fresh threshold).
// • successful-but-EMPTY payload ×2 (debounced, same as CC) → the
// host's no-serve signal (kill / disable / sign-out): drop the
// overlay, end every session, and suppress repaint until served.
// • fetch ERROR → keep the last ad (transient network / CSP without the
// connect-src patch — identical to today's behavior, no regression).
var _adEmptyPolls = 0, _noServe = false;
function pollAd() {
try {
fetch(BASE + "/ad").then(function (r) { return r.json(); })
.then(function (j) {
if (!j || !j.adText) {
_adEmptyPolls++;
if (_adEmptyPolls >= 2 && !_noServe) {
_noServe = true;
dlog("codex.no_serve", { polls: _adEmptyPolls });
// End EVERY session (not just the current key) before the
// drop: billing must stop with the serve, unconditionally.
_vt = Object.create(null);
dropOverlay(); t0 = 0; frameN = 0;
}
return;
}
_adEmptyPolls = 0;
if (_noServe) {
// Host resumed serving (possibly the same creative): re-arm.
// The overlay re-mounts on the next active 80ms tick.
_noServe = false;
dlog("codex.serve_resume", { adId: j.adId });
}
var changed = (j.adId && j.adId !== AD_ID) || (j.adText !== AD)
|| ((j.clickUrl || "") !== CLICKURL);
if (!changed) return;
dlog("codex.ad_rotated",
{ fromId: AD_ID, toId: j.adId, from: AD, to: j.adText });
AD_ID = j.adId || AD_ID;
AD = j.adText;
CLICKURL = j.clickUrl || "";
// Billing fairness on swap: end the old creative's sessions and
// re-arm the impression events — the next paint() opens a FRESH
// session for the new ad (viewShow keys on the ad text) and
// fires impression_rendered/viewable for it. paint()'s
// innerHTML-diff repaints the overlay with the new creative on
// the next 80ms tick (codex rewrites it every dot-frame anyway).
_vt = Object.create(null);
_sent = false; _shown = false;
}).catch(function () {
// Fetch ERROR: keep the last ad — deliberately NOT no-serve.
});
} catch (e) { /* prime directive */ }
}
setInterval(pollAd, 10000);
// One early poll so a patch-time placeholder ("Your ad here") or a
// rotation that landed between patch and boot is replaced within ~5s.
setTimeout(pollAd, 5000);
setInterval(function () {
try {
var now = Date.now();
var row = findRow();
if (!_noServe && row && isThinkingRow(row)) {
paint(row);
} else if (overlay && (now - lastSeenMs) > GRACE_MS) {
dlog("codex.idle", { sinceMs: now - lastSeenMs });
dropOverlay(); t0 = 0; frameN = 0;
}
} catch (e) {
dlog("codex.looperr",
{ msg: String(e && e.message || e).slice(0, 160) });
}
}, 80);
} catch (__vibeads) {
try {
var D = __VIBE_ADS_DEBUG__, C = __VIBE_ADS_CORR__,
B = __VIBE_ADS_BASE__ || ("http://127.0.0.1:" + __VIBE_ADS_PORT__ +
"/vibe-ads/" + __VIBE_ADS_LBTOKEN__);
if (D) fetch(B + "/log", { method: "POST", keepalive: true,
headers: { "content-type": "application/json" },
body: JSON.stringify({ evt: "codex.throw", corr: C, t: "codex",
msg: String(__vibeads && __vibeads.message || __vibeads)
.slice(0, 140) }) }).catch(function () {});
} catch (e) {}
}
return undefined; // ALWAYS — Codex's component untouched
})()