@oar-sh/oar 0.9.10 → 0.9.12
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 +440 -1
- package/README.md +156 -972
- package/package.json +13 -13
- package/server/README.md +112 -25
- 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 +217 -11
- package/server/claude-worker/claude-session-worker.mjs +26 -1
- package/server/claude-worker/task-output-discovery.mjs +126 -0
- package/server/copilot-worker/copilot-sdk-session-process.mjs +235 -14
- package/server/copilot-worker/copilot-sdk-session-worker.mjs +8 -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 +68 -2
- package/server/grok-worker/acp-client.mjs +2 -1
- package/server/grok-worker/acp-host-services.mjs +46 -6
- package/server/grok-worker/grok-background-tasks.mjs +241 -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/0013-previews.mjs +130 -0
- package/server/migrations/0014-conversation-groups.mjs +89 -0
- package/server/migrations/0015-conversation-group-position.mjs +76 -0
- package/server/migrations/0016-share-access-log.mjs +110 -0
- package/server/migrations/0017-conversation-history-indexes.mjs +89 -0
- package/server/public/app/about-ui.js +73 -0
- package/server/public/app/about-view.mjs +234 -0
- package/server/public/app/action-confirmations.js +31 -14
- package/server/public/app/agent-exchange-fold-view.mjs +40 -101
- package/server/public/app/api-client.js +264 -16
- package/server/public/app/ask-user-view.js +17 -15
- package/server/public/app/background-tasks-view.mjs +285 -16
- package/server/public/app/bootstrap.js +253 -37
- package/server/public/app/chat-actions-menu-state.mjs +20 -10
- package/server/public/app/claude-background-command-limit-options.mjs +21 -0
- package/server/public/app/context-menu.mjs +90 -6
- package/server/public/app/conversation-group-drag.mjs +191 -0
- package/server/public/app/conversation-group-order.mjs +265 -0
- package/server/public/app/conversation-group-state.mjs +100 -0
- package/server/public/app/conversation-list-drag.mjs +449 -0
- package/server/public/app/conversation-payload-cache.mjs +357 -0
- package/server/public/app/conversation-turn-state.mjs +81 -0
- package/server/public/app/conversation-view.js +567 -94
- package/server/public/app/conversation-window-retention.mjs +96 -0
- package/server/public/app/cwd-picker.js +1 -1
- package/server/public/app/hidden-messages-view.mjs +312 -0
- package/server/public/app/host-suspend-ui.js +7 -1
- package/server/public/app/journal-view.js +1033 -82
- package/server/public/app/preview-cards.mjs +117 -31
- package/server/public/app/preview-command.mjs +10 -1
- package/server/public/app/relay-board-view.js +19 -6
- package/server/public/app/relay-prompt-sanitizer.mjs +11 -4
- package/server/public/app/relay-restart-error.mjs +16 -0
- package/server/public/app/settings-modal.js +150 -10
- package/server/public/app/settings-tabs.js +1 -1
- package/server/public/app/share-format.mjs +49 -0
- package/server/public/app/share-preview-marks.mjs +167 -0
- package/server/public/app/share-state.js +109 -0
- package/server/public/app/shared-conversations-settings.js +480 -0
- package/server/public/app/socket-handlers.js +138 -20
- package/server/public/app/store.js +121 -30
- package/server/public/app/subagent-row.mjs +3 -3
- package/server/public/app/subagent-viewer.mjs +37 -7
- package/server/public/app/task-console-render.mjs +237 -0
- package/server/public/app/task-console-watch.mjs +42 -0
- package/server/public/app/task-console.mjs +630 -0
- package/server/public/app/task-output-text.mjs +234 -0
- package/server/public/app/thinking-follow.js +0 -4
- package/server/public/app/transcript-island.mjs +73 -0
- package/server/public/app/transcript-jump-bottom-view.mjs +230 -26
- package/server/public/app/transcript-jump-bottom.mjs +170 -22
- 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 +159 -0
- package/server/public/index.html +625 -21
- package/server/relay-tools.md +3 -1
- package/server/relay.mjs +2 -1
- package/server/repositories/conversation-group-repository.mjs +444 -0
- package/server/repositories/message-repository.mjs +9 -0
- package/server/repositories/question-repository.mjs +4 -0
- package/server/repositories/session-repository.mjs +12 -37
- package/server/repositories/share-repository.mjs +322 -0
- package/server/repositories/share-visibility-statements.mjs +10 -0
- package/server/routes/about-routes.mjs +15 -0
- package/server/routes/cache-routes.mjs +4 -0
- package/server/routes/conversation-group-routes.mjs +171 -0
- package/server/routes/messages-routes.mjs +433 -68
- package/server/routes/preview-routes.mjs +9 -1
- package/server/routes/sessions-routes.mjs +901 -253
- package/server/server-runtime.mjs +392 -99
- package/server/services/about-service.mjs +461 -0
- package/server/services/background-task-store.mjs +365 -0
- package/server/services/background-work-view.mjs +228 -0
- package/server/services/cache-rebuild-service.mjs +30 -0
- package/server/services/cli-process-runner.mjs +4 -21
- package/server/services/conversation-history-page.mjs +199 -0
- package/server/services/delete-archive-service.mjs +4 -1
- package/server/services/detached-worker-watch-service.mjs +150 -0
- package/server/services/hidden-message-list.mjs +21 -0
- package/server/services/host-suspend-command.mjs +2 -1
- package/server/services/message-pin-list.mjs +1 -1
- package/server/services/preview-health-probe.mjs +8 -1
- package/server/services/preview-proxy-server.mjs +8 -4
- package/server/services/preview-registry-service.mjs +676 -26
- package/server/services/preview-static-handler.mjs +40 -7
- package/server/services/preview-store.mjs +274 -0
- package/server/services/rate-limit-service.mjs +12 -1
- package/server/services/relay-auth.mjs +183 -0
- package/server/services/relay-cli-launcher-service.mjs +3 -1
- package/server/services/relay-event-service.mjs +17 -0
- package/server/services/relay-prompt-sanitizer.mjs +11 -4
- package/server/services/remote-relay-dispatcher.mjs +187 -7
- package/server/services/request-boundary.mjs +183 -0
- package/server/services/session-activity-tracker.mjs +231 -19
- package/server/services/session-worker-launch-service.mjs +190 -6
- package/server/services/session-worker-process-service.mjs +10 -6
- package/server/services/share-access-log-service.mjs +123 -0
- package/server/services/share-path-redaction.mjs +424 -0
- package/server/services/status-event-service.mjs +7 -3
- package/server/services/task-output-rooms.mjs +259 -0
- package/server/services/task-output-service.mjs +562 -0
- package/server/services/tunnel-worker-path-guard.mjs +24 -41
- package/server/services/windows-autostart-service.mjs +6 -1
- package/server/services/windows-boot-autostart-service.mjs +207 -23
- package/shared/claude-background-command-limit.mjs +83 -0
- package/shared/preview-tool-core.mjs +15 -5
- package/shared/remote-relay-contract.mjs +35 -3
- package/shared/share-path-privacy.mjs +30 -0
- package/shared/task-output-text.mjs +737 -0
- package/shared/windows-system-tools.mjs +106 -0
- package/shared/worker-runtime/background-task-digest.mjs +30 -0
- package/shared/worker-runtime/heartbeat.mjs +24 -0
- package/shared/worker-runtime/kept-posts.mjs +83 -0
- package/shared/worker-runtime/task-output-spool.mjs +181 -0
- package/shared/worker-runtime/worker-websocket-link.mjs +47 -15
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@oar-sh/oar",
|
|
3
|
-
"version": "0.9.
|
|
3
|
+
"version": "0.9.12",
|
|
4
4
|
"type": "module",
|
|
5
5
|
"description": "OAR — Open Agent Relay: drive your local coding agents from any browser",
|
|
6
6
|
"license": "MIT",
|
|
@@ -50,20 +50,20 @@
|
|
|
50
50
|
"test:e2e:raw": "node ./node_modules/@playwright/test/cli.js test --config tests/playwright.config.mjs"
|
|
51
51
|
},
|
|
52
52
|
"dependencies": {
|
|
53
|
-
"@anthropic-ai/claude-agent-sdk": "
|
|
54
|
-
"@cursor/sdk": "
|
|
55
|
-
"@github/copilot-sdk": "
|
|
56
|
-
"better-sqlite3": "
|
|
57
|
-
"express": "
|
|
58
|
-
"qrcode-terminal": "
|
|
59
|
-
"socket.io": "
|
|
60
|
-
"uuid": "
|
|
61
|
-
"web-push": "
|
|
62
|
-
"ws": "
|
|
63
|
-
"zod": "
|
|
53
|
+
"@anthropic-ai/claude-agent-sdk": "0.3.292",
|
|
54
|
+
"@cursor/sdk": "1.0.36",
|
|
55
|
+
"@github/copilot-sdk": "1.0.17",
|
|
56
|
+
"better-sqlite3": "12.11.1",
|
|
57
|
+
"express": "4.22.3",
|
|
58
|
+
"qrcode-terminal": "0.12.0",
|
|
59
|
+
"socket.io": "4.8.3",
|
|
60
|
+
"uuid": "14.0.1",
|
|
61
|
+
"web-push": "3.6.7",
|
|
62
|
+
"ws": "8.21.3",
|
|
63
|
+
"zod": "4.4.3"
|
|
64
64
|
},
|
|
65
65
|
"optionalDependencies": {
|
|
66
|
-
"tty-console": "
|
|
66
|
+
"tty-console": "1.0.4"
|
|
67
67
|
},
|
|
68
68
|
"devDependencies": {
|
|
69
69
|
"@playwright/test": "^1.60.0",
|
package/server/README.md
CHANGED
|
@@ -212,12 +212,18 @@ at confirm time). While a suspend is queued every connected client shows a
|
|
|
212
212
|
banner with the blockers or the countdown and a **Cancel** button, the menu entry
|
|
213
213
|
reads **💤 Suspend pending…**, and reopening the modal offers to cancel. A relay
|
|
214
214
|
restart drops the queued suspend; a push notification (event **Host suspend**)
|
|
215
|
-
reports both the actual suspend and such a drop.
|
|
215
|
+
reports both the actual suspend and such a drop. For the first 45 seconds after
|
|
216
|
+
a relay start the check lists one more blocker, "the relay just started,
|
|
217
|
+
sessions are still reporting": the workers that outlived the previous runtime
|
|
218
|
+
may not have reported their background work yet. A queued **🌄 Restart web relay**
|
|
216
219
|
gets the same banner and can be cancelled from it too.
|
|
217
220
|
|
|
218
221
|
API: `GET /api/host/suspend` (state plus current blockers), `POST /api/host/suspend`,
|
|
219
222
|
`POST /api/host/suspend/cancel`, and `POST /api/relay/shutdown/cancel`
|
|
220
|
-
(
|
|
223
|
+
(token only, like the restart itself). Set `OAR_HOST_SUSPEND_DRY_RUN=1` to log instead of sleeping;
|
|
224
|
+
`OAR_BOOT_REPORTING_WINDOW_MS` changes the 45 seconds (0 to 600000; test relays
|
|
225
|
+
pin `0`, which switches the wait off; a negative value is ignored); an override in effect is
|
|
226
|
+
logged at start.
|
|
221
227
|
|
|
222
228
|
You can also hide the **💤 Suspend host** action from **⚙️ Settings** with the
|
|
223
229
|
**Show Suspend host action** checkbox. This only controls the menu entry; the
|
|
@@ -371,6 +377,41 @@ that started them. The live task set is posted to `POST /api/background-tasks` (
|
|
|
371
377
|
and broadcast as the `background_tasks` socket event; the composer's task panel renders it and can
|
|
372
378
|
stop an individual task over a `worker.control` push.
|
|
373
379
|
|
|
380
|
+
The relay holds the task sets in memory (`services/background-task-store.mjs`), so a relay restart
|
|
381
|
+
starts without them while the workers and their tasks keep running. The worker is the source of
|
|
382
|
+
truth, and the relay asks:
|
|
383
|
+
|
|
384
|
+
- **Digest and resync.** A worker's heartbeat carries `backgroundTasks: { count, digest }`, a digest
|
|
385
|
+
of its sorted task ids (`shared/worker-runtime/background-task-digest.mjs`). When it differs from
|
|
386
|
+
the set held for that worker (a restart, or a lost post either way round), the heartbeat answer
|
|
387
|
+
says `resyncBackgroundTasks: true` and the worker posts its full set with `resync: true`; the
|
|
388
|
+
request repeats on every beat until the two agree. A task restored this way keeps its start time
|
|
389
|
+
and is not announced as started again. Every post of a set carries `order: { epoch, seq }` (the
|
|
390
|
+
worker process and its count of set posts): two posts can overtake each other on the way, and the
|
|
391
|
+
relay drops one that is older than the newest it applied from that process, or that comes from a
|
|
392
|
+
process other than the one whose heartbeat came last for the session. A drop is logged and
|
|
393
|
+
answered `stale: true` with a `reason`; a worker told so for its newest set asks for a heartbeat,
|
|
394
|
+
on which the relay asks for the set again. `epoch` only names the process and is never compared
|
|
395
|
+
as a time. A task that ended only because a set did not list it comes back, without a second
|
|
396
|
+
start, when a later set lists it, unless a settle notice or its run's status has confirmed the
|
|
397
|
+
end meanwhile. A worker that sends no digest (Cursor, Grok, Claude Cloud, or a worker on older
|
|
398
|
+
code) has no opinion and is never asked.
|
|
399
|
+
- **Placeholder.** A live Claude worker on older code cannot answer, but its heartbeat says whether
|
|
400
|
+
background work holds its process (`steering.autoCompactWindow.backgroundWork`). While this
|
|
401
|
+
runtime has not heard from that worker about its tasks, the session reads `background_only` with
|
|
402
|
+
one task "background work, details not reported since the relay restarted (Kill session in its ⋯
|
|
403
|
+
menu ends it)", the panel shows the
|
|
404
|
+
same row without a Stop button, and a host suspend waits. It is derived on read
|
|
405
|
+
(`services/background-work-view.mjs`), raises no event, and goes when the set arrives, the flag
|
|
406
|
+
goes false or the worker is gone.
|
|
407
|
+
- **Ends.** The Claude worker keeps a settle notice whose post failed (the newest 20, for 10
|
|
408
|
+
minutes) and sends it again after the next heartbeat the relay answers; such a post is given up
|
|
409
|
+
after 10 s without an answer.
|
|
410
|
+
- **Run rows left `running`.** A background sub-agent's row whose end was never written would count
|
|
411
|
+
as running work for a day. For a Claude worker that has reported its task set in this runtime, a
|
|
412
|
+
`running` background row that the set does not list and that nothing has updated for 15 minutes
|
|
413
|
+
is left out of the session state, `all_idle` and the suspend blockers.
|
|
414
|
+
|
|
374
415
|
Tasks of type `local_workflow` carry an extra `workflowProgress` digest, because the SDK exposes no
|
|
375
416
|
workflow-progress surface — the worker reads it off disk:
|
|
376
417
|
|
|
@@ -517,7 +558,7 @@ conversation that speaks the usual worker contracts (WebSocket link, heartbeat,
|
|
|
517
558
|
crash guard) but runs no agent: it sends the user's messages to the cloud session and follows the
|
|
518
559
|
session's event stream (`claude-cloud-session-process.mjs`, `claude-cloud-event-normalizer.mjs`).
|
|
519
560
|
The user-facing description, including what the provider does with the Claude CLI's login, is in
|
|
520
|
-
the [
|
|
561
|
+
the [providers guide](../docs/providers.md#claude-cloud); the tracker is
|
|
521
562
|
[docs/feature-tracker/claude-cloud.md](../docs/feature-tracker/claude-cloud.md).
|
|
522
563
|
|
|
523
564
|
### The cloud client and the login
|
|
@@ -845,7 +886,12 @@ posts it from `task_notification`), the sub-agent run statuses and the activity,
|
|
|
845
886
|
timeline posts (the activity clock). A task that leaves the set without word ends after 5 s with
|
|
846
887
|
its run's status, else `completed` with `statusReported: false`; an exit code is read from the task
|
|
847
888
|
summary when it names one (the SDK has no field for it). Memory only: a restart forgets the recently
|
|
848
|
-
finished tasks and starts the clock at boot.
|
|
889
|
+
finished tasks and starts the clock at boot. Running tasks come back when the workers send their
|
|
890
|
+
sets again (see [Background tasks and workflow progress](#background-tasks-and-workflow-progress)):
|
|
891
|
+
a task that started before this runtime did is not announced as started a second time, and the end
|
|
892
|
+
of a task this runtime never knew is still an event when the settle notice describes the task
|
|
893
|
+
(`description`, `taskType`, `startedAt`). `all_idle` waits out the first 45 seconds after a relay
|
|
894
|
+
start, like the suspend check.
|
|
849
895
|
|
|
850
896
|
`GET /api/conversations` and `GET /api/conversation/:id` carry `sessionState` (`running_turn`: a
|
|
851
897
|
turn processing, or queued and not held by a usage-limit pause; `background_only`: tasks running;
|
|
@@ -853,7 +899,12 @@ turn processing, or queued and not held by a usage-limit pause; `background_only
|
|
|
853
899
|
`subagent` | `shell`, `description`, `startedAt`, `lastActivityAt`, `status` `running` |
|
|
854
900
|
`completed` | `failed` | `stopped`, `exitCode`, `summary`, `endedAt`) and `lastActivityAt`.
|
|
855
901
|
`remote_relay`'s `list_sessions` and `read_session` pass them on as `state`, `tasks` and
|
|
856
|
-
`lastActivityAt`; `active` stays and is true for background work too.
|
|
902
|
+
`lastActivityAt`; `active` stays and is true for background work too. `read_session` adds to each
|
|
903
|
+
task its `taskId`, `output` (whether it has output to read) and, for a task with an output file,
|
|
904
|
+
`health` (`minutesSinceOutput`, `quiet`, `quietAfterMinutes`, test-run `counts`);
|
|
905
|
+
`read_task_output {session, task_id, lines?}` (read permission; default 200 lines, at most 500 or
|
|
906
|
+
32 KB) reads the end of that output through `GET /api/conversation/:id/background-task/:taskId/output`,
|
|
907
|
+
scrubbed of credentials.
|
|
857
908
|
|
|
858
909
|
### Browser
|
|
859
910
|
|
|
@@ -871,18 +922,19 @@ socket event (the whole active list after every change).
|
|
|
871
922
|
### Folded agent exchanges
|
|
872
923
|
|
|
873
924
|
A prompt another agent sent the session (a paired relay's, or one of this relay's own: a user row
|
|
874
|
-
with `origin`)
|
|
875
|
-
|
|
876
|
-
|
|
925
|
+
with `origin`) folds into one row, `note from <relay> · <session> · answered below` (`working…`,
|
|
926
|
+
`failed`, `stopped`, `no reply yet`), which opens to the prompt as it renders otherwise.
|
|
927
|
+
Presentation only (`agent-exchange-fold-view.mjs` decides, `syncAgentExchangeFolds` in
|
|
877
928
|
`conversation-view.js` applies it as one more idempotent pass after every render and update, beside
|
|
878
929
|
the separator and steering passes); the transcript data is unchanged. Nothing addressed to the user
|
|
879
|
-
folds away:
|
|
880
|
-
|
|
881
|
-
|
|
882
|
-
|
|
883
|
-
|
|
884
|
-
|
|
885
|
-
jump to
|
|
930
|
+
folds away: the fold covers the agent's prompt alone. The session's reply to it can hold a line for
|
|
931
|
+
the user, so the reply, the live bubble of the running turn and the turn's question, approval and
|
|
932
|
+
plan cards always show, as for a message of the user's own; they are read for the row's status
|
|
933
|
+
only. One prompt is left unfolded: an agent's prompt that was itself steered into a running turn,
|
|
934
|
+
which renders as part of that turn's flow. A folded prompt shows open while its turn is `working…`
|
|
935
|
+
and closes by itself once it is replied, failed or stopped (`agentExchangeFoldOpen`); the user's
|
|
936
|
+
own toggle, kept by prompt id, always wins over that, and a jump to the prompt (search, pins, image
|
|
937
|
+
parent) opens it.
|
|
886
938
|
|
|
887
939
|
## Image conversations (OpenAI BYOK)
|
|
888
940
|
|
|
@@ -911,6 +963,24 @@ images, calling the OpenAI Images API directly rather than going through a CLI w
|
|
|
911
963
|
- The control appears on message hover; already-hidden messages keep their label visible so the state
|
|
912
964
|
is never silently lost.
|
|
913
965
|
- `POST /api/shared/:token/presence` tracks shared-viewer presence, surfaced as a watcher count.
|
|
966
|
+
`sharedPresenceMaxPerConversation` and `sharedPresenceMaxGlobal` cap how many viewers that count
|
|
967
|
+
tracks; they do not limit who can read a link.
|
|
968
|
+
- A revoked or renewed token answers a plain 404 on every shared route, the `/shared/:token` page
|
|
969
|
+
included, exactly like a token that never existed. An open shared page stops at its next poll.
|
|
970
|
+
- Settings → Sharing lists the active shares (`GET /api/shares`) with **Copy link**, **Renew link**
|
|
971
|
+
and **Stop sharing**, and each share's access log. Renewing adds a token to the share's lineage
|
|
972
|
+
(`conversation_shares.lineage_id`, migration 0016) and revokes the old one, so the log, keyed to
|
|
973
|
+
the lineage, carries over. Stopping a share or deleting the conversation deletes its log; nothing
|
|
974
|
+
else prunes it.
|
|
975
|
+
- The access log (`conversation_share_opens`) has one row per open, not per poll: the shared page's
|
|
976
|
+
first fetch carries `open=1` and counts unless the same viewer opened it in the last 10 s; a
|
|
977
|
+
fetch without the flag counts only when that viewer has not fetched the share for 30 minutes
|
|
978
|
+
(a page back from the background, a script). A row keeps the time, the IP address (unless
|
|
979
|
+
**Store viewer IP addresses** is off), the user agent, the country from `cf-ipcountry` (only when
|
|
980
|
+
the request came through a proxy the `trustProxy` setting trusts) and a viewer key: an HMAC of
|
|
981
|
+
address and user agent under a per-relay secret, which counts distinct viewers without the
|
|
982
|
+
address. The address is the one Express derives through `trustProxy`; a forwarded header from an
|
|
983
|
+
untrusted peer is ignored.
|
|
914
984
|
|
|
915
985
|
## Pinned messages
|
|
916
986
|
|
|
@@ -1219,14 +1289,15 @@ Queue metrics include `parkedCount` for turns deferred behind restart/rebind gat
|
|
|
1219
1289
|
| POST | `/api/stream` | (CLI) Push in-flight assistant text stream for current pending message |
|
|
1220
1290
|
| POST | `/api/relay/pause` | Pause dequeueing and drop currently queued messages |
|
|
1221
1291
|
| POST | `/api/relay/resume` | Resume dequeueing after pause |
|
|
1222
|
-
| POST | `/api/relay/shutdown` | Queue
|
|
1292
|
+
| POST | `/api/relay/shutdown` | Queue an authenticated relay shutdown or self-restart |
|
|
1223
1293
|
| GET | `/api/relay-questions` | (CLI/UI) List relay questions by `status` (for example `pending` or `answered`) |
|
|
1224
1294
|
| GET | `/api/relay-question/:id` | (CLI) Fetch a single relay question |
|
|
1225
1295
|
| POST | `/api/relay-question` | (CLI) Create a relay question for the browser |
|
|
1226
1296
|
| POST | `/api/relay-question/:id/answer` | (UI) Submit an answer for a relay question |
|
|
1227
1297
|
| POST | `/api/relay-question/:id/timeout` | (CLI/UI) Mark a relay question timed out |
|
|
1228
|
-
| POST | `/api/heartbeat` | (CLI) Keep CLI status alive |
|
|
1298
|
+
| POST | `/api/heartbeat` | (CLI) Keep CLI status alive. A session worker may send `backgroundTasks: { count, digest }`; the answer carries `resyncBackgroundTasks: true` when the relay's set for that worker differs |
|
|
1229
1299
|
| GET | `/api/status` | Overall status |
|
|
1300
|
+
| GET | `/api/about` | The About OAR modal: `version`, `commit` (the checkout's HEAD read from `.git`, else a `gitHead` stamp in package.json, else `null`), `startedAt` and `uptimeSeconds` of the relay process; `providers[]` (enabled ones only: `cliVersion` from the CLI probe cache, never a new probe, `sdkVersion` from node_modules, `state` `online`/`offline` for the relay CLI that runs Copilot's extension engine and OpenAI, `online`/`idle` with `runningWorkers` for the others); `stats` (conversations, messages, turns = replies, `turnsByProvider`, `firstConversationAt`); `usage` (only what is stored: Copilot premium requests, Cursor and Grok tokens and cost, Claude Cloud cost); `relay` (`dbBytes` = database + WAL, `runningWorkers`); `update` (latest version only while the opt-in check is on). The totals are read in rowid ranges of 10 000 messages with the event loop free in between, and kept for 60 s |
|
|
1230
1301
|
| GET | `/api/models` | Live/cached model catalog used by the UI picker |
|
|
1231
1302
|
| POST | `/api/models/snapshot` | (CLI/relay) Publish discovered model snapshot |
|
|
1232
1303
|
| POST | `/api/conversation/:id/refresh-history` | Re-import an existing conversation's history through the local Copilot SDK |
|
|
@@ -1235,8 +1306,8 @@ Queue metrics include `parkedCount` for turns deferred behind restart/rebind gat
|
|
|
1235
1306
|
| 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 |
|
|
1236
1307
|
| GET | `/api/settings/cursor-allowance` | Read the manual Cursor plan allowances (`cursorModelsUsd`, `otherModelsUsd`, `resetDay`) |
|
|
1237
1308
|
| 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 |
|
|
1238
|
-
| GET | `/api/settings/claude` | Read Claude provider settings (`enabled`, `model`, `models`, `availableModels`) |
|
|
1239
|
-
| POST | `/api/settings/claude` | Enable/disable Claude, set the default model
|
|
1309
|
+
| GET | `/api/settings/claude` | Read Claude provider settings (`enabled`, `model`, `models`, `availableModels`, `attributionMode`, `backgroundCommandLimitMinutes` with the slider bounds `backgroundCommandLimitMinMinutes`, `backgroundCommandLimitMaxMinutes`, `backgroundCommandLimitStepMinutes`, `backgroundCommandLimitDefaultMinutes`) |
|
|
1310
|
+
| 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 |
|
|
1240
1311
|
| 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 |
|
|
1241
1312
|
| 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` |
|
|
1242
1313
|
| 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` |
|
|
@@ -1256,9 +1327,14 @@ Queue metrics include `parkedCount` for turns deferred behind restart/rebind gat
|
|
|
1256
1327
|
| POST | `/api/conversation/:conversationId/subagent/:subagentRunId/cancel` | Request cancellation of one subagent run (unsupported by the Claude runtime) |
|
|
1257
1328
|
| PATCH | `/api/conversation/:id/message/:messageId/share-visibility` | Hide or unhide a single message from shared views |
|
|
1258
1329
|
| PATCH | `/api/conversation/:id/message/:messageId/pin` | `{ pinned }` → pin or unpin a single message; answers with the conversation's pin list and broadcasts `conversation_pins_updated` |
|
|
1259
|
-
| POST | `/api/conversation/:id/share` |
|
|
1260
|
-
|
|
|
1261
|
-
|
|
|
1330
|
+
| POST | `/api/conversation/:id/share` | The conversation's read-only share link, made if it has none (one active share per conversation): `{ token, shareUrl, path, created }`; a new one sends `share_created` |
|
|
1331
|
+
| POST | `/api/conversation/:id/share/renew` | A new token for the active share; the old one answers 404 from then on and the access log carries over. Sends `share_renewed`; 404 without an active share |
|
|
1332
|
+
| DELETE | `/api/conversation/:id/share` | Stop sharing: revokes every token of the conversation and deletes its access log → `{ revokedAt, revokedCount, opensDeleted }`. Sends `share_revoked`; 404 without an active share |
|
|
1333
|
+
| GET | `/api/shares` | Every active share: `{ shares: [{ conversationId, title, archived, token, shareUrl, path, sharedSince, renewedAt, lastOpenedAt, openCount, viewerCount }], storeViewerIp, accessLog }` |
|
|
1334
|
+
| GET | `/api/conversation/:id/share/opens` | The active share's access log, newest first: `?limit=<n>` (default 200, max 1000) → `{ opens: [{ openedAt, ip, country, userAgent, viewer }], total, limit }` |
|
|
1335
|
+
| GET/POST | `/api/settings/shares` | `{ storeViewerIp }`: whether new access log entries (and the status events of shared reads) keep the viewer's IP address; on by default. A change sends `share_settings_changed` |
|
|
1336
|
+
| GET | `/api/shared/:token` | (No auth) Read-only shared conversation view (hidden messages filtered out). `?open=1` marks the page's first fetch, which the access log counts as an open |
|
|
1337
|
+
| POST | `/api/shared/:token/presence` | (No auth) Report shared-viewer presence for the watcher count |
|
|
1262
1338
|
| POST | `/api/openai/images/generate` | Generate images for an OpenAI Image conversation |
|
|
1263
1339
|
| POST | `/api/image-operations/:operationId/execute` | Execute a queued image generation/edit operation |
|
|
1264
1340
|
| GET | `/api/generated-image/:conversationId/:messageId/:imageId/content` | Stream a generated image blob |
|
|
@@ -1275,7 +1351,7 @@ Queue metrics include `parkedCount` for turns deferred behind restart/rebind gat
|
|
|
1275
1351
|
| 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 |
|
|
1276
1352
|
| 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) |
|
|
1277
1353
|
| POST | `/api/relay-events/remote/pull` | (Relay to relay) That relay fetches its events: `{ after, subscriptions: [{ id, watcher }], ended: [ids] }` → `{ ok, events, cursor, more, subscriptions, ended, dropped }` |
|
|
1278
|
-
| POST | `/api/background-task-settled` | (Worker) How a background task ended: `{ conversationId, taskId, toolUseId?, status, summary?, reason? }` (Claude worker, from `task_notification
|
|
1354
|
+
| POST | `/api/background-task-settled` | (Worker) How a background task ended: `{ conversationId, taskId, toolUseId?, status, summary?, reason?, description?, taskType?, startedAt? }` (Claude worker, from `task_notification`; the last three describe a task that was in its published set) |
|
|
1279
1355
|
| GET | `/api/remote-relays/inflight` | (Worker) Number of `remote_relay` calls running for `conversationId` |
|
|
1280
1356
|
| GET/POST | `/api/settings/remote-relays` | This relay's `publicUrl` and `inboundEnabled` (accept prompts from other relays' agents) |
|
|
1281
1357
|
| GET | `/api/settings/agent-sessions` | Agent sessions: `{ enabled, maxWaitSeconds, limits: { minWaitSeconds: 120, maxWaitSeconds: 3600, stepSeconds: 60 }, maxActiveSessions: 4 }`. `enabled` (default `false`) lets this relay's agents start and use sessions on it through `remote_relay`; `maxWaitSeconds` (default `600`) is the longest wait of one `remote_relay` call, for this relay and paired ones |
|
|
@@ -1283,7 +1359,8 @@ Queue metrics include `parkedCount` for turns deferred behind restart/rebind gat
|
|
|
1283
1359
|
|
|
1284
1360
|
### Manual relay shutdown / self-restart
|
|
1285
1361
|
|
|
1286
|
-
- `POST /api/relay/shutdown`
|
|
1362
|
+
- `POST /api/relay/shutdown`, `POST /api/relay/shutdown/cancel` and `POST /api/queue/empty` need the
|
|
1363
|
+
token and nothing else: the browser app calls them from a phone, over a tunnel and over the LAN.
|
|
1287
1364
|
- The request body accepts:
|
|
1288
1365
|
- `reason` (optional string)
|
|
1289
1366
|
- `requestedBy` (optional string)
|
|
@@ -1671,7 +1748,8 @@ service).
|
|
|
1671
1748
|
| `cloudflaredTunnel.token` | — | Tunnel token from the router panel (never logged) |
|
|
1672
1749
|
| `cloudflaredTunnel.binary` | `cloudflared` | Path or command name of the `cloudflared` you installed (OAR ships none); defaults to `cloudflared` on `PATH`. Without one the tunnel stays off and the status says how to install it |
|
|
1673
1750
|
| `cloudflaredTunnel.extraArgs` | `[]` | Extra arguments appended to `cloudflared tunnel run` |
|
|
1674
|
-
| `tunnelMarkerHeaders` | `[]` | Extra edge-injected header names that mark tunnel traffic
|
|
1751
|
+
| `tunnelMarkerHeaders` | `[]` | Extra edge-injected header names that mark tunnel traffic; merged with `COPILOT_TUNNEL_MARKER_HEADERS` and `cf-ray` (an empty list does not switch the env value off). A request carrying one is never "from this machine" (session-worker path guard, image-operation run, sign-in lockout exemption) |
|
|
1752
|
+
| `allowedOrigins` | `[]` | Extra browser origins the Socket.IO channel accepts besides the relay's own origin |
|
|
1675
1753
|
|
|
1676
1754
|
### SDK auto-detection behavior
|
|
1677
1755
|
|
|
@@ -1864,6 +1942,15 @@ the watched one `subscriber_relay_id`, `subscriber_relay_name`, `subscriber_watc
|
|
|
1864
1942
|
`subscriber_watcher_title`, `last_pull_at`, `dropped_count`) and `relay_event_links` (per paired
|
|
1865
1943
|
relay the fetch cursor and the link's health). See [Across paired relays](#across-paired-relays).
|
|
1866
1944
|
|
|
1945
|
+
`0017-conversation-history-indexes.mjs` adds two indexes and nothing else:
|
|
1946
|
+
`queue(response_message_id)` for the sub-agent records of each reply on a history page, and
|
|
1947
|
+
`messages(conversation_id, timestamp, id)` for selecting the page itself. `GET
|
|
1948
|
+
/api/conversation/:id` and the shared view select the requested page in SQL in conversation order
|
|
1949
|
+
(timestamp, then id) and hydrate only its replies, so a page costs the same in a conversation of
|
|
1950
|
+
any length. A conversation with a row whose timestamp is not the relay's ISO form
|
|
1951
|
+
(`YYYY-MM-DDTHH:MM:SS.mmmZ`) or whose id is not lowercase letters, digits and dashes is paged the
|
|
1952
|
+
old way: every row loaded, the page cut afterwards.
|
|
1953
|
+
|
|
1867
1954
|
`0007-claude-cloud.mjs` adds the storage of [Claude Cloud](#claude-cloud-provider) conversations.
|
|
1868
1955
|
It is additive and idempotent, runs on every boot, and a failure is logged and retried on the next
|
|
1869
1956
|
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 } : {}),
|