@oar-sh/oar 0.9.12 → 0.9.13

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (62) hide show
  1. package/CHANGELOG.md +206 -0
  2. package/README.md +9 -9
  3. package/package.json +1 -1
  4. package/server/README.md +186 -11
  5. package/server/claude-worker/claude-session-process.mjs +53 -2
  6. package/server/claude-worker/task-output-discovery.mjs +28 -2
  7. package/server/copilot-worker/copilot-continuation-signals.mjs +11 -5
  8. package/server/copilot-worker/copilot-sdk-session-process.mjs +10 -0
  9. package/server/db-schema.mjs +10 -0
  10. package/server/grok-worker/acp-host-services.mjs +6 -3
  11. package/server/grok-worker/grok-background-tasks.mjs +7 -0
  12. package/server/migrations/0018-background-task-rows.mjs +101 -0
  13. package/server/public/app/action-confirmations.js +12 -15
  14. package/server/public/app/api-client.js +57 -3
  15. package/server/public/app/background-tasks-view.mjs +56 -7
  16. package/server/public/app/bootstrap.js +7 -0
  17. package/server/public/app/conversation-preferences.mjs +10 -4
  18. package/server/public/app/conversation-view.js +33 -9
  19. package/server/public/app/host-suspend-ui.js +45 -44
  20. package/server/public/app/journal-view.js +9 -3
  21. package/server/public/app/push-settings.js +1 -0
  22. package/server/public/app/relay-prompt-sanitizer.mjs +9 -2
  23. package/server/public/app/relay-restart-note-view.mjs +36 -0
  24. package/server/public/app/relay-restart-ui.js +568 -0
  25. package/server/public/app/settings-modal.js +52 -0
  26. package/server/public/app/socket-handlers.js +1 -1
  27. package/server/public/app/task-console-render.mjs +171 -7
  28. package/server/public/app/task-console.mjs +111 -11
  29. package/server/public/app/task-output-text.mjs +15 -6
  30. package/server/public/app/transcript-jump-bottom-view.mjs +5 -1
  31. package/server/public/app/transcript-jump-bottom.mjs +20 -0
  32. package/server/public/index.html +67 -0
  33. package/server/relay-tools.md +5 -0
  34. package/server/repositories/message-repository.mjs +3 -0
  35. package/server/repositories/remote-relay-repository.mjs +4 -2
  36. package/server/routes/messages-routes.mjs +155 -27
  37. package/server/routes/relay-board-routes.mjs +19 -2
  38. package/server/routes/relay-restart-routes.mjs +73 -0
  39. package/server/routes/sessions-routes.mjs +75 -3
  40. package/server/server-runtime.mjs +192 -137
  41. package/server/services/about-service.mjs +13 -12
  42. package/server/services/background-task-persistence.mjs +133 -0
  43. package/server/services/background-task-store.mjs +240 -13
  44. package/server/services/conversation-model-fallback.mjs +57 -0
  45. package/server/services/host-suspend-command.mjs +2 -1
  46. package/server/services/preview-proxy-server.mjs +14 -1
  47. package/server/services/process-list-service.mjs +2 -1
  48. package/server/services/provider-model-selection.mjs +195 -0
  49. package/server/services/push-dispatch-service.mjs +9 -2
  50. package/server/services/relay-event-service.mjs +27 -9
  51. package/server/services/relay-prompt-sanitizer.mjs +7 -0
  52. package/server/services/relay-restart-picture.mjs +209 -0
  53. package/server/services/relay-restart-queue.mjs +380 -0
  54. package/server/services/relay-shutdown-gate.mjs +4 -1
  55. package/server/services/remote-relay-dispatcher.mjs +187 -19
  56. package/server/services/restart-resume-service.mjs +303 -0
  57. package/server/services/session-worker-process-service.mjs +6 -4
  58. package/server/services/task-output-service.mjs +217 -18
  59. package/shared/relay-events.mjs +55 -2
  60. package/shared/remote-relay-contract.mjs +15 -8
  61. package/shared/task-output-text.mjs +196 -83
  62. package/shared/worker-runtime/worker-websocket-link.mjs +96 -20
package/CHANGELOG.md CHANGED
@@ -3,6 +3,212 @@
3
3
  All notable changes to OAR are documented here. The format follows
