Skip to content

feat: add conditional /watch polling and beep alerts#447

Open
alectimison-maker wants to merge 4 commits into
webbrain-one:mainfrom
alectimison-maker:feat/watch-command
Open

feat: add conditional /watch polling and beep alerts#447
alectimison-maker wants to merge 4 commits into
webbrain-one:mainfrom
alectimison-maker:feat/watch-command

Conversation

@alectimison-maker

Copy link
Copy Markdown
Contributor

Summary

  • add /watch [--keep] [--secs <30-120>] [--long | --short] <condition and action> [/beep] for immediate, page-bound conditional polling
  • extend the durable scheduler with watch metadata, baseline-aware partial polls, one-shot/keep outcome handling, safe untrusted observation replay, and duplicate coalescing
  • expose a scoped beep({event_key, message?}) tool only to /beep watches; persist and dedupe event keys before background audio, and play only after verified done(outcome="success")
  • support distinct short/default/long tones through Chrome MV3 offscreen audio and the Firefox background page, honoring notifySound without a second side-panel chime
  • surface watch interval/mode/alert/last-observation state in scheduled cards, localize command/status copy across all shipped locales, and document behavior/security boundaries

Safety properties

  • prior page-derived observations are nonce-delimited and boundary markup is stripped before the next poll
  • ordinary agent runs cannot see or invoke beep; one poll can arm only one fresh event key
  • duplicate, partial, failed, and missing-outcome runs do not play an alert
  • a fresh event key is persisted before best-effort audio playback

Tests

  • node test/run.js: 1142 passed, with only 2 pre-existing main failures (changelog 25.4.0 vs package 25.4.2; locale-dependent WebBrain promotion title)
  • npm run test:security: 60/60 passed
  • npm test: reaches the same two pre-existing node test/run.js failures, so its && does not start the security command; the security command was run separately above

Closes #410

@vercel

vercel Bot commented Jul 22, 2026

Copy link
Copy Markdown

@alectimison-maker is attempting to deploy a commit to the esokullu's projects Team on Vercel.

A member of the Team first needs to authorize it.

},
scheduledAt: createdAt,
nextRunAt: iso(this.now() + 1000),
immediate: true,

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.

[bug] Watch jobs are created with immediate: true and nextRunAt ≈ now+1s, then _completeWatch sets immediate: false and advances nextRunAt by the watch interval. sameScheduledIntent still requires matching scheduledJobIsImmediate and near scheduled times, so after the first poll an identical /watch is no longer treated as a duplicate live job. Users can stack many concurrent Act-mode watches for the same page/condition (amplified by 30–120s intervals and LLM cost).

Suggestion: For source === 'watch', dedupe live jobs by target + watch payload only (ignore immediate / near-time). Extend the existing “identical live watch should be deduped” test to re-create after one successful partial poll.

} else if (event === 'completed') {
ensureScheduledTerminalMessage(job);
await settleScheduledRun(event, job, runTabId);
} else if (event === 'polled' || event === 'triggered') {

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.

[bug] Keep watches emit running on every poll, and the existing handler always does addMessage('assistant', '') with the same dataset.scheduledJobId. The new polled/triggered path only settles the first matching assistant node, and only fills text when that node is still empty. Result: each 30–120s poll leaves an additional empty assistant bubble in chat, while later observations never replace the first filled one.

Suggestion: For job.source === 'watch' on running, reuse findScheduledAssistantMessageForJob(jobId) (or a single sticky run row) instead of creating a new message; on polled/triggered, update that message’s text to the latest lastResult rather than gating on !textEl.textContent.trim().

target: {
type: 'url',
url: parsed.url,
...(tabId != null ? { tabId } : {}),

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.

[bug] createWatchJob stores the initiating tabId on a type: 'url' target. Existing _resolveTab will tabs.update that same tab back to the watched URL whenever the tab has navigated away. At 30–120s this repeatedly hijacks the user’s active browsing session (and can wipe SPA state) instead of following the page or using a background tab.

Suggestion: Prefer a dedicated inactive tab for watches: omit initiating tabId from the URL target (always tabs.create({ active: false })), or if URL diverges create/reuse a background tab rather than forcing tabs.update on the original tab. Document the chosen binding semantics in README/architecture.

&& !!eventKey
&& !duplicateAlert
&& eventKey !== job.watch?.lastTriggeredEventKey;
const alertContractFailed = lastOutcome === 'success'

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.

[suggestion] For /beep watches, any done(outcome="success") without a fresh armed event_key permanently fails the entire job (alertContractFailed). That is intentionally fail-closed for audio, but local/small models frequently omit optional tools; a single miss destroys a long-running --keep watch after it may already have performed the user-requested action.

Suggestion: Consider treating missing arm as a non-fatal “success without alert” (complete/continue without sound, record a warning), or only fail closed when beep was armed with a key that fails the contract. Keep the stronger fail-closed path if product intent is strict, but call it out in the slash help / created message.

@@ -2464,7 +2488,10 @@ async function settleScheduledRun(event, job, tabId = currentTabId) {
if (assistantEl) {

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.

[suggestion] Settlement still only injects job.lastResult when the assistant text is empty. Even after Issue 2 is fixed to reuse one bubble, keep-watch users will not see refreshed observations in chat unless this condition is relaxed for polled/triggered.

Suggestion: For watch poll events, always replace/update the message text with the latest observation (still via formatMarkdown), and keep the empty-only gate for true terminal schedule completions if desired.

@@ -297,6 +298,13 @@ export default {
"sp.slash.record_transcribe": "Record and save a Whisper transcript after stop",

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.

[nit] /watch is outOfBand: true and works while busy, but sp.slash.busy_only_oob still lists only the older OOB commands and does not mention /watch.

Suggestion: Add /watch to the busy-only OOB toast string in all locales (Chrome and Firefox).

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.

Add /watch shorthand with conditional /beep notifications

2 participants