@oar-sh/oar 0.9.11 → 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 +332 -0
- package/README.md +10 -10
- package/package.json +1 -1
- package/server/README.md +201 -12
- package/server/claude-worker/claude-remote-relay-tool.mjs +2 -0
- package/server/claude-worker/claude-sdk-adapter.mjs +57 -4
- package/server/claude-worker/claude-session-process.mjs +179 -0
- package/server/claude-worker/claude-session-worker.mjs +17 -0
- package/server/claude-worker/task-output-discovery.mjs +152 -0
- package/server/copilot-worker/copilot-continuation-signals.mjs +11 -5
- package/server/copilot-worker/copilot-sdk-session-process.mjs +169 -3
- package/server/copilot-worker/copilot-sdk-session-worker.mjs +2 -0
- package/server/copilot-worker/copilot-shell-output-mirror.mjs +94 -0
- package/server/copilot-worker/copilot-task-settle.mjs +96 -0
- package/server/db-schema.mjs +19 -0
- package/server/grok-worker/acp-host-services.mjs +48 -6
- package/server/grok-worker/grok-background-tasks.mjs +248 -0
- package/server/grok-worker/grok-sdk-adapter.mjs +3 -1
- package/server/grok-worker/grok-session-worker.mjs +33 -1
- package/server/grok-worker/grok-turn-runner.mjs +11 -0
- package/server/migrations/0017-conversation-history-indexes.mjs +89 -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 +70 -3
- package/server/public/app/background-tasks-view.mjs +334 -16
- package/server/public/app/bootstrap.js +102 -5
- package/server/public/app/claude-background-command-limit-options.mjs +21 -0
- package/server/public/app/conversation-payload-cache.mjs +357 -0
- package/server/public/app/conversation-preferences.mjs +10 -4
- package/server/public/app/conversation-turn-state.mjs +1 -1
- package/server/public/app/conversation-view.js +462 -8
- package/server/public/app/conversation-window-retention.mjs +96 -0
- package/server/public/app/host-suspend-ui.js +45 -44
- package/server/public/app/journal-view.js +259 -67
- package/server/public/app/push-settings.js +1 -0
- package/server/public/app/relay-prompt-sanitizer.mjs +20 -6
- 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 +162 -2
- package/server/public/app/socket-handlers.js +59 -7
- package/server/public/app/store.js +25 -0
- package/server/public/app/subagent-viewer.mjs +7 -1
- package/server/public/app/task-console-render.mjs +401 -0
- package/server/public/app/task-console-watch.mjs +42 -0
- package/server/public/app/task-console.mjs +730 -0
- package/server/public/app/task-output-text.mjs +243 -0
- package/server/public/app/transcript-island.mjs +73 -0
- package/server/public/app/transcript-jump-bottom-view.mjs +42 -8
- package/server/public/app/transcript-jump-bottom.mjs +68 -11
- package/server/public/app/transcript-reading-anchor.mjs +256 -0
- package/server/public/app/transcript-reload-freshness.mjs +17 -1
- package/server/public/app/transcript-viewport-anchor.mjs +20 -1
- package/server/public/index.html +306 -6
- package/server/relay-tools.md +6 -0
- package/server/repositories/message-repository.mjs +11 -0
- package/server/repositories/question-repository.mjs +4 -0
- package/server/repositories/remote-relay-repository.mjs +4 -2
- package/server/routes/messages-routes.mjs +397 -23
- package/server/routes/relay-board-routes.mjs +19 -2
- package/server/routes/relay-restart-routes.mjs +73 -0
- package/server/routes/sessions-routes.mjs +208 -66
- package/server/server-runtime.mjs +269 -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 +470 -6
- package/server/services/cli-process-runner.mjs +4 -21
- package/server/services/conversation-history-page.mjs +199 -0
- 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 +18 -4
- 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 +316 -20
- package/server/services/restart-resume-service.mjs +303 -0
- package/server/services/session-activity-tracker.mjs +6 -2
- package/server/services/session-worker-process-service.mjs +6 -4
- package/server/services/task-output-rooms.mjs +259 -0
- package/server/services/task-output-service.mjs +761 -0
- package/shared/claude-background-command-limit.mjs +83 -0
- package/shared/relay-events.mjs +55 -2
- package/shared/remote-relay-contract.mjs +47 -10
- package/shared/task-output-text.mjs +850 -0
- package/shared/worker-runtime/task-output-spool.mjs +181 -0
- package/shared/worker-runtime/worker-websocket-link.mjs +96 -20
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,
|
|
@@ -899,7 +994,14 @@ turn processing, or queued and not held by a usage-limit pause; `background_only
|
|
|
899
994
|
`subagent` | `shell`, `description`, `startedAt`, `lastActivityAt`, `status` `running` |
|
|
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
|
-
`lastActivityAt`; `active` stays and is true for background work too.
|
|
997
|
+
`lastActivityAt`; `active` stays and is true for background work too. `read_session` adds to each
|
|
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`);
|
|
1002
|
+
`read_task_output {session, task_id, lines?}` (read permission; default 200 lines, at most 500 or
|
|
1003
|
+
32 KB) reads the end of that output through `GET /api/conversation/:id/background-task/:taskId/output`,
|
|
1004
|
+
scrubbed of credentials.
|
|
903
1005
|
|
|
904
1006
|
### Browser
|
|
905
1007
|
|
|
@@ -1158,6 +1260,7 @@ After launching the server, the CLI opens a worker WebSocket link:
|
|
|
1158
1260
|
- Identifies itself with the bound SDK session id and worker pid
|
|
1159
1261
|
- Receives queued turns over `ws(s)://.../api/session-worker/ws`
|
|
1160
1262
|
- Reconnects starting at 1 second, with exponential backoff capped at 8 seconds
|
|
1263
|
+
(see [Worker link reconnects](#worker-link-reconnects))
|
|
1161
1264
|
- Falls back to `GET /api/pending` only while the worker socket is unavailable
|
|
1162
1265
|
- Processes any message with full PC access (file system, commands, etc.)
|
|
1163
1266
|
- Posts the response back
|
|
@@ -1172,6 +1275,33 @@ After launching the server, the CLI opens a worker WebSocket link:
|
|
|
1172
1275
|
- Answered relay question cards stay visible in the conversation journal, including
|
|
1173
1276
|
the answer you submitted.
|
|
1174
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
|
+
|
|
1175
1305
|
### Session activation behavior (important)
|
|
1176
1306
|
|
|
1177
1307
|
In extension-managed mode, the web server can already be running while the CLI is still
|
|
@@ -1285,6 +1415,9 @@ Queue metrics include `parkedCount` for turns deferred behind restart/rebind gat
|
|
|
1285
1415
|
| POST | `/api/relay/pause` | Pause dequeueing and drop currently queued messages |
|
|
1286
1416
|
| POST | `/api/relay/resume` | Resume dequeueing after pause |
|
|
1287
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 |
|
|
1288
1421
|
| GET | `/api/relay-questions` | (CLI/UI) List relay questions by `status` (for example `pending` or `answered`) |
|
|
1289
1422
|
| GET | `/api/relay-question/:id` | (CLI) Fetch a single relay question |
|
|
1290
1423
|
| POST | `/api/relay-question` | (CLI) Create a relay question for the browser |
|
|
@@ -1301,14 +1434,16 @@ Queue metrics include `parkedCount` for turns deferred behind restart/rebind gat
|
|
|
1301
1434
|
| GET | `/api/usage` | Unified plan usage: one card per configured provider (`providers[]` with `meters`, `details`, `links`) built from the live Copilot quota, the Claude worker's latest stored snapshot, and the derived Cursor cycle totals. A provider that cannot be read yields an unavailable card rather than failing the response, so the modal always renders. While Claude Cloud is on, the Claude card's windows are read live from the account, and a Claude Cloud card is added (credit meters, the relay's cloud spend, and the cost of the conversation named by `?conversationId=` when it is a cloud one); a card may carry `notes[]` (a line of text with an optional https link), and a meter `severity` and `resetKind: 'expiry'`. Legacy top-level Copilot quota fields are spread alongside `providers` for cached clients; `?legacy=1` returns the Copilot-only snapshot on its own |
|
|
1302
1435
|
| GET | `/api/settings/cursor-allowance` | Read the manual Cursor plan allowances (`cursorModelsUsd`, `otherModelsUsd`, `resetDay`) |
|
|
1303
1436
|
| POST | `/api/settings/cursor-allowance` | Set the Cursor pool allowances and billing reset day; `null` clears an allowance, and `resetAccounting: true` re-baselines the derived spend tracking |
|
|
1304
|
-
| GET | `/api/settings/claude` | Read Claude provider settings (`enabled`, `model`, `models`, `availableModels`) |
|
|
1305
|
-
| POST | `/api/settings/claude` | Enable/disable Claude, set the default model
|
|
1437
|
+
| GET | `/api/settings/claude` | Read Claude provider settings (`enabled`, `model`, `models`, `availableModels`, `attributionMode`, `backgroundCommandLimitMinutes` with the slider bounds `backgroundCommandLimitMinMinutes`, `backgroundCommandLimitMaxMinutes`, `backgroundCommandLimitStepMinutes`, `backgroundCommandLimitDefaultMinutes`) |
|
|
1438
|
+
| POST | `/api/settings/claude` | Enable/disable Claude, set the default model, the enabled model subset, the attribution mode or `backgroundCommandLimitMinutes` (clamped to 30…480 and snapped to 30-minute steps; not a number is `400`; read by Claude sessions spawned afterwards); triggers discovery and unstarted-conversation reconciliation |
|
|
1306
1439
|
| GET | `/api/settings/claude-cloud` | Read Claude Cloud settings: `{ enabled, defaultModel, environmentId, environments: [{ id, name }] \| null, environmentsError, account: { loggedIn, email, orgName, subscriptionType } \| null, token: { source, hasToken, expiresAt }, models }`. `token` says where the login is read from (`env`, `file`, `none`) and when it runs out, never the token. `environments` is `null` while the provider is off or the host has no login: nothing is asked of Anthropic then |
|
|
1307
1440
|
| POST | `/api/settings/claude-cloud` | `{ enabled?, defaultModel?, environmentId? }` → `{ ok: true, …the GET body }`. Enabling without a Claude login on the host is `400` with `code: 'claude_cloud_login_missing'`; an empty `environmentId` clears the stored one. Emits `claude_cloud_settings_updated` |
|
|
1308
1441
|
| GET | `/api/settings/cursor` | Read Cursor provider settings — same shape as Claude's: `models` is the enabled subset, `availableModels` the full discovered list, plus per-model `efforts` |
|
|
1309
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 |
|
|
1310
1443
|
| GET | `/api/settings/turn-ceiling` | Read the max turn duration plus slider bounds (`minMinutes`, `maxMinutes`, `stepMinutes`, `defaultMinutes`) |
|
|
1311
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 |
|
|
1312
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 |
|
|
1313
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 |
|
|
1314
1449
|
| POST | `/api/claude-native-session` | (Claude worker) Persist the native Agent SDK session id for resume |
|
|
@@ -1341,7 +1476,7 @@ Queue metrics include `parkedCount` for turns deferred behind restart/rebind gat
|
|
|
1341
1476
|
| POST | `/api/remote-relays/:id/check` | Probe a paired relay now |
|
|
1342
1477
|
| POST | `/api/remote-relays/pair` | (Relay to relay) Another relay introduces itself after the user paired it there |
|
|
1343
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` |
|
|
1344
|
-
| 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 |
|
|
1345
1480
|
| GET | `/api/relay-events/subscriptions` | Every active [relay event subscription](#relay-event-subscriptions) (`{ available, subscriptions }`); refused to other relays' agents |
|
|
1346
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 |
|
|
1347
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) |
|
|
@@ -1360,6 +1495,7 @@ Queue metrics include `parkedCount` for turns deferred behind restart/rebind gat
|
|
|
1360
1495
|
- `reason` (optional string)
|
|
1361
1496
|
- `requestedBy` (optional string)
|
|
1362
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)
|
|
1363
1499
|
- Example restart request:
|
|
1364
1500
|
|
|
1365
1501
|
```json
|
|
@@ -1376,11 +1512,48 @@ Queue metrics include `parkedCount` for turns deferred behind restart/rebind gat
|
|
|
1376
1512
|
- `restart`: boolean mirror of the action
|
|
1377
1513
|
- `requestedAt`, `reason`, `requestedBy`
|
|
1378
1514
|
- `queue`: current `pendingCount` / `processingCount` / `parkedCount`
|
|
1379
|
-
-
|
|
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`
|
|
1380
1525
|
- Ownership after an intentional restart follows the argv role, so exactly one supervisor acts on exit 75:
|
|
1381
1526
|
- `server.js --supervised` exits 75 and relaunches under `.github/extensions/web-relay/server-lifecycle/managed-server.mjs`
|
|
1382
1527
|
- bare `node server/server.js` keeps its supervisor attached, respawns the `--relay-runtime` child, and keeps the same terminal session alive
|
|
1383
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
|
+
|
|
1384
1557
|
### Upload storage model
|
|
1385
1558
|
|
|
1386
1559
|
- Physical blobs are stored in `server/uploads/<sha256>` (content-addressed).
|
|
@@ -1937,6 +2110,22 @@ the watched one `subscriber_relay_id`, `subscriber_relay_name`, `subscriber_watc
|
|
|
1937
2110
|
`subscriber_watcher_title`, `last_pull_at`, `dropped_count`) and `relay_event_links` (per paired
|
|
1938
2111
|
relay the fetch cursor and the link's health). See [Across paired relays](#across-paired-relays).
|
|
1939
2112
|
|
|
2113
|
+
`0017-conversation-history-indexes.mjs` adds two indexes and nothing else:
|
|
2114
|
+
`queue(response_message_id)` for the sub-agent records of each reply on a history page, and
|
|
2115
|
+
`messages(conversation_id, timestamp, id)` for selecting the page itself. `GET
|
|
2116
|
+
/api/conversation/:id` and the shared view select the requested page in SQL in conversation order
|
|
2117
|
+
(timestamp, then id) and hydrate only its replies, so a page costs the same in a conversation of
|
|
2118
|
+
any length. A conversation with a row whose timestamp is not the relay's ISO form
|
|
2119
|
+
(`YYYY-MM-DDTHH:MM:SS.mmmZ`) or whose id is not lowercase letters, digits and dashes is paged the
|
|
2120
|
+
old way: every row loaded, the page cut afterwards.
|
|
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
|
+
|
|
1940
2129
|
`0007-claude-cloud.mjs` adds the storage of [Claude Cloud](#claude-cloud-provider) conversations.
|
|
1941
2130
|
It is additive and idempotent, runs on every boot, and a failure is logged and retried on the next
|
|
1942
2131
|
one:
|
|
@@ -81,6 +81,8 @@ export const REMOTE_RELAY_TOOL_ZOD_SHAPE = {
|
|
|
81
81
|
subscription_id: text('subscription_id'),
|
|
82
82
|
watcher: text('watcher'),
|
|
83
83
|
from: text('from'),
|
|
84
|
+
task_id: text('task_id'),
|
|
85
|
+
lines: number('lines'),
|
|
84
86
|
};
|
|
85
87
|
|
|
86
88
|
export function createRemoteRelayToolDefinition({ api, getConversationId = () => '', dbg = () => {} } = {}) {
|
|
@@ -1,5 +1,6 @@
|
|
|
1
1
|
import { createSdkMcpServer, query } from '@anthropic-ai/claude-agent-sdk';
|
|
2
2
|
|
|
3
|
+
import { formatClaudeBackgroundCommandLimitLabel } from '../../shared/claude-background-command-limit.mjs';
|
|
3
4
|
import { parseThinkingDisplay } from '../../shared/claude-thinking.mjs';
|
|
4
5
|
import { MEDIA_EMBED_INSTRUCTION_TEXT } from '../../shared/media-embed-instructions.mjs';
|
|
5
6
|
import { REMOTE_RELAY_TOOL_CALL_TIMEOUT_MS } from '../../shared/remote-relay-tool-core.mjs';
|
|
@@ -136,11 +137,57 @@ export function permissionModeForRelayMode(relayMode) {
|
|
|
136
137
|
return 'default';
|
|
137
138
|
}
|
|
138
139
|
|
|
139
|
-
|
|
140
|
+
// Bash timeouts the CLI reads from its environment (measured against CLI
|
|
141
|
+
// 2.1.292). BASH_DEFAULT_TIMEOUT_MS is the only lever on the background
|
|
142
|
+
// default (`max(30 min, BASH_DEFAULT_TIMEOUT_MS)`), but it also becomes the
|
|
143
|
+
// foreground default; CLAUDE_CODE_AUTO_BACKGROUND_TIMEOUT_MS therefore caps a
|
|
144
|
+
// main-agent foreground command at 2 min and then moves it to the background
|
|
145
|
+
// instead of letting it block the turn. BASH_MAX_TIMEOUT_MS lifts the ceiling
|
|
146
|
+
// an explicit `timeout` may ask for (foreground and background) to 8 h.
|
|
147
|
+
// Commands that start with `sleep` are never moved to the background by the
|
|
148
|
+
// CLI, and subagents get no auto-background; both run in the foreground for
|
|
149
|
+
// up to the default.
|
|
150
|
+
export const CLAUDE_BASH_MAX_TIMEOUT_MS = 8 * 3_600_000;
|
|
151
|
+
export const CLAUDE_AUTO_BACKGROUND_TIMEOUT_MS = 120_000;
|
|
152
|
+
|
|
153
|
+
/**
|
|
154
|
+
* The environment a Claude CLI spawn gets, or null to inherit the worker's
|
|
155
|
+
* own. `env` REPLACES the subprocess environment in the SDK, so the base is
|
|
156
|
+
* spread in.
|
|
157
|
+
*/
|
|
158
|
+
export function claudeSpawnEnv({ backgroundCommandLimitMs = null, baseEnv = process.env } = {}) {
|
|
159
|
+
const limit = Number(backgroundCommandLimitMs);
|
|
160
|
+
if (backgroundCommandLimitMs === null || !Number.isFinite(limit) || limit <= 0) return null;
|
|
161
|
+
return {
|
|
162
|
+
...baseEnv,
|
|
163
|
+
BASH_DEFAULT_TIMEOUT_MS: String(Math.round(limit)),
|
|
164
|
+
BASH_MAX_TIMEOUT_MS: String(Math.max(CLAUDE_BASH_MAX_TIMEOUT_MS, Math.round(limit))),
|
|
165
|
+
CLAUDE_CODE_AUTO_BACKGROUND_TIMEOUT_MS: String(CLAUDE_AUTO_BACKGROUND_TIMEOUT_MS),
|
|
166
|
+
};
|
|
167
|
+
}
|
|
168
|
+
|
|
169
|
+
/** How to run long commands, with the session's limit when it is known. */
|
|
170
|
+
export function longCommandInstructionText(backgroundCommandLimitMs = null) {
|
|
171
|
+
const limit = Number(backgroundCommandLimitMs);
|
|
172
|
+
const known = backgroundCommandLimitMs !== null && Number.isFinite(limit) && limit > 0;
|
|
173
|
+
const limitClause = known
|
|
174
|
+
? `Background Bash commands are stopped after ${formatClaudeBackgroundCommandLimitLabel(limit / 60_000)} unless you pass a longer timeout (up to 8 h); `
|
|
175
|
+
: 'For long background Bash commands, ';
|
|
176
|
+
return `${limitClause}write the full output to a log file instead of piping it into tail or head, `
|
|
177
|
+
+ 'since the pipe holds the output back and loses it if the command is stopped. '
|
|
178
|
+
+ 'Start runs that must survive a session restart detached with nohup and a log file.';
|
|
179
|
+
}
|
|
180
|
+
|
|
181
|
+
export function systemPromptForRelayMode(relayMode, { backgroundCommandLimitMs = null } = {}) {
|
|
140
182
|
const mode = String(relayMode || 'agent').trim().toLowerCase();
|
|
141
183
|
// The media-embed capability rides every session — unlike the mode nudges it
|
|
142
|
-
// is renderer knowledge the model cannot discover any other way.
|
|
143
|
-
|
|
184
|
+
// is renderer knowledge the model cannot discover any other way. The long
|
|
185
|
+
// command note rides every session too: the limit is the relay's setting.
|
|
186
|
+
const append = [
|
|
187
|
+
MEDIA_EMBED_INSTRUCTION_TEXT,
|
|
188
|
+
longCommandInstructionText(backgroundCommandLimitMs),
|
|
189
|
+
MODE_SYSTEM_PROMPT_APPEND[mode],
|
|
190
|
+
]
|
|
144
191
|
.filter(Boolean)
|
|
145
192
|
.join('\n\n');
|
|
146
193
|
return {
|
|
@@ -233,6 +280,10 @@ export function startClaudeSession({
|
|
|
233
280
|
autoCompactWindow = null,
|
|
234
281
|
thinkingEnabled = null,
|
|
235
282
|
attribution = null,
|
|
283
|
+
// How long a backgrounded Bash command may run without its own timeout
|
|
284
|
+
// (ms, the relay's setting); null leaves the CLI's defaults and the
|
|
285
|
+
// worker's environment alone.
|
|
286
|
+
backgroundCommandLimitMs = null,
|
|
236
287
|
thinkingDisplay = '',
|
|
237
288
|
abortController,
|
|
238
289
|
canUseTool,
|
|
@@ -259,14 +310,16 @@ export function startClaudeSession({
|
|
|
259
310
|
const relayMcpServer = typeof api === 'function'
|
|
260
311
|
? createRelayMcpServer({ api, getConversationId, dbg, remoteRelayTool: remoteRelayTool === true })
|
|
261
312
|
: null;
|
|
313
|
+
const env = claudeSpawnEnv({ backgroundCommandLimitMs });
|
|
262
314
|
const options = {
|
|
263
315
|
cwd,
|
|
264
316
|
permissionMode: permissionModeForRelayMode(relayMode),
|
|
265
|
-
systemPrompt: systemPromptForRelayMode(relayMode),
|
|
317
|
+
systemPrompt: systemPromptForRelayMode(relayMode, { backgroundCommandLimitMs: env ? backgroundCommandLimitMs : null }),
|
|
266
318
|
includePartialMessages: true,
|
|
267
319
|
forwardSubagentText: true,
|
|
268
320
|
...(effortOption ? { effort: effortOption } : {}),
|
|
269
321
|
...(spawnSettings ? { settings: spawnSettings } : {}),
|
|
322
|
+
...(env ? { env } : {}),
|
|
270
323
|
...(normalizedModel && normalizedModel !== 'auto' ? { model: String(model).trim() } : {}),
|
|
271
324
|
...(String(resume || '').trim() ? { resume: String(resume).trim() } : {}),
|
|
272
325
|
...(abortController ? { abortController } : {}),
|