Skip to content

Commit 56e1b63

Browse files
abrichrclaude
andauthored
fix: add update_browse_history handler and fix verify_apps crash (#156)
Bug 6a: The YouTube task config uses "update_browse_history" to pre-populate Chrome's history.sqlite with URLs before the agent runs. This was being skipped as an unknown type. Now translates to a Python script that kills Chrome, finds the History DB, and INSERTs entries with proper Chrome timestamps (microseconds since 1601-01-01). Bug 6b: verify_apps crashed with "AttributeError: 'list' object has no attribute 'replace'" because the WAA task JSON uses "app" (list) but the handler expected "apps" (string). Now handles both parameter names and properly serializes the list with repr(). Co-authored-by: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
1 parent e898d96 commit 56e1b63

1 file changed

Lines changed: 59 additions & 5 deletions

File tree

  • openadapt_evals/adapters/waa

openadapt_evals/adapters/waa/live.py

Lines changed: 59 additions & 5 deletions
Original file line numberDiff line numberDiff line change
@@ -1589,14 +1589,68 @@ def _config_entry_to_command(entry: dict) -> str | None:
15891589
)
15901590

15911591
if entry_type == "verify_apps":
1592-
apps = params.get("apps", [])
1592+
# WAA uses both "apps" (list) and "app" (list) parameter names
1593+
apps = params.get("apps", params.get("app", []))
1594+
if isinstance(apps, str):
1595+
apps = [apps]
15931596
if not apps:
15941597
return None
1595-
apps_str = str(apps).replace("'", "\\'")
1598+
# Use repr() for safe list serialization instead of str().replace()
1599+
apps_repr = repr(apps)
15961600
return (
1597-
"import subprocess, shutil; "
1598-
f"missing = [a for a in {apps_str} if not shutil.which(a)]; "
1599-
"print(f'Missing apps: {{missing}}' if missing else 'All apps found')"
1601+
"import shutil; "
1602+
f"missing = [a for a in {apps_repr} if not shutil.which(a)]; "
1603+
"print(f'Missing apps: {missing}' if missing else 'All apps found')"
1604+
)
1605+
1606+
if entry_type == "update_browse_history":
1607+
history = params.get("history", [])
1608+
if not history:
1609+
return None
1610+
# Build Python script to write Chrome history entries.
1611+
# Chrome stores timestamps as microseconds since 1601-01-01.
1612+
history_repr = repr(history)
1613+
return (
1614+
"import sqlite3, os, time, subprocess, glob; "
1615+
# Kill Chrome so the DB isn't locked
1616+
"subprocess.run('taskkill /F /IM chrome.exe /T', "
1617+
"shell=True, capture_output=True); "
1618+
"time.sleep(1); "
1619+
# Find Chrome's History DB
1620+
"profiles = glob.glob("
1621+
"os.path.expandvars("
1622+
r"r'%LOCALAPPDATA%\\Google\\Chrome\\User Data\\*\\History'"
1623+
")); "
1624+
"db_path = profiles[0] if profiles else "
1625+
"os.path.expandvars("
1626+
r"r'%LOCALAPPDATA%\\Google\\Chrome\\User Data\\Default\\History'"
1627+
"); "
1628+
"conn = sqlite3.connect(db_path); "
1629+
"c = conn.cursor(); "
1630+
# Chrome epoch: microseconds since 1601-01-01
1631+
# Offset from Unix epoch: 11644473600 seconds
1632+
"CHROME_EPOCH_OFFSET = 11644473600; "
1633+
"now_us = int((time.time() + CHROME_EPOCH_OFFSET) * 1_000_000); "
1634+
f"history = {history_repr}; "
1635+
"["
1636+
"("
1637+
"c.execute("
1638+
"'INSERT OR IGNORE INTO urls (url, title, visit_count, "
1639+
"typed_count, last_visit_time) VALUES (?, ?, 1, 0, ?)', "
1640+
"(e['url'], e.get('title', ''), "
1641+
"now_us - int(e.get('visit_time_from_now_in_seconds', 0)) * 1_000_000)"
1642+
"), "
1643+
"c.execute("
1644+
"'INSERT INTO visits (url, visit_time, from_visit, transition, "
1645+
"segment_id, visit_duration) VALUES ("
1646+
"(SELECT id FROM urls WHERE url=?), ?, 0, 805306368, 0, 0)', "
1647+
"(e['url'], "
1648+
"now_us - int(e.get('visit_time_from_now_in_seconds', 0)) * 1_000_000)"
1649+
")"
1650+
") for e in history"
1651+
"]; "
1652+
"conn.commit(); conn.close(); "
1653+
"print(f'Inserted {len(history)} history entries into {db_path}')"
16001654
)
16011655

16021656
# Unknown type — skip

0 commit comments

Comments
 (0)