Skip to content

Add persistent Parallel watches - #39

Merged
shriyatheunicorn merged 6 commits into
browserbase:shriyatheunicorn/parallel-researchfrom
georgeatparallel:george/bell-monitors
Nov 3, 2026
Merged

shriyatheunicorn merged 6 commits into
browserbase:shriyatheunicorn/parallel-researchfrom
georgeatparallel:george/bell-monitors

Conversation

@georgeatparallel

@georgeatparallel georgeatparallel commented Oct 30, 2026 •

Copy link
Copy Markdown
Collaborator

Problem

Bell should keep watching a public topic after the conversation ends, at the frequency the user asks for.

Solution

Adds persistent watches through the Parallel Monitor API. From a private Linq chat, you can create a watch, list recent updates, change its frequency, or stop it. Checks default to daily and support whole hours, days, or weeks from 1 hour to 30 days.

Parallel runs the research. Bell saves state in Postgres and sends cited updates through Linq without keeping an agent or browser session open. Frequency changes reuse the existing monitor and stay visibly pending until confirmed. Retries preserve delivery identity, and recovery accepts timezone offsets without dropping valid events beside a malformed record.

Apply migrations 004 and 005, configure credentials, and start the worker scheduler to enable watches. Stop old workers before deploying this version. Setup and recovery instructions cover the remaining deployment steps.

Testing

  • Reproduced an offset timestamp blocking valid recovery events before the fix.
    • The regression now passes, including equivalent recovery timestamps and mixed valid/invalid records.
  • Upgraded an existing daily watch and reapplied the migration.
    • State was preserved; cadence updates, lost responses, restarts, ownership, and stop races passed local database tests.
  • Verified the built agent includes the frequency-update tool and the migrated local worker responds successfully.
  • ⚠️ Live Parallel-to-Linq creation, updates, and delivery have not been tested.

Monitoring

  • Watch status exposes last-confirmed and pending frequency, health, recovery gaps, and job failures. The worker logs failed attempts.
  • 🚨 Hosted alerts for failed or aging jobs and missing scheduler invocations must be configured by the deployer.

@socket-security

socket-security Bot commented Oct 30, 2026 •

Copy link
Copy Markdown

Review the following changes in direct dependencies. Learn more about Socket for GitHub.

Diff Package Supply Chain
Security
Vulnerability Quality Maintenance License
Addedstandardwebhooks@​1.1.11001008181100

View full report

@georgeatparallel
georgeatparallel marked this pull request as ready for review October 30, 2026 21:50
@georgeatparallel
georgeatparallel requested a review from a team as a code owner October 30, 2026 21:50

@Sunny77D Sunny77D left a comment

Copy link
Copy Markdown
Collaborator

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

lgtm!

Comment thread server/monitors/service.ts Outdated
if (job.kind === "sync") {
if (!watch.remote_id) throw new Error("Missing remote monitor");
const group = typeof job.payload.group === "string" ? job.payload.group : undefined;
let cursor: string | undefined;

Copy link
Copy Markdown
Collaborator

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

can we save where the recovery check got to and pick up from there on the next run?

right now, if fetching missed updates takes longer than the worker's 20-second limit, the next attempt starts over and fetches the same events again. when i reproduced this locally, i noticed after eight attempts, the job is marked failed and future hourly checks don't restart it, so automatic recovery stops for that watch until someone manually intervenes. my hunch is that saving progress and continuing on the next run should let us keep the time limit w/o getting stuck

Comment thread server/monitors/store.ts Outdated
async list(owner: Owner) {
return (await this.pool.query<Watch & { updates: unknown[]; problems: unknown[] }>(`SELECT w.*, COALESCE((SELECT jsonb_agg(e.payload) FROM (SELECT payload FROM bell_monitor_events WHERE watch_id=w.id ORDER BY received_at DESC LIMIT 5) e),'[]') AS updates,
COALESCE((SELECT jsonb_agg(jsonb_build_object('kind',kind,'error',last_error,'failed',failed)) FROM bell_monitor_jobs WHERE watch_id=w.id AND last_error IS NOT NULL),'[]') AS problems
FROM bell_monitors w WHERE issuer=$1 AND owner_id=$2 ORDER BY created_at DESC LIMIT 50`, [owner.issuer, owner.id])).rows;

Copy link
Copy Markdown
Collaborator

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

are we able to give watch_list a way to fetch more than the latest 50 watches?

cancelled watches count toward this limit too. i tested one active watch followed by 50 newer cancelled watches, and the active watch disappeared from the results even though it was still running.
ie in a fresh conversation, BELL then has no way to find that watch's ID to stop it or change its frequency. returning 50 at a time is fine, but we may want a way to request the next batch so older watches stay accessible

georgeatparallel and others added 2 commits October 31, 2026 16:09
A recovery scan that ran past the worker's work budget threw away its
place, so the next attempt refetched the same pages from the newest one.
A watch with more missed history than one budget could never finish, and
after eight such attempts the job was marked failed. Scheduled checks
skip failed jobs, so automatic recovery stopped for that watch until an
operator requeued it by hand.

Check the budget between pages and save the scan's place in the job
payload: the next page cursor, the start time, the newest identity seen,
whether prior coverage was found, and the distinct runs counted so far.
Each claim fetches at least one whole page, so a saved cursor never
splits a page and the next run picks up where this one stopped. Stored
event IDs keep deduplicating, so resuming cannot resend an alert.

Saving progress is no longer treated as a failure. It releases the
attempt it consumed and clears the job's last error, so a long recovery
does not walk into the eight-attempt limit. Resuming stays bounded: the
scan only continues while its cursor moves forward, and for at most
twenty resumes, after which it reports an exceeded budget as before.
A scan that did reach the failed state is restarted, keeping its saved
progress, by the next scheduled check for that watch.

Co-authored-by: George Pickett <georgeatparallel@users.noreply.github.com>
watch_list returned the 50 newest watches and nothing else, with
cancelled ones counted against that limit. One active watch followed by
50 newer cancelled ones therefore dropped off the result entirely, and a
fresh conversation had no way to recover its ID to stop it or change its
frequency.

Order watches with the open ones first - anything not cancelled or
failed - and newest first within each group, so a running watch stays on
the first page however many watches were cancelled after it. Keep 50 per
page and return a next_cursor when older watches remain. The cursor is a
keyset marker over that same order rather than an offset, so pages do
not shift when a watch changes status between calls, and an unusable one
is rejected instead of silently reading from the start.

watch_list now takes an optional cursor and an optional status of all,
open, or closed, and returns { watches, next_cursor } in place of a bare
array. Document both in the tool description, the agent instructions,
and docs/monitors.md.

Co-authored-by: George Pickett <georgeatparallel@users.noreply.github.com>
@cursor
cursor Bot force-pushed the george/bell-monitors branch from 368b05b to c13e889 Compare October 31, 2026 16:09
@shriyatheunicorn
shriyatheunicorn merged commit 8e70524 into browserbase:shriyatheunicorn/parallel-research Nov 3, 2026
3 checks passed
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

3 participants