@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.
- package/CHANGELOG.md +206 -0
- package/README.md +9 -9
- package/package.json +1 -1
- package/server/README.md +186 -11
- package/server/claude-worker/claude-session-process.mjs +53 -2
- package/server/claude-worker/task-output-discovery.mjs +28 -2
- package/server/copilot-worker/copilot-continuation-signals.mjs +11 -5
- package/server/copilot-worker/copilot-sdk-session-process.mjs +10 -0
- package/server/db-schema.mjs +10 -0
- package/server/grok-worker/acp-host-services.mjs +6 -3
- package/server/grok-worker/grok-background-tasks.mjs +7 -0
- package/server/migrations/0018-background-task-rows.mjs +101 -0
- package/server/public/app/action-confirmations.js +12 -15
- package/server/public/app/api-client.js +57 -3
- package/server/public/app/background-tasks-view.mjs +56 -7
- package/server/public/app/bootstrap.js +7 -0
- package/server/public/app/conversation-preferences.mjs +10 -4
- package/server/public/app/conversation-view.js +33 -9
- package/server/public/app/host-suspend-ui.js +45 -44
- package/server/public/app/journal-view.js +9 -3
- package/server/public/app/push-settings.js +1 -0
- package/server/public/app/relay-prompt-sanitizer.mjs +9 -2
- package/server/public/app/relay-restart-note-view.mjs +36 -0
- package/server/public/app/relay-restart-ui.js +568 -0
- package/server/public/app/settings-modal.js +52 -0
- package/server/public/app/socket-handlers.js +1 -1
- package/server/public/app/task-console-render.mjs +171 -7
- package/server/public/app/task-console.mjs +111 -11
- package/server/public/app/task-output-text.mjs +15 -6
- package/server/public/app/transcript-jump-bottom-view.mjs +5 -1
- package/server/public/app/transcript-jump-bottom.mjs +20 -0
- package/server/public/index.html +67 -0
- package/server/relay-tools.md +5 -0
- package/server/repositories/message-repository.mjs +3 -0
- package/server/repositories/remote-relay-repository.mjs +4 -2
- package/server/routes/messages-routes.mjs +155 -27
- package/server/routes/relay-board-routes.mjs +19 -2
- package/server/routes/relay-restart-routes.mjs +73 -0
- package/server/routes/sessions-routes.mjs +75 -3
- package/server/server-runtime.mjs +192 -137
- package/server/services/about-service.mjs +13 -12
- package/server/services/background-task-persistence.mjs +133 -0
- package/server/services/background-task-store.mjs +240 -13
- package/server/services/conversation-model-fallback.mjs +57 -0
- package/server/services/host-suspend-command.mjs +2 -1
- package/server/services/preview-proxy-server.mjs +14 -1
- package/server/services/process-list-service.mjs +2 -1
- package/server/services/provider-model-selection.mjs +195 -0
- package/server/services/push-dispatch-service.mjs +9 -2
- package/server/services/relay-event-service.mjs +27 -9
- package/server/services/relay-prompt-sanitizer.mjs +7 -0
- package/server/services/relay-restart-picture.mjs +209 -0
- package/server/services/relay-restart-queue.mjs +380 -0
- package/server/services/relay-shutdown-gate.mjs +4 -1
- package/server/services/remote-relay-dispatcher.mjs +187 -19
- package/server/services/restart-resume-service.mjs +303 -0
- package/server/services/session-worker-process-service.mjs +6 -4
- package/server/services/task-output-service.mjs +217 -18
- package/shared/relay-events.mjs +55 -2
- package/shared/remote-relay-contract.mjs +15 -8
- package/shared/task-output-text.mjs +196 -83
- 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.
|
|
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.
|
|
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.
|
|
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.
|
|
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.
|
|
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.
|
|
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.
|
|
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.
|
|
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.
|
|
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
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
|
|
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
|
|
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
|
|
778
|
-
question waits for an answer), `all_idle`
|
|
779
|
-
(nothing runs in the watched set any more). `task_started` only when named in `events`.
|
|
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)
|
|
904
|
-
|
|
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
|
-
-
|
|
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:
|