Originally created by: imshaikot
Closes [#32].
Browsentic could not run anything later unless an agent set it up mid-conversation. page.startTimer belongs to one conversation, lives in storage.session, only takes relative intervals of up to 24 hours, and nothing in the panel shows it. There was no way to say "every weekday at 09:00, open github.com/pulls and summarise what is waiting on me", and no way to repeat a recording on a schedule.
Seven commits, each passing yarn check on its own.
1. fix(timer): a fire with no daemon is a skip. onTimerFire returned 'delivered' even when startTurn came back 'offline'. The timer then logged a run that never happened. It now counts as a skip with its own reason, and retries.
2. feat(daemon): the browser can start the daemon. New permission: nativeMessaging.
browsentic setup registers a native messaging host, com.browsentic.daemon, for every installed Chrome, Chrome for Testing, Chromium, Edge, Brave, Vivaldi, Arc and Firefox. Windows uses registry keys.PATH setup saw. Without that, a daemon started from a Dock-launched browser would not find claude or codex. If the CLI has moved, for example out of a cleaned npx cache, the launcher falls back to npx browsentic.allowed_origins lists the unpacked extension's id, derived from its folder the same way Chrome derives it (checked against a real paired id), plus every paired origin. Pairing rewrites it.sendNativeMessage at most once a minute. The host runs ensureDaemon(), replies, and exits.status reports which browsers have the host; uninstall removes it.3. feat(schedules): scheduled tasks.
src/daemon/schedules/). Tasks and each task's last 20 runs live in ~/.browsentic/schedules.json, mode 0600.setTimeout armed for the exact time to the next run, capped at 60 s, re-reading the file on every wake. It fires on time, catches up within a minute of the machine waking, follows DST, and never hits the 24.8-day setTimeout overflow.src/lib/schedules/): once, on chosen days at chosen times, or every N minutes, optionally within a daily window. Local time throughout, so 09:00 stays 09:00 across DST.runTask goes daemon → extension and is answered like an invoke. taskDone reports a finished run. taskList is pushed on connect and after every change. tasks, saveTask, deleteTask, runTaskNow and pauseTasks serve the panel.startTurn, the Send button's path. The agent is told nobody is watching, and is given the last run's one-line result.taskDone, files the transcript under browsentic:taskRuns (never History, whose 50 slots a frequent task would flush), shows a notice and closes the tab.isTrusted clicks, and none in its first 800 ms. Its shadow root is closed, and it only answers for the tab it was drawn on. With no answer in 10 minutes the daemon declines the action, returning APPROVAL_TIMEOUT. A scheduled run is also capped at 20 minutes and never gets Live tools.browsentic tasks [pause|resume|delete] [id], with --json.4. docs(schedules). The scheduling guide, state on disk, CLI and error references, troubleshooting, and extension internals.
5. fix(daemon): a held call is kept alive. The first real scheduled agent run found this. RemoteBridge, the stdio MCP server each agent run talks through, gave up on any invoke after 60 s with DAEMON_UNREACHABLE. An approval holds its invoke for as long as it is open, which is 10 minutes for a scheduled run. So the agent heard "unreachable" a minute in and tried submitForm again, which raised a second card. The first card stayed up and would still have run the action if someone clicked Allow. The run ended on its 20-minute budget instead of saying what it was waiting on.
keepAlive with every invoke. The daemon answers working every 2 s until the result, and the bridge restarts its timeout on each. Only a daemon that goes quiet is unreachable.browsentic, is never sent a frame it can't read.6. fix(daemon): browsentic stop stays stopped. With the wake-up in place, a paired browser redialled about a second after a stop, found nothing and had the host start a new daemon, so stop was undone at once. That also hit the Mac app's Stop and any script that stops the daemon before swapping files. If ~/.browsentic/cli was already gone, the launcher's npx fallback would start the npm release.
stop and restart leave ~/.browsentic/stopped. While it exists, the host replies that the daemon was stopped and starts nothing. The next daemon to start, by any other path (start, setup, an MCP client, the app), removes it.status shows the wake-up as held; the stop output and the docs say how to lift it.uninstall unregisters the host before stopping the daemon, so a browser watching it go cannot start another.7. fix(schedules): review fixes.
runId. A report that arrives after the daemon gave up on the run (the 30 s start timeout, a disconnect, or the 25-minute silence) replaces that entry instead of adding a second one, and a late report from an older run no longer clears the in-flight mark of the current one.task.previous came from a page and went into the next run's system prompt as plain text; it is now inside the same untrusted-data fence as tool results.yarn check on its own. The held run, the hand-back on reconnect, a late report replacing the logged failure, a stale report, keeping a passed time, the fenced result, the host refusing while stopped and a starting daemon clearing the stop are all covered. Rules cover DST (a 23-hour day, a 25-hour day, 02:30 on spring-forward), windows past midnight, and validation. The scheduler runs on fake timers, including sleep catch-up, skip, missed, maxRuns, pause and a disconnect mid-run. The replay planner, outcome and notice rules, and native-host manifests, launcher and message framing are covered too.test/tasks.test.ts): save, run now, taskDone into the log, the list pushed on connect, a refused run, and a browser dropping mid-run.ok with the headline "Example Domain", filed the transcript and closed the tab.runId, and the card shows only Deny and Allow.stop by hand against a scratch daemon on its own port and home: stop left the mark, the host then replied {"ok":false,…} and nothing came up on the port; start removed the mark; with no mark, the host started a daemon as before.ok with the headline The page title is "Example Domain".page.submitForm: the approval card appeared on the page in front, not in the task's background tab. A trusted click on Allow let the run continue. The local server received the submission, and the headline read The page now says "Thanks, Ada".got no answer, no retry, no second card, and nothing submitted. The agent stopped 1.7 s later with Approve the submit and re-run the task to finish it. Before commit 5, the same run heard DAEMON_UNREACHABLE and asked again twice.NativeMessagingHosts folder, which Chrome for Testing reads on macOS. The daemon was killed, and the panel came back online by itself through a daemon the host started. For a browser returning on its session key, that took 2 s. The first kill hit the socket that had just paired, which only the minute reconnect alarm redials, so that one took 32 s.{"op":"ensure"} sent to the built CLI started a detached daemon and got {"ok":true,"port":…} back.yarn build, yarn build:firefox and lint:firefox pass (0 errors; the 11 warnings are all pre-existing bundles). nativeMessaging is in both manifests.nativeMessaging may wait for the user to accept the new permission. Agent tasks that take screenshots need the tab in front on Firefox, so they will not work from a background tab.[#38] (live model lists) merged first and took 19, so this branch is rebased onto main and takes SOCKET_PROTOCOL_VERSION to 20. The browser check above was rerun on the rebased build.
[#40] (hands-free) merged first, so main is merged into this branch. The conflicts in App.tsx, run-port.ts and wxt.config.ts keep both sides: the manifest asks for nativeMessaging and offscreen, /hands-free is checked before a scheduling send, and the orb's approve goes through the same answerApproval that clears a task's card. [#40] didn't change the protocol, so it stays at 20. After the merge, yarn check passes with 1832 tests, both builds pass, lint:firefox shows 0 errors, and the browser check above passes again.
task.browser.browsentic tasks run <id>. Running needs a browser, so it lives in the panel for now.mcp_servers.browsentic.tool_timeout_sec the way the OpenCode runner sets its timeout.🤖 Generated with Claude Code
Ticket changed by: imshaikot