realtime-collector · git:20261105.0d95dc2 · 2026-11-05 · sha256 fe6930f8460e88dc

realtime-collector git:20261105.0d95dc2A

Immutable. This exact content never changes and is served at /api/v1/blob/fe6930f8460e88dc.

---
name: realtime-collector
description: Push an Arbor campaign's live state (phase, iteration in flight, verdicts, baseline and best, the log's tail) to the shared volume every minute, where the dashboard's global mode and other people read it, and set up the ssh access to the share that it needs. Use when starting or restarting a campaign, when a campaign is missing from the share or its snapshot is stale, or when asked to make a campaign visible to others.
---

# Realtime collector: a campaign's live state, on the share

Audience: whoever starts an Arbor campaign, or keeps one running. The
collector is `python3 -m arbor.collect realtime` (`arbor/collect/`, standard
library only). What a campaign learned, as opposed to where it stands, is the
`knowledge-collector` skill; reading either is the `dashboard` skill.

## What it does

Every `--interval` seconds (60 by default) it reads, from files on the host the
campaign runs on:

- `session.json`: iteration, baseline, best, every verdict, STOP and ABORTED
  files;
- the campaign's stdout log (`--log`): the phase (agent working, warmup,
  measured replay, critic, gate), the iteration in flight, gate lines,
  supervisor restarts, and whether an `arbor run` process is alive on this host;
- baseline records (`--results`, `*baseline*.json`), an event log
  (`--events`), and the target the config states (`--config`).

It writes `status.json` and `meta.json` to `<session>/collector/realtime/`, and
pushes both to the share:

```text
/path/to/arbor-share/projects/<project>/<user>/<campaign>/realtime/{status.json,meta.json}
```

It never samples a GPU node: a probe during a measured replay perturbs it. It
never raises either: a section that fails is recorded in the snapshot's `errors`.

## Once per person: reach the share

The share is `/path/to/arbor-share` on the share's pod
`<pod name>` (workspace `<workspace>`), reached
through the platform's ssh gateway. The gateway takes ssh commands, scp and
sftp, but not rsync and not piped stdin. That is why an
upload is one sftp batch (make the directories, put each file under a temporary
name, rename it into place), so a reader never sees half a file. The gateway
also returns exit status 0 for every command and puts its stderr on stdout:
in anything you script against it, print a marker or
JSON and read that, never `$?`.

1. Register your public key on the platform, and add the pod to `~/.ssh/config`
   on every host that will run a collector (your workstation, WSL, a Cluster B
   login node):

   ```text
   Host arbor-share
       HostName <ssh gateway host>
       Port 2222
       User <your platform user id>.<pod name>
       IdentityFile ~/.ssh/<the key you registered>
       IdentitiesOnly yes
       ServerAliveInterval 60
   ```

   The user is `<user id>.<pod>.<container>.<shell>.<workspace>`. If the pod
   is replaced, its name changes; the platform shows the current one.
   Cluster B's login nodes reach the gateway (port 2222 open from `<login node>`).
2. Check it from an Arbor checkout: `python3 -m arbor.collect check`. It runs
   three steps (ssh, the share root, an upload of a probe file it then removes)
   and, for the first that fails, says what to do.

The share root and its `projects/` directory are made once, by a person, never
by a collector: owned by root, the share's group (<gid>) with mode 1775, so every member
of the group can add a project and no one can remove another person's:

```bash
ssh arbor-share 'mkdir -p /path/to/arbor-share/projects && chgrp <gid> /path/to/arbor-share /path/to/arbor-share/projects && chmod 1775 /path/to/arbor-share /path/to/arbor-share/projects'
```

Collectors push summaries, never traces.

## Names: project, user, campaign

The path is `projects/<project>/<user>/<campaign>/`, and the index groups by
it, so get the names right before the first push:

```bash
python3 -m arbor.collect project --session <session> --config <config>
```

- **project**: one name per model family, from
  [`arbor/collect/projects.json`](../../../arbor/collect/projects.json).
  `--project`, else `ARBOR_PROJECT`, else the config's `project` chooses it
  outright (normalized through the table); otherwise the config's
  `model_name`, the pack, the session's name and its identity are tried in
  turn. A name the table does not know becomes a slug of
  itself, and the output says so. For a new model family, add an entry with
  examples (`arbor/tests/test_collect.py` checks every example resolves).
- **user**: `--user`, else `ARBOR_USER`, else the login name. Use your ldap,
  the same on every host, or your campaigns split across two users.
- **campaign**: the session directory's name, or `--campaign`. Keep it across
  restarts and chunks; a new name is a new campaign on the share.

## Run it beside the campaign

On the host that holds the session and the log, from an Arbor checkout:

```bash
python3 -m arbor.collect realtime --session <session> --config <config> \
    --log <the campaign's stdout log> [--slurm-job <job name>] [--results <dir>] [--events <file>] --interval 60
```

- **A driver inside a Slurm job** (a chain of chunks on GPU nodes, while the
  collector runs on a login node) needs `--slurm-job <job name>`: without it
  the collector looks for an `arbor run` process on its own host, finds none,
  and reports a running campaign as not running.

- **It must outlive your shell.** A Cluster B login node kills what an ssh
  session leaves behind; run the collector as a user unit, from a script file
  (sent from PowerShell, the quotes are stripped):

  ```bash
  loginctl enable-linger "$(id -un)"     # once per host
  systemd-run --user --collect --unit=arbor-rt-<campaign> --setenv=PATH="$PATH" \
    /bin/bash -lc "mkdir -p <session>/collector && cd <Arbor checkout> && exec python3 -m arbor.collect realtime --session <session> --config <config> --log <log> >> <session>/collector/realtime.out 2>&1"
  systemctl --user is-active arbor-rt-<campaign>
  ```

  User units are per host; check it on the host that started it.
- **One collector per campaign.** Two race on the same files on the share.
- `--once` takes one snapshot and exits (at the end of a campaign, say);
  `--no-push` writes only the local copy.

## When the share cannot be reached

The collector runs `check` when it starts. If any step fails, it prints which
step and what to do, keeps writing its local copy every interval, and retries a
push every 10 intervals. Each snapshot's `share` block records `share_ok`,
`last_error` and `pushed_at`, and the dashboard turns a failure into a banner.
On the share, the campaign's snapshot ages, and global mode marks it "last
seen ..." after 15 minutes. Exit status (with `--once`): 0 when pushed, 1 when
the share could not be reached or written (the local copy is still written),
2 for bad arguments.

## Reading it

- Every campaign, or one live: the `dashboard` skill.
- By hand: `ssh arbor-share cat /path/to/arbor-share/projects/<project>/<user>/<campaign>/realtime/status.json`,
  or read the path directly on a Cluster B node.

## Settings

| Variable | Default | |
| --- | --- | --- |
| `ARBOR_SHARE_HOST` | `arbor-share` | ssh destination, an alias from `~/.ssh/config` |
| `ARBOR_SHARE_ROOT` | `/path/to/arbor-share` | the share root on that host; a scratch root (under `/tmp`) for tests |
| `ARBOR_SHARE_SSH_OPTS` | none | extra options for both ssh and sftp; use `-F` and `-o` forms (`-o Port=2222`), since ssh and sftp spell `-p` differently |
| `ARBOR_PROJECT`, `ARBOR_USER` | none | the names above |