# Background Watches

A **watch** is Forge's lightweight async primitive for long-running jobs:
you (or the assistant) kick something off that finishes minutes later —
a device firmware upgrade, a Jenkins build, a test run — and instead of
the assistant sitting in the conversation polling (burning tokens,
getting stuck), Forge polls it in the **background** and posts the result
back into this chat when it's done.

You don't manage watches directly most of the time — they appear when a
long job is started and clear themselves when it finishes.

## What you see

- **Progress chip** — while a watch runs, a small status pill shows
  above the chat composer (e.g. "Upgrading NAC 10.15.52.152… poll 3/15,
  build 6956"). It updates in place, is **not** a chat message (doesn't
  clutter the thread), and disappears when the job finishes.
- **Completion message** — when the job reaches its goal (or fails /
  times out), a normal assistant message lands in the chat:
  "NAC … upgrade confirmed — now running build 6957." If your session
  came from Telegram, that message arrives in Telegram.
- **Watch list** — see and control active watches in two places:
  - **/chat** web: the "Background watches" panel in the left sidebar.
  - **Forge web**: user menu → **Monitor** → "Background Watches".
  Each entry shows status (active / done / failed / timed_out), poll
  count, and **Cancel** / **Delete** buttons.

Watches survive a Forge restart (persisted in SQLite) — an in-flight
upgrade keeps being watched after a restart, no chat needed.

## Two ways a watch starts

**1. Connector built-in (`async` block).** Some connector tools are
pre-declared as long tasks by their author — e.g. `nac.upgrade`
automatically registers a watch that polls `nac.get_version` until the
new build is running. Nothing for you to do; it just works.

**2. The assistant decides (`start_watch`).** For long jobs that aren't
pre-declared — a Jenkins build, a one-off cross-connector flow — the
assistant calls the `start_watch` builtin on the fly: it triggers the
job, registers a watch to poll the right status tool, and then stops
talking. You get the result later in chat.

Both use the same background machinery; you experience them identically.

## `start_watch` (for the assistant)

When you've just started a long job and have a tool that reports its
status, register a watch instead of polling in the conversation. Two
poll forms are accepted:

**1. Connector tool** — `"<connector>.<tool>"`, e.g. `jenkins.get_build`:

```
start_watch({
  poll: "jenkins.get_build",
  poll_args: { job_path: "job/foo", build_number: 18 },
  done_match: { path: "result", equals: "SUCCESS" },
  interval_sec: 60, timeout_sec: 1800,
  message: "Build 18 finished: {poll.result}"
})
```

**2. Bare builtin name** — Forge's own status readers:

| Builtin | Pair with | done condition |
|---|---|---|
| `get_pipeline_status` | `trigger_pipeline` → pipeline_id | `done_match={path:"status",equals:"done"}` (or `done_path:"terminal"`) |
| `get_task_status` | `dispatch_task` → task_id | `done_path:"terminal"` |

```
start_watch({
  poll: "get_pipeline_status",                   // bare builtin, no prefix
  poll_args: { pipeline_id: "a8e049a3" },
  done_path: "terminal",
  message: "Pipeline {poll.workflowName} finished: {poll.status}"
})
```

Then **stop** — do not keep calling the status tool in the conversation.
A completion message arrives in chat; the user can cancel it from the
watch list. Pick `done_match`/`done_path` from a field you saw in the
status tool's output (you usually called it once already). Queue/startup
errors (e.g. a build number 404 while still queued) are tolerated for a
while. Guards (max polls, timeout, lifetime, active cap) keep it from
running away; worst case it times out and reports that.

### `_raw` fallback for non-JSON tools

The poll result is parsed as JSON. If the tool returns plain text (some
shell-protocol tools), Forge wraps it as `{_raw: "...full output..."}`
so you can still grep — e.g. `done_match={path:"_raw",contains:"DONE"}`.

## Limits

- Watches are short-lived; they end at done / failed / timed_out /
  cancelled and stop polling.
- `start_watch` reports back only via chat — it doesn't open a separate
  notification channel. Telegram-origin sessions reply on Telegram.
- A watch can't *fix* a job that's stuck waiting for human action (a
  build needing approval); it only watches to a final state and reports.