4
4
  [Keep a Changelog](https://keepachangelog.com/en/1.1.0/); versions follow semver.
5
5
 
6
+ ## [0.9.13] — 2026-10-20
7
+
8
+ Highlights: a queued relay restart now shows every page what it waits for,
9
+ which messages it holds and which background tasks keep running, and after a
10
+ time you set it asks instead of waiting forever; it never restarts by itself
11
+ while turns run, and can tell the sessions a restart cut off to carry on
12
+ (opt-in). The task console survives a relay restart, marks where a `| tail`
13
+ holds output back, keeps the task's row in view, and counts a test run over
14
+ its whole log. A message
15
+ sent without a model keeps the conversation's model, effort and mode,
16
+ `remote_relay` refuses a model the provider does not list, watching agents
17
+ are no longer woken by every finished shell command, workers reconnect after
18
+ a hung restart, and the unit suite passes on Windows again.
19
+
20
+ ### Upgrade notes
21
+
22
+ - Migration 0018 (the table `background_task_rows`) runs by itself on the
23
+ first start. Rows the runtime before this update held in memory are lost
24
+ one last time at the restart into it. The Node.js requirement is unchanged
25
+ (22.13 or newer).
26
+ - New settings in **Settings › General**: **Ask after a queued relay restart
27
+ has waited** (30 minutes by default) and **After a relay restart, tell
28
+ interrupted sessions to carry on** (off by default); a new push event,
29
+ **Queued relay restart waited its maximum time** (Settings › Notifications),
30
+ is on by default.
31
+ - **Watching agents:** a subscription that named no events now leaves out
32
+ successful shell commands (sub-agent ends and failures still wake it); one
33
+ that named its events keeps them. Name `task_finished` to get every task end.
34
+ - **Scripts that post `/api/message` without a model** to an existing
35
+ conversation now get the conversation's own model, effort and mode, or a
36
+ `409 CONVERSATION_MODEL_UNAVAILABLE` when that model left the catalog,
37
+ instead of the relay's first model.
38
+ - The npm package guard allows 7.75 MiB unpacked and 520 entries (was 7.5
39
+ MiB and 510); the restart queue, the stored task rows and the restart note
40
+ grew the server and the app to about 7.57 MiB and 514 entries.
41
+
42
+ ### Added
43
+
44
+ - **After a relay restart, tell interrupted sessions to carry on** (Settings
45
+ › General, off by default, a setting of the relay). A turn a restart cut
46
+ off for good (the agent had already taken the message, so it cannot run
47
+ again) used to stop without the agent ever hearing of it. With the setting
48
+ on, its session gets one compact **relay** note once the relay is back:
49
+ when the relay restarted, and to check what it was doing and carry on.
50
+ The user's message is never sent again, and nothing goes to idle sessions,
51
+ to turns the relay requeues (they run again by themselves), to archived
52
+ conversations or while a usage-limit pause holds the session; a message
53
+ queued meanwhile goes first.
54
+ - **The task console marks where a `| tail` holds the output back.** A
55
+ command piped into `tail` (`npm test 2>&1 | tail -40`) writes nothing
56
+ until it ends, so its console looked stuck. When the task's command line
57
+ shows such a pipe (`tail -f` and `head` pass lines on and are left
58
+ alone), the console marks where the output stops, with a hint to log to a
59
+ file instead, until more output comes or the task ends; the row's quiet
60
+ hint says the same on hover. The Claude, Copilot and Grok workers now report a
61
+ shell task's command line on its row (scrubbed of credentials by the relay
62
+ and cut at 2,000 characters; `read_session` passes it on).
63
+ - **Load earlier in the console's header.** It sat only above the first
64
+ line, out of reach while following the end of a long log; the header now
65
+ has it too whenever there is earlier output.
66
+
67
+ - **A queued relay restart shows what it waits for, and asks instead of
68
+ waiting forever.** **🌄 Restart web relay** first lists the sessions with
69
+ a running turn it would wait for, the messages it would hold, and the
70
+ sessions with background tasks (commands, monitors, sub-agents), each with
71
+ whether it keeps running; **Restart when idle** queues the restart,
72
+ **Restart now** does not wait. While a restart is queued every page shows a
73
+ banner: *Relay restart queued — waiting for N sessions*, the sessions as
74
+ links, how long it has waited, that new messages are held until then, the
75
+ background tasks, and **Restart now** (it asks first and names the
76
+ sessions it does not wait for) and **Cancel restart**. A new setting,
77
+ **Ask after a queued relay restart has waited** (Settings › General,
78
+ 30 minutes by default, 5 minutes to 4 hours or no limit), bounds the wait:
79
+ after it the banner asks *restart now, keep waiting or cancel?* and a push
80
+ notification (a new event, on by default) asks the same. It never
81
+ restarts while the turns run, and still restarts as soon as they end;
82
+ **Keep waiting** waits another period.
83
+ `POST /api/relay/shutdown` answers with the same picture (`waitingFor`,
84
+ `held`, `backgroundTasks` with `survives`), so an agent that requests a
85
+ restart can tell the user, and takes `immediate: true`; `GET
86
+ /api/relay/shutdown` and `POST /api/relay/shutdown/keep-waiting` are new.
87
+
88
+ ### Changed
89
+
90
+ - **The open console leaves the task's row in view.** On a screen wider
91
+ than a phone the console now ends above the task's row in the composer
92
+ panel, so the row's ✓/✘ chip, clock and **Stop** stay visible and live
93
+ beside it; on a phone it stays full-screen, with the same chip in its
94
+ header.
95
+ - **One wording for a task without output.** The row's hover title said
96
+ "output not available for this provider" and the console "Output is not
97
+ available for this provider."; both now use the sentence, from the same
98
+ text the relay answers with.
99
+ - **A watching agent is no longer woken for every shell command that ended
100
+ well.** A relay event subscription that names no `events` now gets
101
+ `subagent_finished` instead of `task_finished`: a background sub-agent that
102
+ finished still wakes the watcher, and so does any task that failed or
103
+ exited non-zero (`task_failed`), but a shell command that ended well does
104
+ not. A busy worker ends one every minute or so, and each was a turn of the
105
+ watcher on its whole context (35 during one 20-minute wait were seen).
106
+ Name `task_finished` in `events` to get every task end, as before.
107
+ Existing subscriptions that named no events switch to the new default by
108
+ themselves; those that named their events keep them. A paired relay on an
109
+ older OAR applies its own default and refuses `subagent_finished` by name.
110
+
111
+ ### Fixed
112
+
113
+ - **A message sent without a model keeps the conversation's model and
114
+ effort.** A message to an existing Copilot conversation that named no model
115
+ (sent by a script or an agent, not by the composer) ran on the first model
116
+ of the catalog with effort `none`, which some models refuse, and then saved
117
+ that model and effort as the conversation's own, so the composer showed
118
+ them from then on. Such a message now runs on the conversation's model and
119
+ effort, and saves nothing it did not name. If the conversation's model has
120
+ left the catalog, the send is refused with a message saying so instead of
121
+ switching to another model. OpenAI, Claude, Cursor and Grok conversations
122
+ keep their effort the same way, and so does the follow-up of a plan
123
+ board's action. A message that names no mode runs in the conversation's
124
+ mode instead of agent mode, and no longer switches a plan-mode
125
+ conversation to agent. A message whose turn failed can be sent again at
126
+ once: it no longer counts as a duplicate of itself.
127
+ - **Test counts of a long run no longer drop out.** The relay counted a
128
+ test run from the last 64 KB of its output only, so a live run that wrote
129
+ more lost its runner's first line: the chip disappeared or undercounted.
130
+ A run still going is now counted over its whole log, read on as it grows
131
+ (and read again in bounded steps after a relay restart); the closing
132
+ summary still decides once it is there.
133
+ - **Credentials in background-task rows are blanked.** A task's
134
+ description, summary and last tool call went to the pages, `read_session`
135
+ and the stored console rows as the worker sent them, and a Grok terminal's
136
+ (or a Copilot shell's) description is its command line. The relay now
137
+ scrubs them like the console output. The scrubber also catches `curl -u
138
+ name:password` (also attached), `--password`/`--token`/`--secret`
139
+ values, `mysql -p…`, `docker login -p`, `sshpass -p`, `redis-cli -a`,
140
+ `aws_secret_access_key` in a file or `aws configure set`, Slack webhook
141
+ paths, `?key=`/`&sig=`/`&token=` query values and an `Authorization`
142
+ header in any case and with any scheme word, and leaves a reference such
143
+ as `$(cat ~/.token)` or `$API_TOKEN` alone.
144
+ - **Inverse text in the task console.** vitest's RUN, PASS and FAIL badges
145
+ (SGR 7) showed as plain words; inverse text now swaps its colours, so
146
+ they show filled.
147
+ - **The task console survives a relay restart.** Its rows lived in the
148
+ relay's memory: after a restart a running command came back to the panel
149
+ once its worker reported, but the dimmed rows of commands that had
150
+ finished within the hour were gone, and a console open at the time said
151
+ the output was no longer available. The rows are now kept in the database
152
+ (migration 0018): finished rows come back with their ✓ or ✘ for the rest
153
+ of their hour, and a running command's console reads its output at once.
154
+ Such a command is shown as running again only when its worker says so
155
+ (until then its console says it waits for the worker, without **Stop**);
156
+ one whose worker did not survive the restart is marked finished with an
157
+ unknown result (a neutral ? instead of ✓ or ✘) after about a minute and a
158
+ half, its output still readable.
159
+ - **A scroll right after older messages load is no longer undone.** The page
160
+ put the reader back where they were when the older page arrived, a frame
161
+ later: scrolling in that frame (up again after the jump-to-bottom button,
162
+ for instance) was reverted, and could leave you at the end, following new
163
+ rows you meant to read later.
164
+ - **The jump-to-bottom badge counts a message that arrives just as you scroll
165
+ up.** One that landed before the page had noted the scroll, a frame and on
166
+ a busy page up to a tenth of a second, was taken as already seen.
167
+ - **The background-task panel no longer shrinks back for a moment.** While
168
+ the agent works the page re-reads the conversation, and an answer read
169
+ before the panel grew put the smaller set of tasks back until the next
170
+ read. The panel jumped, and a reader following the newest messages could be
171
+ left above them.
172
+ - **A worker reconnects after a relay restart even when its connect hangs.**
173
+ A worker's link to the relay was retried only once the socket reported an
174
+ error or a close. A connect that never finished, a socket the relay
175
+ accepted but never greeted, or a close that never completed while the
176
+ relay hung in its shutdown left the worker without a connection and
177
+ without a next attempt, and messages to its session stayed queued; the
178
+ relay's replacement of such a worker does not step in while the session
179
+ is working. The link now gives up a socket that has not completed its
180
+ handshake within 15 seconds, does not wait for a close after the relay
181
+ says it is shutting down, and retries a socket that fails to construct.
182
+ There is always exactly one next attempt, with the usual backoff of up to
183
+ 8 seconds, which now starts over only after a completed handshake. Failed
184
+ attempts log one line each in the worker log, at most one a minute after
185
+ the first three.
186
+ - **`remote_relay` refuses a model the provider does not list.**
187
+ `create_session` passed any model id on, so a Copilot id written with a
188
+ hyphen ("gpt-5-4" for "gpt-5.4") created a session on another model, which
189
+ every later `send` with that id failed to reach. A model in
190
+ `create_session` or `send` is now checked against the list New Chat offers
191
+ for the provider on the target relay, before any approval card: an unknown
192
+ one is refused with the close matches and the valid ids ("Unknown model
193
+ 'gpt-5-4' for Copilot on relay …; did you mean 'gpt-5.4'?"), and nothing is created
194
+ or sent. Claude also takes `opus`, `sonnet` and `haiku` (the newest listed
195
+ model of that name), and a Copilot id with its effort (`gpt-5.4-high`) runs
196
+ as that model with that effort. When the relay has no list for the provider yet, the
197
+ model goes on unchecked and the answer says so. A relay also refuses an
198
+ unknown Copilot model when another relay's agent starts a session on it.
199
+ - **A preview's websocket is dropped when the browser hangs up before the dev
200
+ server answers.** A browser that closed its connection with a plain FIN
201
+ while the upgrade was still waiting on the dev server left both sides
202
+ open, even when a reset followed: the request
203
+ to the dev server stayed pending, and the preview lane could not stop
204
+ until the dev server answered. Both sides are now dropped, and stopping
205
+ the lane also drops upgrades still waiting on the dev server.
206
+ - **The unit test suite passes and finishes on Windows again.** A preview
207
+ test waited forever for a websocket hang-up (fixed in the product, above),
208
+ and several tests expected bare program names or Unix paths where Windows
209
+ resolves the full path of its own tools; two unlocks recorded in the same
210
+ millisecond are now listed in the order they were recorded.
211
+
6
212
  ## [0.9.12] — 2026-10-18
7
213
 
8
214
  Highlights: a background command or monitor in the task panel opens a
package/README.md CHANGED
@@ -45,29 +45,29 @@ flowchart LR
45
45
  <table>
46
46
  <tr>
47
47
  <td width="42%" rowspan="4" valign="top">
48
- <a href="https://oar.sh/assets/shots/cfmail-mobile.png?v=0.9.12"><picture><source media="(prefers-color-scheme: light)" srcset="https://oar.sh/assets/shots/cfmail-mobile.light.png?v=0.9.12"><img src="https://oar.sh/assets/shots/cfmail-mobile.png?v=0.9.12" alt="OAR on a phone: a finished agent turn in a conversation, with the composer below"></picture></a>
48
+ <a href="https://oar.sh/assets/shots/cfmail-mobile.png?v=0.9.13"><picture><source media="(prefers-color-scheme: light)" srcset="https://oar.sh/assets/shots/cfmail-mobile.light.png?v=0.9.13"><img src="https://oar.sh/assets/shots/cfmail-mobile.png?v=0.9.13" alt="OAR on a phone: a finished agent turn in a conversation, with the composer below"></picture></a>
49
49
  <br><sub>On a phone: the same conversations, installable as an app.</sub>
50
50
  </td>
51
51
  <td width="58%" valign="top">
52
- <a href="https://oar.sh/assets/shots/cfmail-agents.png?v=0.9.12"><picture><source media="(prefers-color-scheme: light)" srcset="https://oar.sh/assets/shots/cfmail-agents.light.png?v=0.9.12"><img src="https://oar.sh/assets/shots/cfmail-agents.png?v=0.9.12" alt="Background subagents running in parallel in the task panel, each with live token counts, elapsed time, and its own Stop button"></picture></a>
52
+ <a href="https://oar.sh/assets/shots/cfmail-agents.png?v=0.9.13"><picture><source media="(prefers-color-scheme: light)" srcset="https://oar.sh/assets/shots/cfmail-agents.light.png?v=0.9.13"><img src="https://oar.sh/assets/shots/cfmail-agents.png?v=0.9.13" alt="Background subagents running in parallel in the task panel, each with live token counts, elapsed time, and its own Stop button"></picture></a>
53
53
  <br><sub>Agents working in parallel, each with live token counts and its own Stop.</sub>
54
54
  </td>
55
55
  </tr>
56
56
  <tr>
57
57
  <td valign="top">
58
- <a href="https://oar.sh/assets/shots/cfmail-question.png?v=0.9.12"><picture><source media="(prefers-color-scheme: light)" srcset="https://oar.sh/assets/shots/cfmail-question.light.png?v=0.9.12"><img src="https://oar.sh/assets/shots/cfmail-question.png?v=0.9.12" alt="A question card in the middle of a turn: the agent offers several choices and a free-text reply"></picture></a>
58
+ <a href="https://oar.sh/assets/shots/cfmail-question.png?v=0.9.13"><picture><source media="(prefers-color-scheme: light)" srcset="https://oar.sh/assets/shots/cfmail-question.light.png?v=0.9.13"><img src="https://oar.sh/assets/shots/cfmail-question.png?v=0.9.13" alt="A question card in the middle of a turn: the agent offers several choices and a free-text reply"></picture></a>
59
59
  <br><sub>When the agent needs a decision, it asks, and waits for your answer.</sub>
60
60
  </td>
61
61
  </tr>
62
62
  <tr>
63
63
  <td valign="top">
64
- <a href="https://oar.sh/assets/shots/pingcf-thoughts.png?v=0.9.12"><picture><source media="(prefers-color-scheme: light)" srcset="https://oar.sh/assets/shots/pingcf-thoughts.light.png?v=0.9.12"><img src="https://oar.sh/assets/shots/pingcf-thoughts.png?v=0.9.12" alt="An expanded Thoughts block showing the agent's reasoning above its reply"></picture></a>
64
+ <a href="https://oar.sh/assets/shots/pingcf-thoughts.png?v=0.9.13"><picture><source media="(prefers-color-scheme: light)" srcset="https://oar.sh/assets/shots/pingcf-thoughts.light.png?v=0.9.13"><img src="https://oar.sh/assets/shots/pingcf-thoughts.png?v=0.9.13" alt="An expanded Thoughts block showing the agent's reasoning above its reply"></picture></a>
65
65
  <br><sub>Read the reasoning, not just the answer.</sub>
66
66
  </td>
67
67
  </tr>
68
68
  <tr>
69
69
  <td valign="top">
70
- <a href="https://oar.sh/assets/shots/pagewright-watching.png?v=0.9.12"><picture><source media="(prefers-color-scheme: light)" srcset="https://oar.sh/assets/shots/pagewright-watching.light.png?v=0.9.12"><img src="https://oar.sh/assets/shots/pagewright-watching.png?v=0.9.12" alt="A session watching two other sessions: a finished reply arrives as a compact relay-event row, the watching 2 sessions list is open with Unsubscribe, and a request to also watch a session on a paired relay waits on an Allow / Deny card"></picture></a>
70
+ <a href="https://oar.sh/assets/shots/pagewright-watching.png?v=0.9.13"><picture><source media="(prefers-color-scheme: light)" srcset="https://oar.sh/assets/shots/pagewright-watching.light.png?v=0.9.13"><img src="https://oar.sh/assets/shots/pagewright-watching.png?v=0.9.13" alt="A session watching two other sessions: a finished reply arrives as a compact relay-event row, the watching 2 sessions list is open with Unsubscribe, and a request to also watch a session on a paired relay waits on an Allow / Deny card"></picture></a>
71
71
  <br><sub>Agents that watch agents: woken by events, approving nothing.</sub>
72
72
  </td>
73
73
  </tr>
@@ -116,11 +116,11 @@ API keys are entered in **⚙️ Settings** and stay on the relay host. The Clau
116
116
  <table>
117
117
  <tr>
118
118
  <td width="62%" valign="top">
119
- <a href="https://oar.sh/assets/shots/cfmail-steering.png?v=0.9.12"><picture><source media="(prefers-color-scheme: light)" srcset="https://oar.sh/assets/shots/cfmail-steering.light.png?v=0.9.12"><img src="https://oar.sh/assets/shots/cfmail-steering.png?v=0.9.12" alt="A running Claude turn with a message steered into it mid-turn, the reply streaming with its tool calls, and the Steer button in the composer"></picture></a>
119
+ <a href="https://oar.sh/assets/shots/cfmail-steering.png?v=0.9.13"><picture><source media="(prefers-color-scheme: light)" srcset="https://oar.sh/assets/shots/cfmail-steering.light.png?v=0.9.13"><img src="https://oar.sh/assets/shots/cfmail-steering.png?v=0.9.13" alt="A running Claude turn with a message steered into it mid-turn, the reply streaming with its tool calls, and the Steer button in the composer"></picture></a>
120
120
  <br><sub>Send a message while the agent works, and it steers the running turn.</sub>
121
121
  </td>
122
122
  <td width="38%" valign="top">
123
- <a href="https://oar.sh/assets/shots/usage-copilot-compact.png?v=0.9.12"><picture><source media="(prefers-color-scheme: light)" srcset="https://oar.sh/assets/shots/usage-copilot-compact.light.png?v=0.9.12"><img src="https://oar.sh/assets/shots/usage-copilot-compact.png?v=0.9.12" alt="The GitHub Copilot card in Check Usage: premium requests and plan quota with reset dates, and the cost of the last turn"></picture></a>
123
+ <a href="https://oar.sh/assets/shots/usage-copilot-compact.png?v=0.9.13"><picture><source media="(prefers-color-scheme: light)" srcset="https://oar.sh/assets/shots/usage-copilot-compact.light.png?v=0.9.13"><img src="https://oar.sh/assets/shots/usage-copilot-compact.png?v=0.9.13" alt="The GitHub Copilot card in Check Usage: premium requests and plan quota with reset dates, and the cost of the last turn"></picture></a>
124
124
  <br><sub>What each plan has left, in one place.</sub>
125
125
  </td>
126
126
  </tr>
@@ -137,7 +137,7 @@ API keys are entered in **⚙️ Settings** and stay on the relay host. The Clau
137
137
  - **Share** a conversation as a read-only link, hide single messages and the host's paths from it, and see when it was opened ([Sharing](docs/web-ui.md#sharing)).
138
138
  - **On your phone**: install OAR as an app and get push notifications ([Notifications and the app](docs/web-ui.md#notifications-and-the-app)).
139
139
 
140
- The full tour is in [Using the web UI](docs/web-ui.md); every setting and config key is in [Settings and configuration](docs/configuration.md). What changed in each release is in the [changelog](CHANGELOG.md), with [0.9.12](CHANGELOG.md#0912--2026-10-18) as the latest.
140
+ The full tour is in [Using the web UI](docs/web-ui.md); every setting and config key is in [Settings and configuration](docs/configuration.md). What changed in each release is in the [changelog](CHANGELOG.md), with [0.9.13](CHANGELOG.md#0913--2026-10-20) as the latest.
141
141
 
142
142
  ## Remote access
143
143
 
@@ -188,7 +188,7 @@ flowchart LR
188
188
 
189
189
  You stay in charge: per paired relay you decide whether agents may only read, also prompt, or do everything, and in the `ask` and `plan` modes every write asks you first. A subscription only notifies; it approves nothing.
190
190
 
191
- <p><a href="https://oar.sh/assets/shots/pagewright-cross-relay.png?v=0.9.12"><picture><source media="(prefers-color-scheme: light)" srcset="https://oar.sh/assets/shots/pagewright-cross-relay.light.png?v=0.9.12"><img src="https://oar.sh/assets/shots/pagewright-cross-relay.png?v=0.9.12" alt="A session watching two sessions on a paired relay: a reply arrives as a relay-event row, and after a short outage the events the other relay kept arrive as one replayed row" width="720"></picture></a></p>
191
+ <p><a href="https://oar.sh/assets/shots/pagewright-cross-relay.png?v=0.9.13"><picture><source media="(prefers-color-scheme: light)" srcset="https://oar.sh/assets/shots/pagewright-cross-relay.light.png?v=0.9.13"><img src="https://oar.sh/assets/shots/pagewright-cross-relay.png?v=0.9.13" alt="A session watching two sessions on a paired relay: a reply arrives as a relay-event row, and after a short outage the events the other relay kept arrive as one replayed row" width="720"></picture></a></p>
192
192
 
193
193
  How pairing, permissions and limits work is in [Agents working together](docs/agents-together.md).
194
194
 
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@oar-sh/oar",
3
- "version": "0.9.12",
3
+ "version": "0.9.13",
4
4
  "type": "module",
5
5
  "description": "OAR — Open Agent Relay: drive your local coding agents from any browser",
6
6
  "license": "MIT",
package/server/README.md CHANGED
@@ -216,7 +216,8 @@ reports both the actual suspend and such a drop. For the first 45 seconds after
216
216
  a relay start the check lists one more blocker, "the relay just started,
217
217
  sessions are still reporting": the workers that outlived the previous runtime
218
218
  may not have reported their background work yet. A queued **🌄 Restart web relay**
219
- gets the same banner and can be cancelled from it too.
219
+ gets a banner of its own, with the sessions it waits for, **Restart now** and
220
+ **Cancel restart** (see [Manual relay shutdown / self-restart](#manual-relay-shutdown--self-restart)).
220
221
 
221
222
  API: `GET /api/host/suspend` (state plus current blockers), `POST /api/host/suspend`,
222
223
  `POST /api/host/suspend/cancel`, and `POST /api/relay/shutdown/cancel`
@@ -254,6 +255,47 @@ Behavior notes:
254
255
  - In extension-managed mode, the relay still switches model per message and
255
256
  reports the active model used in the response.
256
257
 
258
+ ### A message without a model, an effort or a mode
259
+
260
+ The composer always sends all three. A client that leaves one out of
261
+ `POST /api/message` (a script, an agent on this relay) gets the conversation's
262
+ own, never the relay-wide default:
263
+
264
+ - **Model**, for an existing Copilot conversation: the conversation's saved
265
+ `preferred_model`, then the runtime session's `provider_model`, then its
266
+ `model`, then the model of the latest user message (`auto` skipped in each);
267
+ only a conversation with none of these runs on the catalog's current model.
268
+ A saved model the catalog no longer lists is refused with
269
+ `409 CONVERSATION_MODEL_UNAVAILABLE` (`model`, `supportedModels`) instead of
270
+ being replaced; the composer shows that text in its banner. An explicit
271
+ `auto` is unchanged (new conversations only). Claude, Cursor, Grok, Claude
272
+ Cloud and OpenAI conversations keep their pinned `provider_model`, as before.
273
+ - **Effort**: for Copilot, an effort the model's variant id carries, else the
274
+ conversation's `preferred_reasoning_effort`, clamped to the nearest level the
275
+ model offers (`nearestSupportedReasoningEffort`, the composer's rule); with
276
+ nothing to keep, the model's lowest level that is not `none`, so `none` is
277
+ chosen only when the conversation asked for it or the model has no other
278
+ level. OpenAI, Claude, Cursor and Grok conversations take the conversation's
279
+ effort the same way; with no effort stored, or one that neither matches nor
280
+ clamps (an `ultracode` on a model without it), they keep the provider's own
281
+ default as before (`none`, which for Claude, Cursor and Grok means the
282
+ model's default). A Claude Cloud conversation always runs with `none`: the
283
+ cloud takes no effort. A board action's follow-up takes the conversation's
284
+ effort as well.
285
+ - **Mode**: a send that names no `relayMode`/`mode` runs in the
286
+ conversation's saved `preferred_relay_mode`, not the relay default.
287
+ - **Preferences**: a send to an existing conversation saves only what it named.
288
+ A model it named is saved (with its effort only when that was named too, else
289
+ the stored effort stays); an effort alone is saved against the stored model,
290
+ and a conversation with no saved model runs with that effort but saves
291
+ nothing; a send that named neither writes nothing. The mode saved with them
292
+ is the one the turn runs in, so a send without a mode leaves the stored mode
293
+ as it is. A send from another relay's agent never writes them, as before.
294
+
295
+ A text repeat inside the duplicate window answers `200` with `duplicate: true`
296
+ and `queued: false`, and nothing runs. A message whose turn **failed** no
297
+ longer counts: sending it again queues it.
298
+
257
299
  ### OpenAI API key
258
300
 
259
301
  Open **Settings → OpenAI API key** to save an OpenAI key and exact model ID
@@ -375,10 +417,14 @@ cached in the worker after the server accepts it, so a failed persist is retried
375
417
  The worker keeps one persistent CLI process per conversation, so background tasks survive the reply
376
418
  that started them. The live task set is posted to `POST /api/background-tasks` (REPLACE semantics)
377
419
  and broadcast as the `background_tasks` socket event; the composer's task panel renders it and can
378
- stop an individual task over a `worker.control` push.
420
+ stop an individual task over a `worker.control` push. The conversation payload carries the set too
421
+ (`backgroundTasks`, `endedBackgroundTasks`); the page's live poll and refresh leave it out when a
422
+ `background_tasks` event for the conversation arrived while their request was on its way, as the
423
+ event's set is at least as new.
379
424
 
380
425
  The relay holds the task sets in memory (`services/background-task-store.mjs`), so a relay restart
381
- starts without them while the workers and their tasks keep running. The worker is the source of
426
+ starts without them while the workers and their tasks keep running (the console rows are stored;
427
+ see below). The worker is the source of
382
428
  truth, and the relay asks:
383
429
 
384
430
  - **Digest and resync.** A worker's heartbeat carries `backgroundTasks: { count, digest }`, a digest
@@ -407,6 +453,43 @@ truth, and the relay asks:
407
453
  - **Ends.** The Claude worker keeps a settle notice whose post failed (the newest 20, for 10
408
454
  minutes) and sends it again after the next heartbeat the relay answers; such a post is given up
409
455
  after 10 s without an answer.
456
+ - **The console rows.** What the task console needs of a shell or Monitor task (its `outputPath`
457
+ and `outputKind`, the ended row with status, exit code and `endedAt`, how a task ended when the
458
+ settle came before the set dropped it) is kept in `background_task_rows` (migration 0018,
459
+ `services/background-task-persistence.mjs`), written only when that part of a set changes, not on
460
+ every progress post. At boot the store restores the ended rows still inside their hour, and holds
461
+ the live ones (`restored`): the output route and the console's live room find a held task (the
462
+ route answers it with `status: 'held'`, `held: true` and `stoppable: false`; the console says it
463
+ waits for its worker), but `get()` and with it the panel, the session state and the suspend check
464
+ do not count it as running, and the stop route only pushes the stop to the worker. The worker's next set settles it (a task in the set is live again, with the
465
+ path it had; one not in it becomes an ended row, with the status the runtime before was told), a
466
+ settle sent again for it ends it, and while a key has held tasks its heartbeat digest never
467
+ counts as in step, so the relay asks for the set. Tasks of a worker that sends no set within twice
468
+ the boot reporting window (at least 15 s; 90 s by default) are ended with status `unknown` and the
469
+ pages get the `background_tasks` event (`endUnconfirmed`); a later set that lists one brings it
470
+ back. A held task's end time is when the runtime before last knew it running: a stored live row's
471
+ `updated_at` is written with the set and confirmed at most every 30 minutes while the worker's
472
+ digest agrees, and a live row not confirmed for 6 hours (a worker that went away in an earlier
473
+ runtime without an empty set) is deleted at boot instead of restored. Health readings are not
474
+ stored: the poller reads the files again within 10 s, the ended rows once. A runner's counts come
475
+ from the last 64 KB when its closing summary is there. A live log that outgrew that window is read
476
+ whole by a per-task runner scan (`services/task-output-service.mjs`): it looks for a runner's
477
+ header (a test run after a long install or build prints its header once) and counts from there,
478
+ reads on from where it stopped as the file grows, and starts over on a new file under the path
479
+ (shorter, another inode, an older mtime). The scans live in memory and share 4 MB per tick, each
480
+ tick starting at another task, so after a restart a long log is caught up over a few ticks (the
481
+ window's reading stands until then) and one busy log cannot keep the others waiting. An ended row
482
+ never starts a scan, so the rows a restart brings back are read in their window only. What a
483
+ worker says about a task (description, summary, last tool call, and a shell row's `command`, which
484
+ the Claude worker takes from the Bash or Monitor call that started the task, Copilot from the
485
+ shell tool's arguments or the task registry, Grok from the terminal) goes through `cleanTaskText`
486
+ on the publish and settle routes: control codes and colours out, credentials scrubbed as in the
487
+ output, then cut (a command at 2,000 characters; a Grok description is its command line). It is
488
+ stored with the console rows, so a held or ended row keeps it; share links carry no task rows. The
489
+ `none` answers of the output route use the page's texts (`TASK_OUTPUT_NONE_MESSAGES` in
490
+ `public/app/task-console-render.mjs`). Rows go with their hour, the 20-per-key limit, a set that
491
+ drops them or the conversation's delete. Without the table (a failed migration) the store works in
492
+ memory as before.
410
493
  - **Run rows left `running`.** A background sub-agent's row whose end was never written would count
411
494
  as running work for a day. For a Claude worker that has reported its task set in this runtime, a
412
495
  `running` background row that the set does not list and that nothing has updated for 15 minutes
@@ -773,10 +856,22 @@ be on, as for every local action; a paired relay: see [Across paired relays](#ac
773
856
  **Events** (`shared/relay-events.mjs`): `question_asked` (prompt, options, question id; an approval
774
857
  card is marked as one only the user can answer), `error` (`turn_failed`, `usage_limit`,
775
858
  `worker_died`), `turn_end` (status `done` or `stopped`, reply preview), `task_finished` /
776
- `task_failed` (background sub-agents and shell tasks: kind, status, exit code where known),
777
- `stalled` (no activity for `stalled_minutes`, default 15, while a turn or task runs and no
778
- question waits for an answer), `all_idle`
779
- (nothing runs in the watched set any more). `task_started` only when named in `events`. With
859
+ `task_failed` (background sub-agents and shell tasks: kind, status, exit code where known; a
860
+ non-zero exit is `task_failed`), `stalled` (no activity for `stalled_minutes`, default 15, while a
861
+ turn or task runs and no question waits for an answer), `all_idle`
862
+ (nothing runs in the watched set any more). `task_started` only when named in `events`.
863
+ `subagent_finished` is no event of its own but a narrower `task_finished` a subscription can name
864
+ (`RELAY_EVENT_SUBSCRIBABLE_TYPES`): the end of a task that is not a shell command, delivered as a
865
+ `task_finished` line. The default (`events` left out) is every type but `task_started` and
866
+ `task_finished`, with `subagent_finished`: a shell command that ended well wakes only a watcher
867
+ that names `task_finished`, which takes every task end. A busy worker ends a shell command every
868
+ minute or so, and each was a wake-up, a model turn on the watcher's whole context. A subscription
869
+ whose stored list is exactly the old default (every type but `task_started`, in any order) named no
870
+ events and is read as the new default (`effectiveRelayEventTypes`, at read time, the row is never
871
+ rewritten, so an older relay reading the same database sees what it stored); any other list,
872
+ including the old default plus more, is the watcher's choice and stays. A subscribe that names
873
+ exactly the old default list stores `subagent_finished` with it, so it is never read as the default. A mirror shows the paired
874
+ relay's list as that relay returned it. With
780
875
  `heartbeat_minutes` a `heartbeat` event comes at that interval while the watched set has work
781
876
  running, none while it is idle. `match` (a case-insensitive regular expression) filters `turn_end`
782
877
  and task events by their text; other events pass. `once` ends a subscription after its first event,
@@ -900,8 +995,10 @@ turn processing, or queued and not held by a usage-limit pause; `background_only
900
995
  `completed` | `failed` | `stopped`, `exitCode`, `summary`, `endedAt`) and `lastActivityAt`.
901
996
  `remote_relay`'s `list_sessions` and `read_session` pass them on as `state`, `tasks` and
902
997
  `lastActivityAt`; `active` stays and is true for background work too. `read_session` adds to each
903
- task its `taskId`, `output` (whether it has output to read) and, for a task with an output file,
904
- `health` (`minutesSinceOutput`, `quiet`, `quietAfterMinutes`, test-run `counts`);
998
+ task its `taskId`, `output` (whether it has output to read), a shell task's `command` (its command
999
+ line where the worker reports one, scrubbed of credentials and cut at 2,000 characters by the relay
1000
+ when the worker posts it, shown to at most 500) and, for a task with an output file, `health`
1001
+ (`minutesSinceOutput`, `quiet`, `quietAfterMinutes`, test-run `counts`);
905
1002
  `read_task_output {session, task_id, lines?}` (read permission; default 200 lines, at most 500 or
906
1003
  32 KB) reads the end of that output through `GET /api/conversation/:id/background-task/:taskId/output`,
907
1004
  scrubbed of credentials.
@@ -1163,6 +1260,7 @@ After launching the server, the CLI opens a worker WebSocket link:
1163
1260
  - Identifies itself with the bound SDK session id and worker pid
1164
1261
  - Receives queued turns over `ws(s)://.../api/session-worker/ws`
1165
1262
  - Reconnects starting at 1 second, with exponential backoff capped at 8 seconds
1263
+ (see [Worker link reconnects](#worker-link-reconnects))
1166
1264
  - Falls back to `GET /api/pending` only while the worker socket is unavailable
1167
1265
  - Processes any message with full PC access (file system, commands, etc.)
1168
1266
  - Posts the response back
@@ -1177,6 +1275,33 @@ After launching the server, the CLI opens a worker WebSocket link:
1177
1275
  - Answered relay question cards stay visible in the conversation journal, including
1178
1276
  the answer you submitted.
1179
1277
 
1278
+ ### Worker link reconnects
1279
+
1280
+ Every session worker (Copilot SDK, Claude, Claude Cloud, Cursor, Grok) and the Copilot CLI extension
1281
+ use the same link, `shared/worker-runtime/worker-websocket-link.mjs`. Its reconnect rules:
1282
+
1283
+ - A connection counts as made at the relay's first `server.hello`, which the relay sends as soon as it
1284
+ accepts the socket. A socket still connecting, or open without that hello, after 15 seconds
1285
+ (`connectTimeoutMs`) is given up and retried: a relay hung in its shutdown can hold a socket in
1286
+ either state without ever ending it.
1287
+ - The link ends a socket on whichever comes first of an error, a close, that deadline, a stale
1288
+ connection (30 seconds without a server frame) and the relay's `server.draining`, and schedules
1289
+ the next attempt from there; later events from the same socket are ignored. It never waits for a
1290
+ close event, which Node 22 does not fire for a socket closed while connecting, and a socket whose
1291
+ constructor throws is retried too.
1292
+ - There is at most one pending reconnect and one socket at a time. The delay starts at 1 second and
1293
+ doubles up to 8 seconds, plus up to 250 ms of jitter; it starts over only after a completed
1294
+ handshake, so a relay that accepts and drops the socket is not retried every second.
1295
+ - Each failed attempt logs one `worker ws disconnected <reason> attempt=<n> retryInMs=<ms>` line in
1296
+ the worker log for the first three, then at most one a minute, with `notLogged=<n>` counting the
1297
+ ones in between.
1298
+ - `stop()` (worker shutdown) clears the pending reconnect and the deadline and closes the socket
1299
+ without scheduling another attempt.
1300
+
1301
+ The relay's detached-worker watch (`services/detached-worker-watch-service.mjs`) is the safety net
1302
+ behind this: it replaces a worker that has had no socket for 60 seconds while a message waits, but
1303
+ never while a turn or background work of that session runs, so the link has to reconnect by itself.
1304
+
1180
1305
  ### Session activation behavior (important)
1181
1306
 
1182
1307
  In extension-managed mode, the web server can already be running while the CLI is still
@@ -1290,6 +1415,9 @@ Queue metrics include `parkedCount` for turns deferred behind restart/rebind gat
1290
1415
  | POST | `/api/relay/pause` | Pause dequeueing and drop currently queued messages |
1291
1416
  | POST | `/api/relay/resume` | Resume dequeueing after pause |
1292
1417
  | POST | `/api/relay/shutdown` | Queue an authenticated relay shutdown or self-restart |
1418
+ | GET | `/api/relay/shutdown` | The queued request's state (as `relay_shutdown_state`), plus `preview`: what a restart asked now would wait for and leave running (`?action=shutdown` for a shutdown). See [Manual relay shutdown / self-restart](#manual-relay-shutdown--self-restart) |
1419
+ | POST | `/api/relay/shutdown/keep-waiting` | Answer to the max-wait question: the queued request waits another full period, counted from now → `{ ok, extended, …state }` |
1420
+ | POST | `/api/relay/shutdown/cancel` | Withdraw a queued (not yet started) shutdown or restart; held messages go out at once |
1293
1421
  | GET | `/api/relay-questions` | (CLI/UI) List relay questions by `status` (for example `pending` or `answered`) |
1294
1422
  | GET | `/api/relay-question/:id` | (CLI) Fetch a single relay question |
1295
1423
  | POST | `/api/relay-question` | (CLI) Create a relay question for the browser |
@@ -1314,6 +1442,8 @@ Queue metrics include `parkedCount` for turns deferred behind restart/rebind gat
1314
1442
  | POST | `/api/settings/cursor` | Set/remove the Cursor API key, default model, or enabled model subset; key changes reset discovered models and effort tiers |
1315
1443
  | GET | `/api/settings/turn-ceiling` | Read the max turn duration plus slider bounds (`minMinutes`, `maxMinutes`, `stepMinutes`, `defaultMinutes`) |
1316
1444
  | POST | `/api/settings/turn-ceiling` | Set the max turn duration in minutes (`0` = no limit) |
1445
+ | GET | `/api/settings/relay-restart-max-wait` | How long a queued relay restart waits before it asks: `{ maxWaitMinutes, choices: [5, 10, 15, 30, 60, 120, 240, 0], defaultMinutes: 30, overrideMs }`; `0` = no limit; `overrideMs` is `OAR_RELAY_RESTART_MAX_WAIT_MS` when set |
1446
+ | POST | `/api/settings/relay-restart-max-wait` | `{ maxWaitMinutes }` → the GET body; anything but one of `choices` is `400`. A queued request measures its current period against the new value at once |
1317
1447
  | GET | `/api/settings/subagent-record-retention` | How long sub-agent records keep tool inputs and output: `{ retentionDays, choices: [0, 7, 14, 30, 90], defaultDays: 90 }`; `0` = keep for good |
1318
1448
  | POST | `/api/settings/subagent-record-retention` | `{ retentionDays }` → the GET body; anything but one of `choices` is `400`. Runs a retention sweep 5 s later |
1319
1449
  | POST | `/api/claude-native-session` | (Claude worker) Persist the native Agent SDK session id for resume |
@@ -1346,7 +1476,7 @@ Queue metrics include `parkedCount` for turns deferred behind restart/rebind gat
1346
1476
  | POST | `/api/remote-relays/:id/check` | Probe a paired relay now |
1347
1477
  | POST | `/api/remote-relays/pair` | (Relay to relay) Another relay introduces itself after the user paired it there |
1348
1478
  | GET | `/api/remote-relays/summary` | (Worker) `{ count, online, localEnabled }`: the number of paired relays, how many of them are online, and whether agent sessions are switched on. Workers register `remote_relay` when `count` is above zero or `localEnabled` is `true` |
1349
- | POST | `/api/remote-relays/tool` | (Worker) One `remote_relay` tool call: `{ conversationId, action, args }`. For a paired relay the relay checks the mention unlock, the permission, the hop limit and (ask/plan mode) the user's approval, then forwards the call to the remote. For this relay itself (named by its name or `this`; refused with `REMOTE_RELAY_LOCAL_DISABLED` while agent sessions are off) it checks instead that the call does not aim at the caller's own conversation (`REMOTE_RELAY_OWN_SESSION`) and, for `create_session`, that the caller was not created by an agent (`REMOTE_RELAY_NESTED_SESSION`), that fewer than 4 sessions it created have a turn queued or running (`REMOTE_RELAY_SESSION_LIMIT`) and that the user allowed it once for the conversation; the call then runs against this relay's own routes in-process. `wait_seconds` is cut to the `maxWaitSeconds` setting for every target |
1479
+ | POST | `/api/remote-relays/tool` | (Worker) One `remote_relay` tool call: `{ conversationId, action, args }`. For a paired relay the relay checks the mention unlock, the permission, the hop limit and (ask/plan mode) the user's approval, then forwards the call to the remote. For this relay itself (named by its name or `this`; refused with `REMOTE_RELAY_LOCAL_DISABLED` while agent sessions are off) it checks instead that the call does not aim at the caller's own conversation (`REMOTE_RELAY_OWN_SESSION`) and, for `create_session`, that the caller was not created by an agent (`REMOTE_RELAY_NESTED_SESSION`), that fewer than 4 sessions it created have a turn queued or running (`REMOTE_RELAY_SESSION_LIMIT`) and that the user allowed it once for the conversation; the call then runs against this relay's own routes in-process. A `model` in `create_session` or `send` is checked first, before any approval card, against the list New Chat offers for the provider on the target (`/api/models` for Copilot and OpenAI, the provider settings routes for Claude, Cursor, Grok and Claude Cloud): one it does not list is refused with `REMOTE_RELAY_UNKNOWN_MODEL`, `suggestions` (the close matches) and `models`, and nothing is created or sent. Casing follows the list; a Claude alias (`opus`, `sonnet`, `haiku`, with an optional `[1m]`) resolves to the newest listed model of its family by version number (a date suffix ignored, the undated id first, `[1m]` ids only when the alias asks and they are listed); a `[1m]` id with only its base listed runs as the base, with a note; a Copilot id with its effort (`gpt-5.4-high`) goes to the bootstrap and the first prompt as the listed id plus `reasoningEffort` (an explicit `effort` wins, and a note says so), but only when the catalogue lists that effort for the id (`reasoningByModel`): `gpt-5.1-codex-max` with only `gpt-5.1-codex` listed is unknown, with the base suggested; `auto` passes. `send` also takes the provider's whole discovered list (`availableModels`) and the session's own model, as the message route does. Without a list (discovery has not run, the route did not answer; `auto` alone is no list) the model goes on unchecked and `modelNote` says so, as it does when the bootstrap binds another model than the one asked. A provider not set up there answers `REMOTE_RELAY_PROVIDER_UNAVAILABLE` before its model is looked at. The bootstrap route itself checks a Copilot model from an agent against Copilot's own models: an unknown one is refused (`COPILOT_MODEL_UNAVAILABLE`, with `supportedModels` and `suggestedModels`; another provider's model gets that provider's answer), a listed one is bound in the list's casing, an effort-suffixed one as the listed id plus that effort when `reasoningByModel` lists it; the browser's New Chat keeps its fallback to the current model. `wait_seconds` is cut to the `maxWaitSeconds` setting for every target |
1350
1480
  | GET | `/api/relay-events/subscriptions` | Every active [relay event subscription](#relay-event-subscriptions) (`{ available, subscriptions }`); refused to other relays' agents |
1351
1481
  | DELETE | `/api/relay-events/subscriptions/:id` | The user's Unsubscribe (a paired relay's subscription too: it learns with its next fetch); refused to other relays' agents |
1352
1482
  | POST | `/api/relay-events/remote/subscribe` | (Relay to relay) A paired relay's agent subscribes to sessions here: `{ watcher: { conversationId, title }, subscriber: { name }, scope, sessions, events?, once?, match?, stalled_minutes?, heartbeat_minutes?, dry_run? }` → `{ ok, subscription }`. Another relay's request only, inbound switch on; see [Across paired relays](#across-paired-relays) |
@@ -1365,6 +1495,7 @@ Queue metrics include `parkedCount` for turns deferred behind restart/rebind gat
1365
1495
  - `reason` (optional string)
1366
1496
  - `requestedBy` (optional string)
1367
1497
  - `restart` (optional boolean-ish flag; `true` queues self-restart instead of plain shutdown)
1498
+ - `immediate` (optional boolean-ish flag; `true` does not wait for the running turns)
1368
1499
  - Example restart request:
1369
1500
 
1370
1501
  ```json
@@ -1381,11 +1512,48 @@ Queue metrics include `parkedCount` for turns deferred behind restart/rebind gat
1381
1512
  - `restart`: boolean mirror of the action
1382
1513
  - `requestedAt`, `reason`, `requestedBy`
1383
1514
  - `queue`: current `pendingCount` / `processingCount` / `parkedCount`
1384
- - The relay waits until no turn is processing before acting; this API is not an interrupt or cancel mechanism. While the request is queued no pending message is handed to a worker (a turn started meanwhile would postpone the restart again); pending and parked rows stay in the database and run after the restart.
1515
+ - `waitingFor`, `held`, `backgroundTasks`, `survival`, `maxWaitMs`, `deadlineAt`, `immediate` (below)
1516
+ - The relay waits until no turn is processing before acting. While the request is queued no pending message is handed to a worker (a turn started meanwhile would postpone the restart again); pending and parked rows stay in the database and run after the restart.
1517
+ - `immediate: true` (boolean-ish, like `restart`) starts it at once, running turns or not: the banner's **Restart now**. The answer still names what it did not wait for.
1518
+ - Maximum wait (`services/relay-restart-queue.mjs`): after **Ask after a queued relay restart has waited** (`relay_restart_max_wait_minutes`, 30 by default, 5–240 or `0` = no limit; `OAR_RELAY_RESTART_MAX_WAIT_MS` overrides it in ms, for test relays) the request stays queued and sets `question: { askedAt }`; the relay pushes one notification (titled *Relay restart waiting*; switch **Queued relay restart waited its maximum time**, `relayRestart`) and never restarts while turns run; it still goes ahead as soon as the last turn ends, question or not. A raised maximum (or none) that moves the deadline into the future withdraws the question; `/keep-waiting` without a question does nothing (`extended: false`). An `immediate` request may name the queued request it was shown (`expectRequestedAt`, its `requestedAt`); when that request is gone the answer is `accepted: false, stale: true` and nothing starts. Agents are told never to send `immediate` (`relay-tools.md`). The user answers with `immediate`, `/keep-waiting` (another full period from now; counted in `keptWaiting`) or `/cancel`. With no limit the request, and the messages it holds, wait until the turns end or someone decides.
1519
+ - What it waits for, holds and leaves running (`services/relay-restart-picture.mjs`), in the answer, in `GET /api/relay/shutdown`, in `/api/status` → `relayShutdown` and in the `relay_shutdown_state` socket event (sent on every change and on connect):
1520
+ - `waitingFor`: `[{ conversationId, title, turns }]`, the sessions with a processing turn
1521
+ - `held`: `{ count, conversations: [{ conversationId, title, count }] }`, pending and parked messages
1522
+ - `backgroundTasks`: `[{ conversationId, title, count, detail, survives, why }]`, sessions with running background work of a live worker (the task panel's sets and running sub-agent runs, through `background-work-view.mjs`), then sessions with console tasks held since the last restart (`background-task-store.mjs` `listRestored`), which no worker has confirmed yet: `survives: null`
1523
+ - `survival`: `{ action, workersSurvive, summary }`. Workers are separate processes (tmux, detached, a console window) and outlive a restart, which the relay's supervisor or the CLI extension takes; they outlive a shutdown too, except under a systemd service (detected from `INVOCATION_ID` and a `.service` cgroup), whose stop may take the workers with it (`survives: false`, shown as "may stop"; a worker in a tmux session the service did not start survives, which the relay cannot tell). A Claude Cloud session runs at Anthropic and always survives
1524
+ - while queued also `holdsNewTurns`, `maxWaitMs`, `waitingSince` (start of the current period), `deadlineAt` (`null` without a limit), `question`, `keptWaiting`
1385
1525
  - Ownership after an intentional restart follows the argv role, so exactly one supervisor acts on exit 75:
1386
1526
  - `server.js --supervised` exits 75 and relaunches under `.github/extensions/web-relay/server-lifecycle/managed-server.mjs`
1387
1527
  - bare `node server/server.js` keeps its supervisor attached, respawns the `--relay-runtime` child, and keeps the same terminal session alive
1388
1528
 
1529
+ ### Interrupted turns after a restart
1530
+
1531
+ `services/restart-resume-service.mjs` implements **After a relay restart, tell interrupted sessions
1532
+ to carry on** (`app_settings` `restart_resume_notes`, `'1'` on, off when absent; `GET/POST
1533
+ /api/settings/restart-resume` `{ enabled }`, a boolean, 400 otherwise). At boot it notes the queue
1534
+ rows that are `processing` (the turns the runtime before left) and follows them through the
1535
+ recovery paths, which report each outcome (`noteRestartTurnOutcome`):
1536
+
1537
+ | Path | `cut` (cannot run again) | `requeued` | `failed` |
1538
+ | --- | --- | --- | --- |
1539
+ | stale sweep (`recoverProcessingOlderThan`) | stale continuation dropped; consumed row failed | requeued | over the retry budget |
1540
+ | dead worker (`recoverDeadWorkerProcessingRows`) | continuation dropped; consumed row failed | requeued | over the retry budget |
1541
+ | owner heartbeat idle (`recoverOwnedProcessingRowsForSession`) | continuation dropped; consumed row failed; a started turn failed as `turn-replay-blocked` | requeued | |
1542
+
1543
+ A heartbeat that lists a row (`activeQueueMessageId(s)`) claims it: its worker lived through the
1544
+ restart and has the turn, so whatever ends it later is not put down to the restart, and the
1545
+ session gets no note. A row that leaves `processing` another way is read from the queue: back in `pending` or `parked` is
1546
+ `requeued`, anything else (a reply from a worker that lived through the restart) is settled. Once
1547
+ none of a conversation's rows of the boot is processing, it gets a note when one was `cut` and none
1548
+ was `requeued`, `claimed` or `failed`; the stale sweep's 15 s tick decides the ones that settled on their own,
1549
+ and the watch ends 30 minutes after the boot. The note is a message of the relay's (role `user`,
1550
+ `kind = 'relay-restart-resume'`) with a pending queue row on the interrupted turn's model, mode and
1551
+ owner, queued at the time it is written (behind a message the user queued meanwhile), as the
1552
+ usage-limit follow-up is; the primer then starts the worker. No note when the setting is off at
1553
+ that moment, for a deleted or archived conversation, while the conversation holds a usage-limit
1554
+ follow-up, or a second time in the same runtime. The browser renders it as a compact relay row
1555
+ (`relay-restart-note-view.mjs`).
1556
+
1389
1557
  ### Upload storage model
1390
1558
 
1391
1559
  - Physical blobs are stored in `server/uploads/<sha256>` (content-addressed).
@@ -1951,6 +2119,13 @@ any length. A conversation with a row whose timestamp is not the relay's ISO for
1951
2119
  (`YYYY-MM-DDTHH:MM:SS.mmmZ`) or whose id is not lowercase letters, digits and dashes is paged the
1952
2120
  old way: every row loaded, the page cut afterwards.
1953
2121
 
2122
+ `0018-background-task-rows.mjs` adds `background_task_rows` (`store_key`, `task_id`, `state`
2123
+ `live` | `ended`, `row_json`, `ended_at`, `updated_at`; one row per key and task): the task
2124
+ console's rows across a relay restart (see
2125
+ [Background tasks and workflow progress](#background-tasks-and-workflow-progress)). Additive and
2126
+ idempotent; a failure is logged and retried on the next boot, and the store keeps its rows in
2127
+ memory meanwhile. An older build ignores the table.
2128
+
1954
2129
  `0007-claude-cloud.mjs` adds the storage of [Claude Cloud](#claude-cloud-provider) conversations.
1955
2130
  It is additive and idempotent, runs on every boot, and a failure is logged and retried on the next
1956
2131
  one: