diff --git a/.claude-plugin/plugin.json b/.claude-plugin/plugin.json
index 7ad29f0..19800f6 100644
--- a/.claude-plugin/plugin.json
+++ b/.claude-plugin/plugin.json
@@ -1,6 +1,6 @@
{
"name": "downrange",
- "version": "1.1.6",
+ "version": "1.1.7",
"description": "A band, pane, and /downrange table: progress, ETA, and last output for Claude Code background shells and Monitor watches, tool activity for subagents, state for workflows, and the next /loop wakeup.",
"author": { "name": "dot-agi" },
"license": "MIT",
diff --git a/AGENTS.md b/AGENTS.md
index 759c03a..c71ce82 100644
--- a/AGENTS.md
+++ b/AGENTS.md
@@ -75,8 +75,9 @@ Run all of them after each change. All must pass.
- Write event names in `on(...)` as string literals. Write each matcher value as a literal or as a constant of `hooks/register.tsx`: `claude plugin validate` shows an imported constant as `?`. Use static imports only. Hooks run with no DOM and no Node.
- Export every type in `types/index.d.ts`. `claude plugin validate` refuses a contract type without `export`.
- Keep the other modules pure: no `$`, no I/O, and no clock reads. Pass `now` in.
-- Keep the jobs and the wakeup in module memory, `memory`: it is the source of truth, and it survives `/clear`, `/resume`, and `/branch`. `$.state` holds a mirror of the current session's view for drawing, and **Hide** and the last answered status prompt as atoms. The only module variables are `ticker`, `isTicking`, `lastTickAt`, `memory`, `since`, `sessions`, `switchedAt`, `mirror`, `historyWrites`, `reported`, and `isAnyRatio`, the `genericRatios` option that `register` sets.
+- Keep the jobs and the wakeup in module memory, `memory`: it is the source of truth, and it survives `/clear`, `/resume`, and `/branch`. `$.state` holds a mirror of the current session's view for drawing, and **Hide** and the last answered status prompt as atoms. The only module variables are `ticker`, `isTicking`, `lastTickAt`, `memory`, `since`, `sessions`, `switchedAt`, `pending`, `mirror`, `historyWrites`, `reported`, and `isAnyRatio`, the `genericRatios` option that `register` sets.
- Change `memory.jobs` only through `writeJobs` and `memory.wakeup` only through `writeWakeup`: they change memory at once, then write the mirror through `mirrored`, wait until a write that holds the change has landed, and keep the ticker current. A failed write is no failure of the hook (see `persisted`): memory took the change, so the rest of the hook runs. Each hook passes one `deadlineOf()` to all its writes: it waits `MIRROR_WAIT_MS` at most in all, and a wait that passes its deadline leaves the write's waiters. A reload of the mod empties module variables and keeps `$.state`: only `loaded`, at the first `session.start`, change of memory, or mirror write, reads the jobs and the wakeup from `$.state` into memory. Each change of memory waits for that read and applies on top of it, and no mirror write goes out before it. The mirror keeps a removal for each ended job that the session does not show (see `viewedJobs`). Read the mirror only to draw: in a render hook, `/downrange`, and the status answer.
+- Settle each `/clear`, `/resume`, and `/branch` in two ways. `classic.SessionStart` is the main way. Under managed settings or a Team or Enterprise login, Claude Code's built-in guard keeps every `classic.*` event from the mod, so `switched` is the second way. `session.end` sets `pending`. Every hook other than `session.start`, `session.end`, `classic.SessionStart`, and the render hooks calls `switched` first, and so does each tick. A `command.run` hook for every command calls `switched`, then passes the command on unchanged, so that a command that reloads the mod, such as `/reload-plugins`, finds the view in `$.state`. A `config.set` hook for every key calls `switched` before a `/config` row changes, because a change of an option reloads the mod. It returns `next(e)` unchanged: it never refuses a change and never changes a value. A settle clears `pending` before its write lands, so `mirror.settle` holds the write of each settle, by `switched` or by `classic.SessionStart`, until it lands or fails. Both hooks call `settleLanded` after `switched`: it waits for that write within the hook's deadline, and with no settle in progress it waits for nothing. Then, when the last mirror write failed (`mirror.isBehind`), both write the view once more through `persisted`, within the same deadline. A failed write there is no failure of the hook, and with no write behind they write nothing. `session.end` also runs `switched` once at once, with `$.clock.after(0)` (see `settledAtOnce`), because the first tick can be a second away. Both ways must give a session the same start, and `classic.SessionStart` must keep every change that came after `switched`. Never settle a `/branch` before `classic.SessionStart`: during that event, `$.session.id()` still answers the parent's id.
- Render hooks never write state. Buttons write from their press handlers.
- Give every `on(...)` registration a `.catch` that logs the failure under the hook's label, such as `tool.call{tool=Bash}`, and passes the event on with `next(e)`: write it as a function literal that calls `passOn`. `claude plugin validate` refuses a `.catch` that is not a function literal or the name of one. The `/downrange` command and the pane answer with the cause instead.
- Start every diagnostic with `downrange: : `, then the cause. Never log output text, prompt text, or a command line.
diff --git a/README.md b/README.md
index 6db2e3c..e716c9d 100644
--- a/README.md
+++ b/README.md
@@ -4,7 +4,7 @@
-
+
Progress and an ETA for background shells and Monitor watches, read from their real output or past runs, with no model request.
@@ -28,7 +28,7 @@ Downrange reads that output on your machine and shows where each job stands.
- **The background tasks of your session.** Shell commands (`run_in_background`, ctrl+b, or a command moved at its timeout), Monitor watches, background and remote subagents, teammates, workflow runs, and the pending dynamic `/loop` wakeup. For background sessions, use agent view (`claude agents`).
- **Liveness flags.** `quiet 3m 00s` for a shell command or subagent that stayed silent for 2 minutes or more, and `3 failed calls in a row` for a struggling subagent.
- **A table that prints mid-turn.** `/downrange` answers at once, even while Claude works. With the `answerStatusPrompts` option on, a prompt such as `status?` gets the same table.
-- **In step with Claude Code.** At each turn end, Downrange checks its rows against Claude Code's task list. It adds tasks it missed, and marks a job that left the list `ended without a report`. Running jobs stay in view across `/clear`, `/resume`, and `/branch`.
+- **In step with Claude Code.** At each turn end, Downrange checks its rows against Claude Code's task list. It adds tasks it missed, and marks a job that left the list `ended without a report`. A shell also ends when its output file ends with Claude Code's `[exited with code N]` line. Running jobs stay in view across `/clear`, `/resume`, and `/branch`. Under managed settings or a Team or Enterprise login, Claude Code sends Downrange no task list (see [Known limits](#known-limits)).
- **Read-only by design.** Downrange never stops or changes a task. Built-in `/tasks` stops tasks.

@@ -57,10 +57,10 @@ flowchart LR
- **Launches.** Hooks on the Bash, Monitor, Agent, and Workflow tools record each background launch with its id and title, and a shell's output file. A ScheduleWakeup result sets the wakeup. A TaskStop result marks its job stopped.
- **Output.** The ticker reads each running shell's output file every 2 seconds, or every 10 seconds above 256 KiB. It keeps the end of the file: the last 16,384 characters, or the last 16,384 bytes through `tail` above 4 MiB. Progress comes from the last 10 display lines of that end.
-- **Redirect files.** Some commands send their output to a file, such as `> build.log` or `| tee build.log`. The ticker then also reads the one file that the command's text names, its redirect file, in the same way. At each read, the row shows the file that changed last: Claude Code's output file or the redirect file. On equal times, it shows Claude Code's output file. While the row shows the redirect file, it notes `output: build.log`. The end and the exit code still come from Claude Code's notification. Downrange follows no redirect file on Windows, or in a command with code that may not run, such as an `if` or a function.
-- **Ends.** Task notifications end jobs and carry Monitor events. The agent list updates subagent states. Stop and SubagentStop snapshots add tasks that Downrange missed, and end tasks that left the list after a 10-second grace period. A scheduled prompt that fires from 5 seconds before the wakeup's due time clears the wakeup. So does a snapshot with no one-shot schedule, after the same grace period.
+- **Redirect files.** Some commands send their output to a file, such as `> build.log` or `| tee build.log`. The ticker then also reads the one file that the command's text names, its redirect file, in the same way. At each read, the row shows the file that changed last: Claude Code's output file or the redirect file. On equal times, it shows Claude Code's output file. While the row shows the redirect file, it notes `output: build.log`. The end and the exit code still come from Claude Code's notification, or from the `[exited with code N]` line at the end of Claude Code's output file. Downrange follows no redirect file on Windows, or in a command with code that may not run, such as an `if` or a function.
+- **Ends.** Task notifications end jobs and carry Monitor events. A shell also ends when a read of its output file finds Claude Code's `[exited with code N]` line at the end, with that exit code. The agent list updates subagent states, every second while a subagent runs and at each turn end. Stop and SubagentStop snapshots add tasks that Downrange missed, and end tasks that left the list after a 10-second grace period. A scheduled prompt that fires from 5 seconds before the wakeup's due time clears the wakeup. So does a snapshot with no one-shot schedule, after the same grace period. Managed settings or a Team or Enterprise login keep the snapshots from Downrange (see [Known limits](#known-limits)).
- **Usual durations.** A clean end (exit code 0 or none) of a shell that Downrange saw start adds its run time to a history keyed by a hash of folder and command. The usual-duration ETA is the median of the last 5 runs.
-- **Views and session changes.** Downrange keeps the jobs and the wakeup in its own memory, which `/clear`, `/resume`, and `/branch` do not touch. After each change, it writes a copy of the session's view to session state, and the band, the pane, and `/downrange` draw from that copy. So the new session shows the running jobs and the wakeup at once, with their ages, last lines, progress, ETAs, and file reads. Each session shows the jobs that ended after it started. **Hide** does not carry. The ticker stops when nothing is live.
+- **Views and session changes.** Downrange keeps the jobs and the wakeup in its own memory, which `/clear`, `/resume`, and `/branch` do not touch. After each change, it writes a copy of the session's view to session state, and the band, the pane, and `/downrange` draw from that copy. So the new session shows the running jobs and the wakeup at once, or within a second under managed settings or a Team or Enterprise login, with their ages, last lines, progress, ETAs, and file reads. Each session shows the jobs that ended after it started. **Hide** does not carry. The ticker stops when nothing is live.
## Install
@@ -187,7 +187,7 @@ Set options in `/config`, or under `pluginConfigs["downrange@downrange"].options
### What it does not catch
- **Progress.** Ratios outside known formats, such as `loss 12/40`, unless `genericRatios` is on. Dates such as `[10/11]` can still count.
-- **Jobs.** Fixed-interval loops such as `/loop 10m`, other cron schedules, jobs after `/background` until the next turn end, and work that goes on after its job ends. `nohup` alone does not end a job at once. A command that ends with `&` does. Downrange follows no file of a command with `&` or `nohup`.
+- **Jobs.** Fixed-interval loops such as `/loop 10m`, other cron schedules, jobs after `/background` until the next turn end (under managed settings or a Team or Enterprise login, at all), and work that goes on after its job ends. `nohup` alone does not end a job at once. A command that ends with `&` does. Downrange follows no file of a command with `&` or `nohup`.
- **Stalls.** Stuck remote subagents, teammates in their own panes, and waiting agents. They show no `quiet` flag.
- **ETAs.** Run time in the foreground before ctrl+b or a timeout move. In output silence, a rate ETA runs down to `<1m` and stays there.
@@ -205,7 +205,8 @@ The list below holds the limits you can hit in daily use. [docs/usage.md](docs/u
- **Commands that subagents start.** A command with the note `ends with its agent` ends when its subagent answers. It then shows `ended with its agent` and no exit code, unless its own notification arrives later.
- **Agents that wait.** A subagent that the agent list reports as pending, waiting, or idle shows that word, beside its tool count when it shows one. It shows no `quiet` flag and no failed-calls flag, however long it stays in that state.
- **Agents that run elsewhere.** Downrange sees the tool calls only of agents that run in this Claude Code process. A remote subagent shows `remote`, and a teammate in a terminal pane of its own shows `own pane`. Their rows show no tool count and no `quiet` flag, so a stuck or dead one looks the same as a busy one. A row whose launch result Downrange missed, or that an earlier version of Downrange saved, still keeps `0 tools` and shows `quiet` from 2 minutes after launch, even while the agent works. Its notification or a snapshot still ends it.
-- **A session moved to another process.** `/background` and a hand-off to the desktop app start a fresh process that resumes the conversation. Downrange starts empty there. Running tasks return at the next turn end, with `≥` ages and no last line. A shell's notification can still name its output file, end it, and show its last line. The `/loop` wakeup returns only with the next `ScheduleWakeup` result.
+- **A session moved to another process.** `/background` and a hand-off to the desktop app start a fresh process that resumes the conversation. Downrange starts empty there. Running tasks return at the next turn end, where turn-end snapshots reach Downrange, with `≥` ages and no last line. A shell's notification can still name its output file, end it, and show its last line. The `/loop` wakeup returns only with the next `ScheduleWakeup` result.
+- **Managed settings and Team or Enterprise logins.** There, Claude Code's built-in guard keeps every `classic.*` hook event from Downrange, so it gets no turn-end snapshot of tasks and schedules. A shell still ends at its notification, or at the `[exited with code N]` line at the end of its output file. When a job's end notice carries no status, only another input ends it: a shell at that line, a subagent when the agent list reports its end, a command with the note `ends with its agent` when its subagent answers, and any job at a `TaskStop` call that stops it. Without such an input, its row keeps running until the process ends. A cancelled `/loop` wakeup shows until 10 minutes past due. A task that Downrange did not see start joins only through a notification: a Monitor event, or an end notice with a status. After `/clear`, `/resume`, or `/branch`, the new session shows the running jobs and the wakeup when Downrange writes the copy of its view, which it does at once, and no job that ended before the change, unless this process showed that session before. A reload that no slash command and no `/config` change causes, such as one after an edit of a settings file, before that copy lands loses the jobs and the wakeup. [docs/usage.md](docs/usage.md#known-limits) has the details.
@@ -225,7 +226,7 @@ Downrange watches events and changes almost none of them. It makes no network re
It also reads the session's working folder, to hash it with a command and to place a relative redirect file, and the list of the session's agents and their states.
- **Conversation data it reads.** The input and result of the tool calls in the hook table below: a command or description, the task id, the output file path, and the wakeup time and reason. Downrange reads a background command's text to find its redirect file. One input field of each tool call a tracked subagent makes (see the last item). Task notifications. The `background_tasks` and `session_crons` lists of Stop and SubagentStop hooks. The text of a prompt you type, only when `answerStatusPrompts` is on, to compare it with the status phrases. Downrange reads no transcript file.
- **What it keeps.** Memory, while Claude Code runs, and a copy of the session's view in session state, which Claude Code holds for the session and which other mods in the session can read. They hold job titles, last output lines, notes, tool summaries, and the wakeup's reason, each cut to 200 characters or fewer. It also holds task ids, output file paths, redirect file paths, progress, exit codes, times, and the last status prompt it answered. The duration store holds run times under a hash of folder and command, with no command text, no path, and no output.
-- **Permissions.** Downrange never changes a permission decision. No hook denies or approves a call, rewrites a tool's input, or answers a permission prompt. Claude Code and you decide as before. `claude plugin validate` lists most hooks as gating hooks with a `.catch`, because their events can block. Each such `.catch`, such as the one on `classic.SessionStart` in `hooks/register.tsx`, only logs the failure and passes the event on unchanged with `next(e)`. The `/downrange` command and the Jobs pane show the failure instead.
+- **Permissions.** Downrange never changes a permission decision. No hook denies or approves a call or a `/config` change, rewrites a tool's input, or answers a permission prompt. Claude Code and you decide as before. `claude plugin validate` lists most hooks as gating hooks with a `.catch`, because their events can block. Each such `.catch`, such as the one on `classic.SessionStart` in `hooks/register.tsx`, only logs the failure and passes the event on unchanged with `next(e)`. The `/downrange` command and the Jobs pane show the failure instead.
- **The `Reflect.get` call in `hooks/jobs.ts`.** It reads one named field of a tracked subagent's tool input, from a fixed list in that file. The fields are `command` for Bash and Monitor, the file name from `file_path` or `notebook_path`, the `pattern` of Grep and Glob, the host of a WebFetch `url`, the `query` of WebSearch, and the `description` of Agent. That field becomes a summary such as `Edit: view.ts`, cut to 60 characters. Each tool's input has its own shape, so the code reads the field by name. It calls no function and reads no other field. Other tools show only their name.
@@ -234,15 +235,17 @@ Downrange watches events and changes almost none of them. It makes no network re
| Event | What the hook does | What it changes |
| --- | --- | --- |
| `session.start` | Registers `/downrange`, and loads the jobs and the wakeup from session state into memory, as after a reload | Nothing |
-| `session.end` | On `/clear`, `/resume`, or `/branch`: notes the time, where the next session's **Finished** starts | Nothing |
-| `classic.SessionStart` after `/clear`, `/resume`, or `/branch` | Takes the new session's id from the event, writes that session's view of the jobs and the wakeup to session state, and starts the once-a-second timer | Nothing. It adds no context, instructions, or settings |
-| `command.run` for `downrange` only | Prints the table for `/downrange`, and opens or closes the pane for `/downrange pane` | Only its own command |
+| `session.end` | On `/clear`, `/resume`, or `/branch`: notes the time, where the next session's **Finished** starts, and the session that ended, and starts the once-a-second timer. Then Downrange reads the session id and writes the new session's view of the jobs and the wakeup to session state at once, and again at each of its other hooks, except the ones that draw, and at each timer period, until it knows the new session | Nothing |
+| `classic.SessionStart` after `/clear`, `/resume`, or `/branch` | Takes the new session's id from the event, writes that session's view of the jobs and the wakeup to session state, and starts the once-a-second timer. Managed settings or a Team or Enterprise login keep this event from Downrange. It then does this with the id that Claude Code gives, once that id names the new session: at once after `session.end`, or at its next hook or timer period | Nothing. It adds no context, instructions, or settings |
+| `command.run` for `downrange` | Prints the table for `/downrange`, and opens or closes the pane for `/downrange pane` | Only its own command |
+| `command.run` for any command | Until Downrange knows the new session after `/clear`, `/resume`, or `/branch`, reads the session id and writes the new session's view of the jobs and the wakeup to session state, before the command runs. While the copy of that view is still being written, it waits until the copy lands, 2 seconds at most. When the last write of the copy failed, it writes the copy once more, within the same 2 seconds. So a command that reloads Downrange, such as `/reload-plugins`, keeps them | Nothing. The command runs as typed |
+| `config.set` for any row | Until Downrange knows the new session after `/clear`, `/resume`, or `/branch`, reads the session id and writes the new session's view of the jobs and the wakeup to session state, before the `/config` row changes. While the copy of that view is still being written, it waits until the copy lands, 2 seconds at most. When the last write of the copy failed, it writes the copy once more, within the same 2 seconds. So a change of a Downrange option, which reloads Downrange, keeps them | Nothing. It never refuses a change and never changes a value |
| `tool.call` for Bash, Monitor, Agent, Workflow, ScheduleWakeup, and TaskStop | Lets the call run, then reads its result to record a background launch, the wakeup, or a stop | Nothing. The call's input and result stay as they were |
| `tool.call` for any tool | For a subagent that Downrange tracks, counts the call and keeps a short summary of it | Nothing |
| `session.append` | Reads a subagent's stored task notification to end jobs | Nothing |
-| `turn.complete` | After a subagent answers, ends the commands tied to that answer and reads the agent list | Nothing |
+| `turn.complete` | After a subagent answers, ends the commands tied to that answer and reads the agent list. After each main-loop turn, reads the agent list when Downrange shows a subagent, as `classic.Stop` does | Nothing |
| `prompt.submit` | Reads task notifications to end jobs, and a scheduled prompt to clear the wakeup | Only with `answerStatusPrompts` on: it drops an exact status prompt while Claude is idle and a job runs, and shows the table instead |
-| `classic.Stop`, `classic.SubagentStop` | Reads the `background_tasks` and `session_crons` lists in the hook input to correct jobs and the wakeup | Nothing. It never blocks a stop |
+| `classic.Stop`, `classic.SubagentStop` | Reads the `background_tasks` and `session_crons` lists in the hook input to correct jobs and the wakeup. Managed settings or a Team or Enterprise login keep these events from Downrange | Nothing. It never blocks a stop |
| `ui.render` | Draws the band above the prompt and the Jobs pane | Adds the band on top of what the prompt area already shows. Draws only its own pane |
@@ -259,7 +262,7 @@ Downrange watches events and changes almost none of them. It makes no network re
| A job shows `output unavailable` or `tail failed` | The output file or the redirect file is gone, or `tail` is not on PATH (common on Windows, for outputs over 4 MiB) | Run `claude --debug` and search for `downrange: clock.every:` |
| `/downrange pane` prints `The Jobs pane is not shown here` | Nothing draws in this session, or the surface places no panes | Use `/downrange` |
| Ages and output lines stop moving | Another mod refused a timer period | The next change starts the timer again; `claude --debug` shows `the ticker stopped` |
-| Jobs end only when Claude's turn ends | Some Claude Code notices are plain text and carry no structured end status | No action for plain notices. If `claude --debug` shows `a task notification held no complete` or `unknown notification status`, report that line |
+| Jobs end only when Claude's turn ends, or never | Some Claude Code notices are plain text and carry no structured end status. A shell then ends at the `[exited with code N]` line of its output file, and another job at the next turn-end snapshot. Managed settings or a Team or Enterprise login send Downrange no snapshot | No action for plain notices. If `claude --debug` shows `a task notification held no complete` or `unknown notification status`, report that line |
| A job shows `ended with its agent` | The background command of a subagent that Claude waited for ends when that subagent answers | Nothing |
| Something else looks wrong | A hook failed | The first failure of each hook shows as a dim `downrange:` line; `claude --debug` has every one |
diff --git a/docs/usage.md b/docs/usage.md
index 0845a6a..3c02e4f 100644
--- a/docs/usage.md
+++ b/docs/usage.md
@@ -8,13 +8,13 @@ A job here is one background task of this session. Claude Code's agent view call
| Work | Starts when | Ends when |
| --- | --- | --- |
-| Shell command | A Bash call runs in the background: `run_in_background`, ctrl+b, or a command moved to the background at its timeout | Its notification arrives, `TaskStop` stops it, or the session's task list no longer holds it |
+| Shell command | A Bash call runs in the background: `run_in_background`, ctrl+b, or a command moved to the background at its timeout | Its notification arrives, `TaskStop` stops it, a read of its output file finds Claude Code's closing `[exited with code N]` line, or the session's task list no longer holds it |
| Monitor | A Monitor call starts a watch | Its end notification arrives, or the task list no longer holds it |
| Subagent | An Agent call runs in the background or remotely | Its notification arrives, the agent list reports its end, or the task list no longer holds it |
| Workflow | A Workflow call launches a run | Its notification arrives, or the task list no longer holds it |
-| Wakeup | `ScheduleWakeup` sets the next dynamic `/loop` wakeup | A scheduled prompt fires from 5 seconds before its due time on, `ScheduleWakeup` stops it, a turn-end snapshot taken at least 10 seconds after scheduling holds no one-shot schedule, or it is 10 minutes past due. The prompt and the one-shot schedule can belong to another schedule |
+| Wakeup | `ScheduleWakeup` sets the next dynamic `/loop` wakeup | A scheduled prompt fires from 5 seconds before its due time on, `ScheduleWakeup` stops it, a turn-end snapshot taken at least 10 seconds after scheduling holds no one-shot schedule, or it is 10 minutes past due. The prompt and the one-shot schedule can belong to another schedule. Where no turn-end snapshot reaches Downrange, only the other ways clear it (see [Known limits](#known-limits)) |
-Work that started before Downrange saw it joins when a notification or the session's task list names it. Its age then starts with `≥` (at least).
+Work that started before Downrange saw it joins when a notification names it, or the session's task list, where turn-end snapshots reach Downrange. Its age then starts with `≥` (at least).
A wakeup has a 10-second grace period after scheduling because a turn-end snapshot can predate it. If cancellation falls within that period, it clears at a later eligible turn end or 10 minutes past due.
@@ -150,11 +150,11 @@ Some background commands send their output to a file, such as `python3 build.py
1. At launch, Downrange reads the command's text and looks for the one file that it sends its output to. These forms count: `> FILE`, `>> FILE`, `1> FILE`, `1>> FILE`, `&> FILE`, `&>> FILE`, and `>& FILE`. A `| tee FILE` or `| tee -a FILE` at the end of a pipeline also counts.
2. At each read, Downrange looks at both files: Claude Code's output file and the redirect file. The row shows the file that changed last, by modification time. When the two times are equal, the row shows Claude Code's output file. For example, in `ls scripts/ && python3 scripts/run_tests.py > /tmp/run_tests_output.log 2>&1`, the `ls` lines go to Claude Code's output file first. The test lines go to the redirect file later, so the row shows them.
-3. Claude Code's output file counts only while it holds a line of the job. The `[exited with code N]` line that Claude Code adds at the end is no line of the job, and it does not count as a change of that file.
+3. Claude Code's output file counts only while it holds a line of the job. The `[exited with code N]` line that Claude Code adds at the end is no line of the job, and it does not count as a change of that file. It ends the job, with that exit code.
4. Each read of the redirect file is the same as a read of Claude Code's output file: the same intervals, the same size limits, and `tail` above 4 MiB. Downrange reads the redirect file again when its size or its modification time changes.
5. While the row shows lines from the redirect file, it takes the progress, the ETA, and the last line from that file, and shows the note `output: `. When the row changes from one file to the other, it drops the progress and the last line of the first file, and the rate ETA starts again.
6. When the redirect file does not exist yet, or is not a regular file, the row shows Claude Code's output file if that file counts. Otherwise the row shows `output unavailable` until a read of the redirect file works.
-7. The end and the exit code still come from Claude Code's notification. A line such as `exit=1` in the file shows as the last line, and Downrange does not read an exit code from it.
+7. The end and the exit code come from Claude Code's notification, or from the `[exited with code N]` line at the end of Claude Code's output file. A line such as `exit=1` in the redirect file shows as the last line, and Downrange does not read an exit code from it.
The path must be literal: absolute, or relative to the session's working folder at launch. On Windows, Downrange follows no redirect file. The shell there can map a path to another folder. Downrange does not follow:
@@ -172,7 +172,7 @@ The path must be literal: absolute, or relative to the session's working folder
## Usual durations
-A shell command that Downrange saw start adds its run time to the history of that command in that folder. This happens when its completion notice arrives while its row still runs and reports exit code 0 or no exit code. The run time starts when the command went to the background, so it leaves out time in the foreground before ctrl+b or a timeout move. Runs of every launch mode share one history. The ETA uses the median of the last 5 runs. The history keeps up to 200 commands under hashed keys and holds no command text, path, or output. It lasts across sessions.
+A shell command that Downrange saw start adds its run time to the history of that command in that folder. This happens when its completion notice, or the `[exited with code 0]` line at the end of its output file, arrives while its row still runs, with exit code 0 or no exit code. The run time starts when the command went to the background, so it leaves out time in the foreground before ctrl+b or a timeout move. Runs of every launch mode share one history. The ETA uses the median of the last 5 runs. The history keeps up to 200 commands under hashed keys and holds no command text, path, or output. It lasts across sessions.
## Answering status prompts locally
@@ -211,13 +211,13 @@ A change takes effect at once: Claude Code reloads the mod with the new values.
## `/clear`, `/resume`, and `/branch`
-Background work keeps running after these commands, in the same Claude Code process. The jobs and the wakeup belong to that process: Downrange keeps them in its own memory, which these commands do not touch. Every session of the process shows the same running jobs and the same wakeup. The band, the pane, and `/downrange` show them at once in the new session:
+Background work keeps running after these commands, in the same Claude Code process. The jobs and the wakeup belong to that process: Downrange keeps them in its own memory, which these commands do not touch. Every session of the process shows the same running jobs and the same wakeup. The band, the pane, and `/downrange` show them at once in the new session, or within a second under managed settings or a Team or Enterprise login (see [Known limits](#known-limits)):
- Each job keeps its real age, last line, progress, and ETA. Downrange goes on reading each shell's output file and redirect file, as before the command.
- Each update applies at once, also during the change: an output read, a launch, a notification, a subagent's tool call, a turn-end snapshot, or a scheduled prompt. The rules are the same as in a session. So a late notification or Monitor event of a job that ended before the change does not bring the job back.
- **Finished** shows the jobs that ended after the session started. A session that this process has not shown before starts when the old session ends, so it shows the jobs that end during the change and after it. An in-app `/resume` back to a session that this process showed keeps that session's start, for the last 32 sessions. **Hide** does not carry: a band that you hid shows again while work is live.
-Downrange draws from a copy of the session's view in session state, which Claude Code clears at each of these commands. Downrange writes the copy again after each change, and when the new session starts. It writes one copy at a time, in order, so the last copy holds the new session's view.
+Downrange draws from a copy of the session's view in session state, which Claude Code clears at each of these commands. Downrange writes the copy again after each change, and when the new session starts. It writes one copy at a time, in order, so the last copy holds the new session's view. Until Downrange knows the new session, the copy shows every running job and the wakeup, and no job that ended before the change.
Only a change inside one Claude Code process carries. `/fork` copies the conversation into a background session in a process of its own: Downrange starts empty there, and your session keeps its rows. `claude --resume` from a shell also starts a new process, where Downrange starts empty.
@@ -233,7 +233,7 @@ Only a change inside one Claude Code process carries. `/fork` copies the convers
- **Monitor progress after an update.** An earlier version of Downrange saved progress with no mark of its format, and counted any ratio. With `genericRatios` off, the first load of this version drops that progress and its rate ETA. A shell reads its output again at once, and its rate ETA starts again from the new reading. A Monitor shows its event count until its next event in a known format.
- **Monitor deadlines and rate ETAs.** A Monitor watch shows its deadline only when it has no progress-based ETA. Once its progress gives a rate ETA, or reaches 100% and shows `finishing`, the deadline no longer shows. A rate ETA can run past the deadline: a 30-minute watch that goes from 1% to 2% in its first minute shows `~1h 38m`, although its deadline is 29 minutes away.
- **Time in the foreground before a move to the background.** A shell job's clock starts when the Bash call returns its background result. A command that first ran in the foreground, such as one moved at its timeout or with ctrl+b, loses that time. Its age, its `took` time, and the run it adds to the duration history count only the time after the move. For example, a command moved after 2 minutes that ends 1 minute later shows `took 1m 00s` and records 1 minute. Runs of one command in one folder share one history, whatever the launch mode. The next run of that command that starts in the background then shows `~1m usual`, even when it needs 3 minutes, and in the pane it can turn yellow early. A moved run can get a usual-duration ETA that is too long.
-- **Duration history after a snapshot end.** A run adds to the duration history only when its completion notice arrives while its row still runs. When a turn-end snapshot already ended the row as `ended without a report`, the later notice marks it completed but adds no duration.
+- **Duration history after another end.** A run adds to the duration history only while its row still runs: when its completion notice arrives, or when a read finds the `[exited with code 0]` line at the end of its output file. When another input already ended the row, neither adds a duration. For example, when a turn-end snapshot already ended the row as `ended without a report`, the later notice marks it completed but adds no duration. When the closing line ended the row first, the later notice adds no second run.
- **Simultaneous duration records.** Two sessions recording durations together can lose new runs, because the duration store has no versioned write. One lost write can hold several runs. When it held a command's first runs, that command keeps no history and shows no usual-duration estimate. Runs stored before both writes began remain, with their estimates.
- **Failed output reads.** While reads of a shell's output fail, its row shows no `quiet` flag and no rate ETA. When a shell job ends, Downrange reads its output one more time. If that read fails, Downrange does not try it again. The job can keep `output unavailable` or `tail failed` and the last line of an earlier read. Only a later ending, or a later notification that names the output file, asks for a new read. Reads of a redirect file fail and try again in the same way.
- **Large outputs on Windows.** Outputs over 4 MiB need `tail` on PATH. Without it, such a job shows `tail failed`. A launch or end note that the job already shows, such as `backgrounded by you`, stays in its place; `claude --debug` still shows `cannot tail the output of task `.
@@ -245,9 +245,11 @@ Only a change inside one Claude Code process carries. `/fork` copies the convers
- **Agents that wait.** A subagent that the agent list reports as pending, waiting, or idle shows that word, beside its tool count when it shows one. It shows no `quiet` flag and no failed-calls flag, however long it stays in that state.
- **Agents that run elsewhere.** Downrange sees the tool calls only of agents that run in this Claude Code process. A remote subagent shows `remote`, and a teammate in a terminal pane of its own shows `own pane`. Their rows show no tool count and no `quiet` flag, so a stuck or dead one looks the same as a busy one. A row whose launch result Downrange missed, or that an earlier version of Downrange saved, still keeps `0 tools` and shows `quiet` from 2 minutes after launch, even while the agent works. Its notification or a snapshot still ends it.
- **A teammate whose pane closed.** A teammate in a terminal pane of its own writes `running` or `idle` in its team's roster. When that pane closes or dies, the roster keeps the last word. Its notification or a snapshot ends its row, but a later agent list read shows it running again. That read comes at each turn end, and every second while another agent runs. The row can then switch between ended and running, keep the band visible, and show `idle` with no end note. Check the teammate's own pane before you act on the row. Use **Hide** to dismiss the band.
-- **A session moved to another process.** `/background` and a hand-off to the desktop app start a fresh process that resumes the conversation. Downrange starts empty there. Running tasks return at the next turn end, with `≥` ages and no last line. A shell's notification can still name its output file, end it, and show its last line. The `/loop` wakeup returns only with the next `ScheduleWakeup` result.
-- **Changes during `/clear`, `/resume`, or `/branch`.** No update is lost at these commands: it goes to Downrange's memory at once. The band and the pane can lag for a moment: when a copy that Downrange began before the command is slow to land, the new session's view shows when that copy lands. Downrange remembers the last 2,000 tasks that left its list, and forgets the oldest first. A task does not count toward the 2,000 while it is back in the list. A finished job leaves the list through **Clear finished** or through the 20-job limit of **Finished**. **Clear finished** clears the finished jobs of every session in the process. A reload of Downrange, as an update or a change of its options causes, starts its memory again from the copy of the current session's view, so the current session keeps its jobs, its wakeup, and **Hide**. An update that comes during the reload waits for that copy and applies on top of it. Each hook also waits until a copy that holds its update has landed, so a reload right after it keeps the update. One hook waits 2 seconds at most in all, from its first wait, also when it makes more than one update. So when Claude Code answers a write slowly, or updates come without a pause, a hook can go on with its update in memory only. A hook that waits for a tool call between two updates, such as the one that counts a subagent's tool call, can go on at once after the call. The ticker runs either way: it reads outputs, and it writes the copy again. A reload of Downrange before the next copy lands then loses that update; a later turn-end snapshot corrects the jobs, and the next `ScheduleWakeup` result corrects the wakeup. Ended jobs that this session did not show, and the starts of the other sessions, are then gone. Downrange keeps the removal of each such job, within the 2,000, so a late notification or Monitor event does not bring it back.
-- **Another identity for the mod.** Claude Code keeps the duration history per plugin identity, `@`. Installed from the `downrange` marketplace, Downrange is `downrange@downrange`. Loaded with `--plugin-dir`, it is `downrange@inline`. A copy under another identity, such as one installed from another marketplace, keeps a separate history, which starts empty. It shows no usual-duration ETA until it records runs of its own. The jobs and the wakeup live in session state under the plugin name `downrange`. A build under another plugin name cannot read them, so it starts with no jobs and no wakeup. A later turn-end snapshot brings running tasks back, with `≥` ages. The `/loop` wakeup returns only with the next `ScheduleWakeup` result.
+- **A session moved to another process.** `/background` and a hand-off to the desktop app start a fresh process that resumes the conversation. Downrange starts empty there. Running tasks return at the next turn end, where turn-end snapshots reach Downrange, with `≥` ages and no last line. A shell's notification can still name its output file, end it, and show its last line. The `/loop` wakeup returns only with the next `ScheduleWakeup` result.
+- **Changes during `/clear`, `/resume`, or `/branch`.** No update is lost at these commands: it goes to Downrange's memory at once. The band and the pane can lag for a moment: when a copy that Downrange began before the command is slow to land, the new session's view shows when that copy lands. Downrange remembers the last 2,000 tasks that left its list, and forgets the oldest first. A task does not count toward the 2,000 while it is back in the list. A finished job leaves the list through **Clear finished** or through the 20-job limit of **Finished**. **Clear finished** clears the finished jobs of every session in the process. A reload of Downrange, as an update or a change of its options causes, starts its memory again from the copy of the current session's view, so the current session keeps its jobs, its wakeup, and **Hide**. An update that comes during the reload waits for that copy and applies on top of it. Each hook also waits until a copy that holds its update has landed, so a reload right after it keeps the update. One hook waits 2 seconds at most in all, from its first wait, also when it makes more than one update. So when Claude Code answers a write slowly, or updates come without a pause, a hook can go on with its update in memory only. A hook that waits for a tool call between two updates, such as the one that counts a subagent's tool call, can go on at once after the call. The ticker runs either way: it reads outputs, and it writes the copy again. A reload of Downrange before the next copy lands then loses that update; a later turn-end snapshot corrects the jobs, where turn-end snapshots reach Downrange, and the next `ScheduleWakeup` result corrects the wakeup. Ended jobs that this session did not show, and the starts of the other sessions, are then gone. Downrange keeps the removal of each such job, within the 2,000, so a late notification or Monitor event does not bring it back.
+- **Another identity for the mod.** Claude Code keeps the duration history per plugin identity, `@`. Installed from the `downrange` marketplace, Downrange is `downrange@downrange`. Loaded with `--plugin-dir`, it is `downrange@inline`. A copy under another identity, such as one installed from another marketplace, keeps a separate history, which starts empty. It shows no usual-duration ETA until it records runs of its own. The jobs and the wakeup live in session state under the plugin name `downrange`. A build under another plugin name cannot read them, so it starts with no jobs and no wakeup. A later turn-end snapshot brings running tasks back, with `≥` ages, where turn-end snapshots reach Downrange. The `/loop` wakeup returns only with the next `ScheduleWakeup` result.
+- **Managed settings and Team or Enterprise logins.** Claude Code loads a built-in guard when the machine has managed settings, or when the session uses a Team or Enterprise `/login`. The guard keeps every `classic.*` hook event from the mods that you install, so Downrange gets no `classic.SessionStart`, `classic.Stop`, and `classic.SubagentStop`. Its other hooks still run. At `/clear`, `/resume`, and `/branch`, Claude Code clears the session's state, which holds the copy of the view that the band and the pane draw. Downrange reads the session id and writes the copy of the new session's view at once after the change. Until the id names the new session, it does so again at each of its hooks other than a drawing, and at each timer period, once a second. So the band, the pane, and `/downrange` show the running jobs and the wakeup at once, unless Claude Code clears the state after that first copy. They then show them at the next copy. Until Downrange knows the new session, they show no job that ended before the change. Downrange cannot tell `/branch` from an in-app `/resume` there. A session that this process showed keeps its start for **Finished**, for the last 32 sessions. Any other session starts at the change, so a branch shows no job that ended in its parent. A slash command, such as `/reload-plugins`, and a change in `/config` write the copy of the new session's view before they take effect. When that copy is still being written, they wait until it lands, 2 seconds at most, as each hook waits for its copy (see **Changes during `/clear`, `/resume`, or `/branch`** above). When the last write of the copy failed, they write it once more, within the same 2 seconds. So a reload of Downrange that they cause, such as at a change of a Downrange option, keeps the jobs and the wakeup, unless that write fails too. Only a reload that no slash command and no `/config` change causes, such as one after an edit of a settings file, can still lose them. It loses them when it comes after Claude Code cleared the state and before Downrange's next copy of the view lands. Downrange writes that copy at once after the change. If Claude Code clears the state after that, Downrange writes the copy again at the next timer period, or, once it knows the new session, at the next update of the jobs or the wakeup. Such a reload starts Downrange's memory empty. The jobs, the wakeup, and the removals of ended jobs are then gone. A Monitor returns with its next event, with a `≥` age. Another running job shows again only when its notification ends it, and the wakeup only with the next `ScheduleWakeup` result. With no turn-end snapshot, Downrange never compares its rows with Claude Code's task list. A `TaskStop` call ends the row of the task that it stops. A shell also ends at its notification, at the `[exited with code N]` line at the end of its output file, or, with the note `ends with its agent`, when its subagent answers. A Monitor or workflow also ends at its notification. A subagent or teammate also ends at its notification, or when the agent list reports its end. A row that none of these ends keeps running until the process ends. That happens when its end notice is plain text, holds no complete block, or has a status word that Downrange does not know, and no other of these inputs comes. It happens to a teammate that leaves the agent list as it ends, and to an agent whose late end Downrange dropped (see **Inputs that reach Downrange late** above), when it leaves the agent list before a list read reports that end. **Clear finished** keeps running rows, and **Hide** hides the band only until a new job starts or the session changes. A cancelled `/loop` wakeup shows its countdown, then `due now`, until 10 minutes past due. Downrange adds a task that it did not see start only from a notification: a Monitor event adds a running Monitor, with a `≥` age, and a notification with an end status adds the ended task to **Finished**. So a session moved to another process starts empty. A task that started before the move shows only when such a notification of it arrives. `awaiting confirmation` never shows.
+- **A closing line that the job prints.** A read that finds a line such as `[exited with code 0]` at the end of Claude Code's output file ends the row of a running shell, with that exit code. Claude Code writes that line when the command ends. A job that prints such a line itself as its last line ends its row early, and the row stays ended while the job goes on. The job's notification later ends it again, with the real exit code.
- **Quoted failure text in task descriptions.** A failed or stopped task's description can contain quoted text followed by `failed:` or `was stopped:`, such as `Investigate "build" failed: exit code 2`. If Downrange first sees the task at its end, it can mistake description text for the end details. This also happens when an already tracked task's title was shortened or cleaned. The band, pane, and `/downrange` can show an incorrect title or exit code; end notes can hide the actual cause. Check the full task notification in the transcript before acting on those details. Use short task descriptions without quoted failure phrases to avoid this ambiguity.
## Troubleshooting
@@ -257,10 +259,10 @@ Run `claude --debug`, or `claude --debug-file `, and search for `downrange
| Message | Meaning | What to do |
| --- | --- | --- |
| `: hook throw: …` or `: hook timeout: …` | A hook failed, such as `tool.call{tool=Bash}`. Handlers other than `/downrange` and the Jobs pane pass the event on unchanged | Report a bug with the line |
-| `state.set: cannot write the copy of the session's view: …; the next tick writes it again` | Claude Code refused or failed a write of the copy that the band and the pane draw. Downrange's memory took the update, and the hook went on | Nothing. The next tick writes the copy again. Report a bug with the line if it repeats |
+| `state.set: cannot write the copy of the session's view: …; the next tick writes it again` | Claude Code refused or failed a write of the copy that the band and the pane draw. Downrange's memory took the update, and the hook went on | Nothing. The next tick writes the copy again. A slash command or a `/config` change that comes before that tick writes it once more before it takes effect. Report a bug with the line if it repeats |
| `clock.every: …` with a state write's cause | A copy of the session's view that the band and the pane draw could not be written. Downrange's memory holds every update, and each tick tries the copy again | Nothing. Report a bug with the line if it repeats |
| `session.start: /downrange was not registered: …` | Claude Code rejected command registration; a built-in name conflict is one possible cause | Read the appended cause. Open the pane with the band's **Open** |
-| `state.get: unsupported or invalid jobs state; reset safely` or `unsupported or invalid wakeup state; reset safely` | The stored format is unknown or invalid | Running tasks can return through snapshots; a new wakeup result restores its countdown |
+| `state.get: unsupported or invalid jobs state; reset safely` or `unsupported or invalid wakeup state; reset safely` | The stored format is unknown or invalid | Running tasks can return at the next turn end, where turn-end snapshots reach Downrange; a new wakeup result restores its countdown |
| `tool.call: no output file in the result of task ; …` | The tool result named no output file | Nothing. The job shows without a last line |
| `clock.every: cannot read the output of task : …` | The output file is gone or unreadable | Nothing. Reads try again while the job runs |
| `clock.every: cannot tail the output of task : …` | `tail` failed on an output over 4 MiB | Check that `tail` is on PATH |
@@ -269,21 +271,22 @@ Run `claude --debug`, or `claude --debug-file `, and search for `downrange
| `tool.call: cannot place the redirect file of task : …` | The session's working folder could not be read at launch, so Downrange does not follow the redirect file of this job | Nothing. The job shows the lines of Claude Code's output file |
| `clock.every: cannot list agents: …` | The agent list failed | Nothing. Agent states update after the next good list |
| `tool.call: duration history unavailable: …` | The history lookup failed for this launch; its usual-duration estimate stays unavailable | Recovery benefits later launches |
-| `prompt.submit: duration history unavailable: …` or `session.append: duration history unavailable: …` | Recording a completed run failed; that sample is lost, but existing estimates remain | Recovery benefits later recordings |
+| `prompt.submit: duration history unavailable: …`, `session.append: duration history unavailable: …`, or `clock.every: duration history unavailable: …` | Recording a completed run failed; that sample is lost, but existing estimates remain | Recovery benefits later recordings |
| `: duration history had the wrong shape; …` | The stored history was damaged | Nothing. The next record replaces it |
| `command.run: the Jobs pane was not placed: …` | Nothing draws in this session, or the surface does not show this pane | Use `/downrange` |
| `command.run: the Jobs pane could not be toggled: …` | Opening or closing the pane failed | Use `/downrange` |
| `ui.press: