Mine rather than the mechanism's, and recorded because the mechanism invites it: having acted on a fire, I armed a fresh watcher while the previous one was still alive. Two watchers on one branch produce two notifications for one event, and the second is indistinguishable from a real second push. It compounds the self-trigger fault above — the agent pushes, both fire, neither has anything to report. The discipline is dull: stop the watcher before you push, re-arm after. A pgrep before arming costs nothing and catches it. For Command HQ it is a hazard rather than tidiness. A commander armed twice is two processes doing the same work behind one card. Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
126 lines
6.8 KiB
Markdown
126 lines
6.8 KiB
Markdown
# Background jobs: what broke while actually running one
|
|
|
|
Written 2026-08-18 by the mobile side, after arming, firing, draining and re-arming a real watcher
|
|
(poll `git ls-remote` on this repo, react to each push) through the Background Job dialog.
|
|
|
|
Command HQ is a home for these, so anything wrong with the mechanism shows up there first and worst: a
|
|
commander is a watcher that is supposed to run for months.
|
|
|
|
Everything below is from running one, not from reading the code.
|
|
|
|
---
|
|
|
|
## 1. "Keep going" and "tell me" are mutually exclusive, and the dialog offers them as if they were not
|
|
|
|
The dialog's two options are **Stop when it fires** and **Keep going — act each time and carry on**.
|
|
The preamble expands the second into: *"Do NOT exit when it fires... the stream is the notification,
|
|
not the exit."*
|
|
|
|
That is true for a human watching a log. It is **false for the agent**, and the agent is who was asked
|
|
to react. A background process reaches the model **only when it exits** — that is the harness's
|
|
notification. A job that loops forever can do shell work and nothing else. It cannot read a diff,
|
|
cannot judge whether the push matters, cannot write code.
|
|
|
|
So "keep going" silently means "the agent will never see this again". For a watcher whose whole point
|
|
is *"when it fires, go and do something"*, that is the wrong default and it fails invisibly: the job is
|
|
alive, the output is accumulating, and nobody is reading it.
|
|
|
|
**What the owner actually wanted**, worked out in conversation and now the shape we use:
|
|
|
|
> exit on change → agent acts → **drain** (check again immediately) → act again if more arrived →
|
|
> re-arm the watcher when nothing is left
|
|
|
|
**Asked for:** a third option in the dialog, something like *"Exit and hand back to the agent, then
|
|
re-arm"*, and it should be the default for anything the agent must react to. The existing two are then
|
|
honest: stop-on-fire is one-shot, keep-going is for a job whose *output* is the product.
|
|
|
|
## 2. Drain before re-arming, or pushes are lost
|
|
|
|
Not a nicety. Between the job exiting and the agent re-arming, more can land. Re-arming from a baseline
|
|
captured before the work began re-fires immediately; re-arming from "now" silently swallows whatever
|
|
arrived in the gap.
|
|
|
|
The rule that works: after acting, ask again **before** re-arming. If something new is there, act again.
|
|
Only when the source is quiet does the watcher go back on. The owner specified this himself; it should
|
|
be in the preamble rather than rediscovered per job.
|
|
|
|
## 3. A watcher on a shared branch fires on the agent's own writes
|
|
|
|
Observed within an hour. The watcher polled `master` on this repo; the agent then pushed a COMMS note to
|
|
`master`; the watcher fired; there were no new commits to read.
|
|
|
|
**Author filtering does not help here** — the platform side and the mobile side are both Claude, so the
|
|
commits are indistinguishable by author.
|
|
|
|
What works: re-baseline from **local HEAD** after any push of your own, which is what a
|
|
`git pull` + `git rev-parse HEAD` naturally gives you. Worth stating in the preamble as a rule for any
|
|
watcher whose subject the agent also writes to, because the failure looks exactly like a real event.
|
|
|
|
## 3b. Re-arming without stopping the old one leaves two watchers
|
|
|
|
Mine, not the mechanism's, and worth recording because the mechanism invites it. Having acted on a
|
|
fire, I armed a fresh watcher while the previous one was still alive — the old one had fired but its
|
|
process had not been reaped yet. Two watchers on the same branch then produce two notifications for one
|
|
event, and the second looks exactly like a real second push.
|
|
|
|
It compounds §3: the agent pushes, both watchers fire, and neither has anything to report.
|
|
|
|
The discipline that works is dull and reliable: **stop the watcher before you push, re-arm after.** Not
|
|
"re-arm when convenient". A `pgrep` for the job before arming a new one costs nothing and catches it.
|
|
|
|
For Command HQ this is a real hazard rather than a tidiness point — a commander that has been
|
|
accidentally armed twice is two processes doing the same work, and the grid would show one card.
|
|
|
|
## 4. `timeout` is not available, and the preamble insists on it
|
|
|
|
Rule 6 says *"`timeout` every network call, or a hung one leaves it alive and blind."* Correct in
|
|
substance. But this machine is a Mac, and **macOS ships neither `timeout` nor `gtimeout`** — so the rule
|
|
as written cannot be followed, and following it literally produces a job that dies instantly on
|
|
`command not found`.
|
|
|
|
What was used instead, and what the preamble should suggest for a git probe:
|
|
|
|
```
|
|
GIT_SSH_COMMAND='ssh -o ConnectTimeout=15 -o ServerAliveInterval=5 -o ServerAliveCountMax=3'
|
|
```
|
|
|
|
The general point: the rule should name the *outcome* (no unbounded network call) and give a portable
|
|
example, rather than name a binary that is missing on the machine most of this runs on.
|
|
|
|
## 5. A job the owner stops is reported as a failure
|
|
|
|
Stopping a watcher by hand surfaced as **"failed with exit code 144"**. Rule 7 already anticipates the
|
|
confusion from the agent's side — *"an exit is at least as likely to be me as it is to be a failure"* —
|
|
but the harness still presents a deliberate stop as a fault.
|
|
|
|
For Command HQ this matters more than it does in `/chat`: a grid of cards showing "failed" for every
|
|
watcher the owner turned off would be actively misleading. Worth distinguishing stopped-by-request from
|
|
died, at whatever layer knows the difference.
|
|
|
|
## 6. The working directory offered was the wrong one
|
|
|
|
The dialog filled in `/Users/pastilhas` — the conversation's cwd — for a job that had to run inside a
|
|
git checkout. It was correct behaviour, and still the wrong answer, because the directory a watcher
|
|
needs is a property of what it watches, not of where the conversation happens to be.
|
|
|
|
Command HQ makes this moot for commanders, since they all share one directory by design. Noting it for
|
|
the dialog as used from `/chat`.
|
|
|
|
---
|
|
|
|
## What worked, and should not be traded away
|
|
|
|
**"Ask before arming" earned its place immediately.** One question — exit or keep going — changed the
|
|
whole design of the job. The instruction that *a question is the only thing that may come before
|
|
starting* is good discipline and produced exactly the right conversation.
|
|
|
|
**The output rules are right.** One line, no preface, no reading the output back to check it started.
|
|
|
|
**One caveat on those rules:** they forbid saying anything except a question or that one line, which
|
|
left nowhere to report a *mechanical impossibility* — that "keep going" cannot notify the agent. That
|
|
was worth more than the terseness cost. Suggest allowing, explicitly, a statement of the form "what you
|
|
asked for cannot work as specified, here is why" alongside the question.
|
|
|
|
**The preamble text itself is the feature.** It is what makes a job tracked rather than orphaned, and
|
|
copying it verbatim into the mobile client — diffed to confirm — was the right call over paraphrasing.
|