@oar-sh/oar 0.9.9 → 0.9.11
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 +561 -1
- package/README.md +162 -909
- package/bin/bat/oar.bat +46 -46
- package/package.json +14 -14
- package/server/README.md +369 -13
- package/server/claude-worker/claude-remote-relay-tool.mjs +10 -0
- package/server/claude-worker/claude-session-process.mjs +121 -9
- package/server/claude-worker/claude-session-worker.mjs +9 -1
- package/server/claude-worker/claude-turn-publisher.mjs +3 -0
- package/server/claude-worker/sdk-message-normalizer.mjs +31 -4
- package/server/claude-worker/subagent-timeline-recorder.mjs +572 -0
- package/server/copilot-worker/copilot-sdk-session-process.mjs +79 -12
- package/server/copilot-worker/copilot-sdk-session-worker.mjs +6 -0
- package/server/cursor-worker/cursor-sdk-adapter.mjs +56 -2
- package/server/db-schema.mjs +95 -2
- package/server/grok-worker/acp-client.mjs +2 -1
- package/server/grok-worker/acp-host-services.mjs +4 -3
- package/server/migrations/0009-subagent-timeline.mjs +135 -0
- package/server/migrations/0010-subagent-timeline-pruning.mjs +89 -0
- package/server/migrations/0011-relay-event-subscriptions.mjs +125 -0
- package/server/migrations/0012-cross-relay-event-subscriptions.mjs +116 -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/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 +187 -0
- package/server/public/app/api-client.js +312 -16
- package/server/public/app/archive-confirm.mjs +28 -0
- package/server/public/app/ask-user-view.js +17 -12
- package/server/public/app/attachments-view.js +516 -6
- package/server/public/app/background-tasks-view.mjs +18 -26
- package/server/public/app/bootstrap.js +336 -59
- package/server/public/app/chat-actions-menu-state.mjs +126 -0
- package/server/public/app/claude-cloud-conversation-ui.js +10 -22
- package/server/public/app/composer-control-state.mjs +16 -0
- package/server/public/app/composer-keyboard.mjs +54 -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-turn-state.mjs +81 -0
- package/server/public/app/conversation-view.js +436 -117
- package/server/public/app/cwd-picker.js +1 -1
- package/server/public/app/explorer-edit-state.mjs +68 -0
- package/server/public/app/hidden-messages-view.mjs +312 -0
- package/server/public/app/host-suspend-ui.js +13 -4
- package/server/public/app/journal-view.js +930 -45
- package/server/public/app/no-conversation-state.mjs +58 -0
- package/server/public/app/preview-cards.mjs +117 -31
- package/server/public/app/preview-command.mjs +10 -1
- package/server/public/app/provider-readiness.mjs +120 -0
- package/server/public/app/relay-board-view.js +19 -3
- package/server/public/app/relay-event-subscriptions-ui.js +184 -0
- package/server/public/app/relay-event-view.mjs +234 -0
- package/server/public/app/relay-restart-error.mjs +16 -0
- package/server/public/app/settings-modal.js +93 -9
- 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 +137 -40
- package/server/public/app/store.js +115 -32
- package/server/public/app/subagent-row.mjs +138 -0
- package/server/public/app/subagent-timeline-view.mjs +186 -0
- package/server/public/app/subagent-viewer-watch.mjs +50 -0
- package/server/public/app/subagent-viewer.mjs +558 -0
- package/server/public/app/task-row-format.mjs +29 -0
- package/server/public/app/transcript-jump-bottom-view.mjs +464 -0
- package/server/public/app/transcript-jump-bottom.mjs +210 -0
- package/server/public/app/transcript-reload-freshness.mjs +25 -0
- package/server/public/app/transcript-viewport-anchor.mjs +140 -0
- package/server/public/app/update-ui.js +1 -1
- package/server/public/app/viewer-history.mjs +5 -3
- package/server/public/index.html +774 -12
- package/server/relay-tools.md +2 -1
- package/server/relay.mjs +2 -1
- package/server/repositories/conversation-group-repository.mjs +444 -0
- package/server/repositories/message-repository.mjs +30 -3
- package/server/repositories/question-repository.mjs +9 -3
- package/server/repositories/remote-relay-repository.mjs +6 -2
- 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/repositories/subagent-timeline-statements.mjs +170 -0
- package/server/routes/about-routes.mjs +15 -0
- package/server/routes/ask-user-routes.mjs +3 -0
- package/server/routes/cache-routes.mjs +4 -0
- package/server/routes/conversation-group-routes.mjs +171 -0
- package/server/routes/messages-routes.mjs +698 -114
- package/server/routes/preview-routes.mjs +9 -1
- package/server/routes/relay-event-routes.mjs +157 -0
- package/server/routes/remote-relay-routes.mjs +15 -0
- package/server/routes/sessions-routes.mjs +843 -193
- package/server/server-runtime.mjs +559 -160
- package/server/services/about-service.mjs +461 -0
- package/server/services/background-task-store.mjs +128 -0
- package/server/services/background-work-view.mjs +228 -0
- package/server/services/cache-rebuild-service.mjs +30 -0
- package/server/services/delete-archive-service.mjs +4 -1
- package/server/services/detached-worker-watch-service.mjs +150 -0
- package/server/services/explorer-entry-name.mjs +26 -0
- package/server/services/explorer-mutations.mjs +122 -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 +49 -27
- 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/push-dispatch-service.mjs +18 -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-link-service.mjs +289 -0
- package/server/services/relay-event-service.mjs +1952 -0
- package/server/services/relay-shutdown-gate.mjs +31 -0
- package/server/services/remote-relay-caller-context.mjs +52 -7
- package/server/services/remote-relay-dispatcher.mjs +551 -7
- package/server/services/remote-relay-registry-service.mjs +11 -1
- package/server/services/request-boundary.mjs +183 -0
- package/server/services/session-activity-tracker.mjs +695 -0
- package/server/services/session-history-refresh-service.mjs +7 -0
- 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/stale-attempt-log-limiter.mjs +96 -0
- package/server/services/stale-recovery-clock-guard.mjs +57 -0
- package/server/services/stale-subagent-run-sweep.mjs +109 -0
- package/server/services/status-event-service.mjs +7 -3
- package/server/services/subagent-record-retention.mjs +164 -0
- package/server/services/subagent-timeline-service.mjs +328 -0
- package/server/services/subagent-viewer-rooms.mjs +119 -0
- package/server/services/tunnel-worker-path-guard.mjs +24 -41
- package/server/services/upgrade-socket-guard.mjs +16 -0
- package/server/services/windows-autostart-service.mjs +6 -1
- package/server/services/windows-boot-autostart-service.mjs +207 -23
- package/shared/preview-tool-core.mjs +15 -5
- package/shared/relay-events.mjs +371 -0
- package/shared/remote-relay-contract.mjs +160 -6
- package/shared/share-path-privacy.mjs +30 -0
- package/shared/subagent-record-retention.mjs +31 -0
- package/shared/subagent-timeline.mjs +79 -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/worker-websocket-link.mjs +47 -15
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
|
|
|
@@ -400,6 +441,69 @@ tree renderer as the live panel, `buildWorkflowRunCard` in `public/app/backgroun
|
|
|
400
441
|
survives a reload. A history rebuild deletes a conversation's `workflow_runs` alongside its
|
|
401
442
|
`subagent_runs`, because both key on message ids the rebuild replaces.
|
|
402
443
|
|
|
444
|
+
### Sub-agent records (the sub-agent viewer)
|
|
445
|
+
|
|
446
|
+
Every sub-agent run keeps its whole record in `subagent_timeline` (migration 0009): one row per
|
|
447
|
+
thought, reply text, tool call (full input), tool result (cut at 4 KB, the original length kept in
|
|
448
|
+
the entry's metadata), child agent or note, numbered per run by `seq` in the order the relay first
|
|
449
|
+
saw it. A writer names each entry with its own `entry_key`; a second write under the same key
|
|
450
|
+
updates the entry in place and keeps its `seq`, which is how a thought or a reply streams. A run
|
|
451
|
+
keeps at most 2000 entries plus one note saying where it was cut (`subagent_runs.truncated`).
|
|
452
|
+
`subagent_runs` also carries the run header the rows show: model, type, task id, tokens, whether it
|
|
453
|
+
runs in the background, and who writes its timeline.
|
|
454
|
+
|
|
455
|
+
- **Claude**: `claude-worker/subagent-timeline-recorder.mjs` reads every SDK message at process
|
|
456
|
+
level, before any turn takes it, and posts entries to `POST /api/subagent-timeline` keyed by the
|
|
457
|
+
run (coalesced every 500 ms; a run the relay does not know yet is retried). A background agent
|
|
458
|
+
outlives the turn that spawned it, so its frames arrive between turns and during later turns;
|
|
459
|
+
they go to its own record, never to a later turn's live lanes. A background launch's receipt
|
|
460
|
+
(`tool_use_result.status: 'async_launched'`) leaves the run running; its `task_notification`
|
|
461
|
+
ends it. Spawns carry `timelineOwner: 'worker'`.
|
|
462
|
+
- **Copilot and Cursor** publish their sub-agents on the live lanes only (`/api/activity`,
|
|
463
|
+
`/api/thought`, `/api/stream` with a `subagentRunId`); the relay copies each frame into the run's
|
|
464
|
+
timeline, uncut. Fields they do not report (model, tokens) stay blank.
|
|
465
|
+
- `POST /api/background-tasks` copies model, type, task id and tokens onto the run whose id is the
|
|
466
|
+
task's `toolUseId`, so the row keeps them after the in-memory task set is cleared.
|
|
467
|
+
- Every `subagent_status` event carries the whole header and the run's own turn (`messageId`). A
|
|
468
|
+
turn that fails closes its foreground runs; background runs are closed by their worker, or by the
|
|
469
|
+
relay when the worker dies.
|
|
470
|
+
|
|
471
|
+
The browser draws one row per top-level run in the reply bubble (`public/app/subagent-row.mjs`, the
|
|
472
|
+
panel's row grammar); a row's title, like a sub-agent task's title in the composer panel, opens
|
|
473
|
+
`public/app/subagent-viewer.mjs`. The viewer reads `GET /api/conversation/:id/subagent/:runId`, grows
|
|
474
|
+
with the `subagent_timeline` event, and fetches again from the last contiguous `seq` after a missed
|
|
475
|
+
event or a reconnect. A run recorded before the timeline existed is served from its tagged
|
|
476
|
+
activity, thought and stream rows (`legacy: true`).
|
|
477
|
+
|
|
478
|
+
**Live delivery.** `subagent_timeline` goes only to the pages whose viewer shows that run. The viewer
|
|
479
|
+
names its run with the socket message `subagent_viewer_watch { conversationId, subagentRunId }`
|
|
480
|
+
(acknowledged with `{ ok: true }` or `{ ok: false, code }`: `missing-id`, `not-found`,
|
|
481
|
+
`conversation-mismatch`) when it opens, moves to a child or its parent, and after every reconnect,
|
|
482
|
+
and sends `subagent_viewer_unwatch` when it closes. The relay checks the run exists and belongs to
|
|
483
|
+
the conversation and keeps each socket in at most one room, `subagent-viewer:<runId>`
|
|
484
|
+
(`services/subagent-viewer-rooms.mjs`). Batches go to that room as volatile packets, so they are
|
|
485
|
+
never kept for a reconnect replay and a backed-up connection drops them. socket.io also drops a
|
|
486
|
+
volatile packet while the connection is still writing an earlier one, even from the same tick
|
|
487
|
+
(a worker post that changes the header emits `subagent_status` first), so the relay holds batches
|
|
488
|
+
one tick, merged per run, before sending. The viewer covers what is dropped: it waits for the
|
|
489
|
+
watch's answer before its first read, fetches on a `seq` hole, after a reconnect, and once more when
|
|
490
|
+
the run ends. A refused `not-found` watch is sent again on the run's next `subagent_status`. The lane
|
|
491
|
+
events (`relay_activity`, `relay_thought`, `relay_stream`, `subagent_status`) go to every page as
|
|
492
|
+
before.
|
|
493
|
+
|
|
494
|
+
**Retention.** After `subagent_record_retention_days` (app setting, `0` = keep for good, otherwise 7,
|
|
495
|
+
14, 30 or 90; default 90; Settings → General → *Keep sub-agent tool output*) the relay strips a
|
|
496
|
+
finished run's tool entries: a `tool_result` keeps its row with empty text, a `tool_call` keeps its
|
|
497
|
+
one-line summary (`subagentToolCallSummary` in `shared/subagent-timeline.mjs`, the viewer's rule) in
|
|
498
|
+
place of the input. Both get `pruned_at` (migration 0010) and metadata
|
|
499
|
+
`{ pruned: true, prunedAt, originalChars, retentionDays }`; the viewer shows them as "Input removed
|
|
500
|
+
after N days" / "output removed after N days". Thoughts, reply text, notes, child rows and the runs
|
|
501
|
+
stay, and the age is the entry's `updated_at`; runs still `running` (or `pending`, `processing`) are
|
|
502
|
+
never touched, nor are the lane rows. `services/subagent-record-retention.mjs` sweeps 30 s after
|
|
503
|
+
start, once a day and 5 s after the setting changes, 2000 rows per transaction with the event loop
|
|
504
|
+
free in between, through a partial index over the unstripped tool entries, and logs one
|
|
505
|
+
`SUBAGENT RETENTION …` line. No VACUUM: SQLite reuses the freed pages.
|
|
506
|
+
|
|
403
507
|
### Context usage
|
|
404
508
|
|
|
405
509
|
Claude sessions have no Copilot `events.jsonl` to tail. Instead the worker reads the live
|
|
@@ -431,6 +535,20 @@ On Windows it opens a `Claude Worker <id>` terminal window.
|
|
|
431
535
|
turns. Those replies carry no usage line rather than displaying Copilot premium-request numbers under
|
|
432
536
|
a reply that never touched Copilot.
|
|
433
537
|
|
|
538
|
+
### Plan usage for agents
|
|
539
|
+
|
|
540
|
+
`remote_relay`'s `usage {relay}` (permission `read`, like `relay_info`; this relay too, as `this`)
|
|
541
|
+
reads `GET /api/usage` and `GET /api/usage-limit` on the target and answers per provider card
|
|
542
|
+
(Copilot, Claude, Claude Cloud, Cursor, Grok): `status` (`not-configured` and `unavailable` say why
|
|
543
|
+
in `message`), `plan`, `source`, `capturedAt`, the meters (`used`, `allowance`, `remaining`,
|
|
544
|
+
`utilization`, `unit`, `resetAt`, `severity`) and the card's detail rows and notes; plus
|
|
545
|
+
`usageLimit` (`account`: the Claude window, its utilization, reset and overage; `pauses`: the turns
|
|
546
|
+
held at a usage limit, by session). The dispatcher copies an allow-list of fields
|
|
547
|
+
(`usageCard` in `services/remote-relay-dispatcher.mjs`), so a field the usage report gains later is
|
|
548
|
+
not passed on; account labels and links are left out, and an e-mail address or a token-shaped
|
|
549
|
+
string in a text is masked. A relay without `/api/usage` per provider answers
|
|
550
|
+
`REMOTE_RELAY_UNSUPPORTED`; one without `/api/usage-limit` answers `usageLimit: null` with a note.
|
|
551
|
+
|
|
434
552
|
## Claude Cloud provider
|
|
435
553
|
|
|
436
554
|
Provider type `claude-cloud`: a provider of its own, never an alias of `claude`. Its conversations
|
|
@@ -440,7 +558,7 @@ conversation that speaks the usual worker contracts (WebSocket link, heartbeat,
|
|
|
440
558
|
crash guard) but runs no agent: it sends the user's messages to the cloud session and follows the
|
|
441
559
|
session's event stream (`claude-cloud-session-process.mjs`, `claude-cloud-event-normalizer.mjs`).
|
|
442
560
|
The user-facing description, including what the provider does with the Claude CLI's login, is in
|
|
443
|
-
the [
|
|
561
|
+
the [providers guide](../docs/providers.md#claude-cloud); the tracker is
|
|
444
562
|
[docs/feature-tracker/claude-cloud.md](../docs/feature-tracker/claude-cloud.md).
|
|
445
563
|
|
|
446
564
|
### The cloud client and the login
|
|
@@ -630,6 +748,189 @@ turn finishing and delivery — leaving a permanent spinner. `activeTurn: false`
|
|
|
630
748
|
client's optimistic state. Relay shutdown also waits 300 ms before disconnecting sockets so a terminal
|
|
631
749
|
`message_status` emitted just before an idle-deferred restart still reaches the browser.
|
|
632
750
|
|
|
751
|
+
## Relay event subscriptions
|
|
752
|
+
|
|
753
|
+
A session (the **watcher**) can ask to be told what happens in other sessions of this relay or of a
|
|
754
|
+
paired relay, through `remote_relay`. The relay tells it by writing a short event message into the
|
|
755
|
+
watcher's conversation; nothing reads transcripts on disk, and it works for every provider.
|
|
756
|
+
|
|
757
|
+
**Actions** (`relay` may be left out and means `this`; on this relay the agent-sessions setting must
|
|
758
|
+
be on, as for every local action; a paired relay: see [Across paired relays](#across-paired-relays)):
|
|
759
|
+
|
|
760
|
+
- `subscribe {sessions: [ids] | ["all"], events?, once?, match?, stalled_minutes?, heartbeat_minutes?}`
|
|
761
|
+
— `all` watches every session, ones created later too. The watcher's own conversation is never
|
|
762
|
+
watched. The first subscribe of a conversation shows the user an approval card naming the
|
|
763
|
+
sessions; a subscription to `all` is always named on a card (asked again after an Allow that did
|
|
764
|
+
not cover it). The Allow is remembered as unlock rows `self:watch` / `self:watch-all`.
|
|
765
|
+
- `unsubscribe {subscription_id | "all"}`, `list_subscriptions {watcher?}` (another conversation's
|
|
766
|
+
subscriptions, to inspect before a transfer).
|
|
767
|
+
- `transfer_subscriptions {from}` — moves every active subscription of another conversation, with
|
|
768
|
+
its settings and its undelivered events, to the caller: a watcher is replaced without a moment
|
|
769
|
+
where events go nowhere. Always an approval card (from where, how many, whether `all` is among
|
|
770
|
+
them), also after the caller's first-subscribe Allow. The new watcher is dropped from moved
|
|
771
|
+
session lists.
|
|
772
|
+
|
|
773
|
+
**Events** (`shared/relay-events.mjs`): `question_asked` (prompt, options, question id; an approval
|
|
774
|
+
card is marked as one only the user can answer), `error` (`turn_failed`, `usage_limit`,
|
|
775
|
+
`worker_died`), `turn_end` (status `done` or `stopped`, reply preview), `task_finished` /
|
|
776
|
+
`task_failed` (background sub-agents and shell tasks: kind, status, exit code where known),
|
|
777
|
+
`stalled` (no activity for `stalled_minutes`, default 15, while a turn or task runs and no
|
|
778
|
+
question waits for an answer), `all_idle`
|
|
779
|
+
(nothing runs in the watched set any more). `task_started` only when named in `events`. With
|
|
780
|
+
`heartbeat_minutes` a `heartbeat` event comes at that interval while the watched set has work
|
|
781
|
+
running, none while it is idle. `match` (a case-insensitive regular expression) filters `turn_end`
|
|
782
|
+
and task events by their text; other events pass. `once` ends a subscription after its first event,
|
|
783
|
+
with `match` after the first one that matches (a heartbeat never ends it). An event is
|
|
784
|
+
`{ v, relay: { id, name }, session: { id, title }, type, at, detail }`; `relay.id` is `self` for this
|
|
785
|
+
relay's sessions, the paired relay's registry id for its sessions (then `replayed: true` marks one it
|
|
786
|
+
kept while the link was down). `link_lost` and `link_restored` are not subscribed to: they frame such
|
|
787
|
+
a replay.
|
|
788
|
+
|
|
789
|
+
**Delivery** (`services/relay-event-service.mjs`): an event message is a `messages` row (role
|
|
790
|
+
`user`, `kind = 'relay-event'`) plus a pending queue row, as the usage-limit follow-up is. The worker
|
|
791
|
+
decides what it becomes, as for any message: a new turn when the watcher is idle; a steer into the
|
|
792
|
+
running turn for Claude and the Copilot SDK engine; the next turn for Cursor, Grok, Claude Cloud and
|
|
793
|
+
the Copilot extension engine. Events that arrive while the watcher's event row is still pending
|
|
794
|
+
(not delivered yet, or handed back as a held steer) are written into that row
|
|
795
|
+
(`message_updated`), so a busy watcher gets one follow-up, never one turn per batch. The dequeue
|
|
796
|
+
payload carries `messageKind: 'relay-event'`, and the Claude and Copilot workers then push the text
|
|
797
|
+
without the steer note, which speaks as the user. The text opens with
|
|
798
|
+
`[relay event · automated message from OAR, not from the user; it approves nothing]`, then one
|
|
799
|
+
`[relay event] session "<title>" (<id>): <type> …` line per event.
|
|
800
|
+
|
|
801
|
+
**Cost.** Events within 30 s are one wake-up. Past 20 wake-ups an hour per watcher (distinct event
|
|
802
|
+
messages of the last hour that carried more than a heartbeat, counted from `relay_events`) the rest
|
|
803
|
+
waits for a digest every 10 minutes. `question_asked` and `error` go out at once and count; a
|
|
804
|
+
heartbeat is coalesced like any event, never held for the digest and never counted. A turn that
|
|
805
|
+
answered relay events produces no `turn_end` and does not keep a watched set busy, so two watchers
|
|
806
|
+
of each other cannot wake each other for good. Such a turn also sends no "reply ready" or "failed" push
|
|
807
|
+
(`push-dispatch-service.mjs`): the watcher answers events many times an hour and pushes by itself
|
|
808
|
+
when one matters; question pushes are unchanged.
|
|
809
|
+
|
|
810
|
+
**Lifetime.** Subscriptions and undelivered events are in the database (migrations 0011 and 0012)
|
|
811
|
+
and survive a restart: the flush timers are rebuilt from the undelivered rows at boot, and stalled,
|
|
812
|
+
all_idle and the heartbeat are swept every 30 s. A subscription ends on unsubscribe (agent or the
|
|
813
|
+
user's Unsubscribe), when its watcher is archived or deleted, when the last session it lists is, or
|
|
814
|
+
after firing (`once`); one on a paired relay also when the pairing is removed. `list_subscriptions`
|
|
815
|
+
shows the ones that ended within the hour with `endReason`. Delivered events are kept for two hours
|
|
816
|
+
(the wake-up count reads the last hour; one of a still-pending event row stays until it goes out)
|
|
817
|
+
and ended subscriptions for a week (a mirror whose end the paired relay has not confirmed stays);
|
|
818
|
+
the sweep prunes both every ten minutes. A `running` sub-agent run whose worker is gone is closed by
|
|
819
|
+
`services/stale-subagent-run-sweep.mjs` (a minute after boot, then every ten minutes), and one not
|
|
820
|
+
touched for a day is not counted as running meanwhile.
|
|
821
|
+
|
|
822
|
+
**Hook points**: `/api/response` finalize (turn_end, or error at the usage limit), `failQueueMessage`
|
|
823
|
+
(error; a Stop is turn_end `stopped`), `failRecoveredRowTerminally`, `recoverDeadWorkerProcessingRows`
|
|
824
|
+
and the background-only worker death (worker_died), `/api/relay-question` and the approval card's
|
|
825
|
+
notify (question_asked), the activity tracker's task edges, and the archive and delete routes.
|
|
826
|
+
|
|
827
|
+
### Across paired relays
|
|
828
|
+
|
|
829
|
+
The watching relay (**A**, it holds the watcher) and the watched one (**B**) each keep a row of the
|
|
830
|
+
subscription. Only A calls, over its pairing: a pairing in one direction is enough.
|
|
831
|
+
|
|
832
|
+
- **Gate.** The four actions with `relay: <paired relay>` pass the same checks as any `remote_relay`
|
|
833
|
+
call there: the relay unlocked in the watcher's conversation (the user named it, or its agent wrote
|
|
834
|
+
there), A's permission for it at least `read`, and B's inbound switch on (B refuses with
|
|
835
|
+
`REMOTE_INBOUND_DISABLED`). `subscribe` first asks B with a dry run (sessions, limits, switch,
|
|
836
|
+
version), then shows the watch card on A naming the relay (and `all`), remembered as
|
|
837
|
+
`<registry id>:watch` / `:watch-all` and forgotten with the pairing, then subscribes on B and stores
|
|
838
|
+
the mirror on A. No card on B. A relay that predates these routes answers Express's plain 404,
|
|
839
|
+
which A reports as `REMOTE_RELAY_UNSUPPORTED` ("runs an OAR version without event subscriptions
|
|
840
|
+
across relays").
|
|
841
|
+
- **B's side** (`relay-event-routes.mjs`): `POST /api/relay-events/remote/subscribe` and
|
|
842
|
+
`POST /api/relay-events/remote/pull` take only another relay's request (the `X-OAR-Remote-Origin`
|
|
843
|
+
header names it; this relay's own agents and the browser get `403`) and only while the inbound
|
|
844
|
+
switch is on. The subscription is an ordinary row whose watcher is the key
|
|
845
|
+
`remote:<that relay's id>`, which names no conversation, so nothing a paired relay asks for is ever
|
|
846
|
+
written into a conversation of B's; its events are recorded, swept (stalled, all_idle, heartbeat)
|
|
847
|
+
and filtered like any, and wait for the fetch. Retention: per subscription the newest 500 undelivered
|
|
848
|
+
events (questions and errors kept longest), none older than 7 days; a subscription not fetched for
|
|
849
|
+
7 days ends (`subscriber-gone`). What was dropped is counted and reported once.
|
|
850
|
+
- **The fetch** (`services/relay-event-link-service.mjs` on A): every 5 s while A holds active
|
|
851
|
+
subscriptions on B or an end B has not confirmed, every 30 s after a minute without an answer,
|
|
852
|
+
nothing once none are left. The body is `{ after, subscriptions: [{ id, watcher }], ended: [ids] }`:
|
|
853
|
+
B deletes what A stored up to `after`, applies the ends, updates the watcher names, and answers
|
|
854
|
+
`{ events, cursor, more, subscriptions: [state], ended, dropped }` (at most 200 events a page).
|
|
855
|
+
A writes the events into its watchers' undelivered events (only for subscriptions it holds, into the
|
|
856
|
+
watcher it stored), ends or updates its mirrors from the states, clears confirmed ends and moves its
|
|
857
|
+
cursor in one transaction, so a restart of either relay loses or repeats nothing. From there
|
|
858
|
+
delivery is the same as for A's own sessions: coalescing, the cap and digest, the steer, the loop
|
|
859
|
+
guard (an event turn on either relay is no `turn_end` for the other).
|
|
860
|
+
- **Gaps.** A link without an answer for 60 s (failing fetches, or A down) is lost from its last
|
|
861
|
+
answer on. The first fetch after that, every page of it, is held and written as one urgent row per
|
|
862
|
+
watcher with subscriptions there: `link_lost` (no answer since …), `Replayed: N events …` with
|
|
863
|
+
questions and errors first, `link_restored` (after how long, how many kept, how many dropped, which
|
|
864
|
+
subscriptions ended there meanwhile). A shorter outage only delays. Nothing wakes the watcher when
|
|
865
|
+
the link drops; `list_subscriptions` shows each paired subscription's `link` (`ok`, `failing`,
|
|
866
|
+
`lost`), and a `heartbeat_minutes` subscription notices the silence.
|
|
867
|
+
- **Ends.** Unsubscribe and an archived watcher end the mirror at once and mark it until B confirms
|
|
868
|
+
(`list_subscriptions` shows `endPending`); `unsubscribe` waits up to 10 s for that and says so when B
|
|
869
|
+
did not answer. An archived session on B, or B's user ending the subscription there, ends the mirror
|
|
870
|
+
with the next fetch (`archived`, `ended-on-relay`). Removing the pairing sends one last fetch with
|
|
871
|
+
every subscription as ended, then ends the mirrors (`pairing-removed`).
|
|
872
|
+
- `unsubscribe "all"` and `transfer_subscriptions` act on the named relay's subscriptions; with `this`,
|
|
873
|
+
`list_subscriptions` lists all of the watcher's (each with `relay`) and `transfer_subscriptions`
|
|
874
|
+
moves all, its card naming the paired relays among them. A transfer reaches B as the new watcher's
|
|
875
|
+
name with the next fetch.
|
|
876
|
+
|
|
877
|
+
`/api/remote-relays/tool` refuses a call that carries another relay's origin: a paired relay never
|
|
878
|
+
acts, or subscribes, for one of this relay's conversations.
|
|
879
|
+
|
|
880
|
+
### Session state and tasks
|
|
881
|
+
|
|
882
|
+
`services/session-activity-tracker.mjs` knows what each session is doing, from the
|
|
883
|
+
`/api/background-tasks` sets (a new task id is a start, a task gone from the set has ended, a
|
|
884
|
+
changed one is activity), `POST /api/background-task-settled` (how a task ended; the Claude worker
|
|
885
|
+
posts it from `task_notification`), the sub-agent run statuses and the activity, thought, stream and
|
|
886
|
+
timeline posts (the activity clock). A task that leaves the set without word ends after 5 s with
|
|
887
|
+
its run's status, else `completed` with `statusReported: false`; an exit code is read from the task
|
|
888
|
+
summary when it names one (the SDK has no field for it). Memory only: a restart forgets the recently
|
|
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.
|
|
895
|
+
|
|
896
|
+
`GET /api/conversations` and `GET /api/conversation/:id` carry `sessionState` (`running_turn`: a
|
|
897
|
+
turn processing, or queued and not held by a usage-limit pause; `background_only`: tasks running;
|
|
898
|
+
`idle`), `sessionTasks` (running tasks, then up to five that ended within the hour: `kind`
|
|
899
|
+
`subagent` | `shell`, `description`, `startedAt`, `lastActivityAt`, `status` `running` |
|
|
900
|
+
`completed` | `failed` | `stopped`, `exitCode`, `summary`, `endedAt`) and `lastActivityAt`.
|
|
901
|
+
`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.
|
|
903
|
+
|
|
904
|
+
### Browser
|
|
905
|
+
|
|
906
|
+
An event message renders as a compact row (`relay-event-view.mjs`) folded to its summary
|
|
907
|
+
(`2 events · question, reply`); a row with a question or an error starts open, a replay carries a
|
|
908
|
+
**replayed** tag, and the user's fold is kept across re-renders. A watcher's header shows **👁 watching
|
|
909
|
+
N sessions** (or all; sessions on paired relays count, the chip's title names those relays), which
|
|
910
|
+
opens its subscriptions with an **Unsubscribe** button each (`lab-relay · Report builder`); a watched
|
|
911
|
+
session shows a **watched** badge in its header and, when watched by id, in the sidebar (a
|
|
912
|
+
subscription to all would badge every row; one on a paired relay badges none of this relay's
|
|
913
|
+
sessions). On the watched relay the badge names a paired relay's watcher with its relay.
|
|
914
|
+
The list comes from `GET /api/relay-events/subscriptions` and the `relay_event_subscriptions`
|
|
915
|
+
socket event (the whole active list after every change).
|
|
916
|
+
|
|
917
|
+
### Folded agent exchanges
|
|
918
|
+
|
|
919
|
+
A prompt another agent sent the session (a paired relay's, or one of this relay's own: a user row
|
|
920
|
+
with `origin`) folds into one row, `note from <relay> · <session> · answered below` (`working…`,
|
|
921
|
+
`failed`, `stopped`, `no reply yet`), which opens to the prompt as it renders otherwise.
|
|
922
|
+
Presentation only (`agent-exchange-fold-view.mjs` decides, `syncAgentExchangeFolds` in
|
|
923
|
+
`conversation-view.js` applies it as one more idempotent pass after every render and update, beside
|
|
924
|
+
the separator and steering passes); the transcript data is unchanged. Nothing addressed to the user
|
|
925
|
+
folds away: the fold covers the agent's prompt alone. The session's reply to it can hold a line for
|
|
926
|
+
the user, so the reply, the live bubble of the running turn and the turn's question, approval and
|
|
927
|
+
plan cards always show, as for a message of the user's own; they are read for the row's status
|
|
928
|
+
only. One prompt is left unfolded: an agent's prompt that was itself steered into a running turn,
|
|
929
|
+
which renders as part of that turn's flow. A folded prompt shows open while its turn is `working…`
|
|
930
|
+
and closes by itself once it is replied, failed or stopped (`agentExchangeFoldOpen`); the user's
|
|
931
|
+
own toggle, kept by prompt id, always wins over that, and a jump to the prompt (search, pins, image
|
|
932
|
+
parent) opens it.
|
|
933
|
+
|
|
633
934
|
## Image conversations (OpenAI BYOK)
|
|
634
935
|
|
|
635
936
|
Choosing **OpenAI Image (BYOK)** in New Chat creates a conversation whose replies are generated
|
|
@@ -657,6 +958,24 @@ images, calling the OpenAI Images API directly rather than going through a CLI w
|
|
|
657
958
|
- The control appears on message hover; already-hidden messages keep their label visible so the state
|
|
658
959
|
is never silently lost.
|
|
659
960
|
- `POST /api/shared/:token/presence` tracks shared-viewer presence, surfaced as a watcher count.
|
|
961
|
+
`sharedPresenceMaxPerConversation` and `sharedPresenceMaxGlobal` cap how many viewers that count
|
|
962
|
+
tracks; they do not limit who can read a link.
|
|
963
|
+
- A revoked or renewed token answers a plain 404 on every shared route, the `/shared/:token` page
|
|
964
|
+
included, exactly like a token that never existed. An open shared page stops at its next poll.
|
|
965
|
+
- Settings → Sharing lists the active shares (`GET /api/shares`) with **Copy link**, **Renew link**
|
|
966
|
+
and **Stop sharing**, and each share's access log. Renewing adds a token to the share's lineage
|
|
967
|
+
(`conversation_shares.lineage_id`, migration 0016) and revokes the old one, so the log, keyed to
|
|
968
|
+
the lineage, carries over. Stopping a share or deleting the conversation deletes its log; nothing
|
|
969
|
+
else prunes it.
|
|
970
|
+
- The access log (`conversation_share_opens`) has one row per open, not per poll: the shared page's
|
|
971
|
+
first fetch carries `open=1` and counts unless the same viewer opened it in the last 10 s; a
|
|
972
|
+
fetch without the flag counts only when that viewer has not fetched the share for 30 minutes
|
|
973
|
+
(a page back from the background, a script). A row keeps the time, the IP address (unless
|
|
974
|
+
**Store viewer IP addresses** is off), the user agent, the country from `cf-ipcountry` (only when
|
|
975
|
+
the request came through a proxy the `trustProxy` setting trusts) and a viewer key: an HMAC of
|
|
976
|
+
address and user agent under a per-relay secret, which counts distinct viewers without the
|
|
977
|
+
address. The address is the one Express derives through `trustProxy`; a forwarded header from an
|
|
978
|
+
untrusted peer is ignored.
|
|
660
979
|
|
|
661
980
|
## Pinned messages
|
|
662
981
|
|
|
@@ -715,7 +1034,7 @@ When a newer version is available, Settings shows a dismissible-per-version
|
|
|
715
1034
|
card (not dismissible if the release is marked critical — it still never
|
|
716
1035
|
auto-applies). On npm-global installs, **Update** runs
|
|
717
1036
|
`npm i -g @oar-sh/oar@<version>` with the log streamed into the card, then the
|
|
718
|
-
relay restarts once the
|
|
1037
|
+
relay restarts once the running turns finish (queued messages wait for it). After the restart the relay
|
|
719
1038
|
verifies the running version actually changed; if not, the npm log is surfaced
|
|
720
1039
|
as a failure. Git checkouts are detect-only — the card shows a "pull to
|
|
721
1040
|
update" hint instead of the button. `oar update [--beta] [--to X.Y.Z]` does
|
|
@@ -934,6 +1253,12 @@ Queue metrics include `parkedCount` for turns deferred behind restart/rebind gat
|
|
|
934
1253
|
| GET | `/api/drives/roots` | Return browsable root(s) for explorer drive mode — on Windows returns fixed/removable drive letters; on Linux returns a single `/` root node |
|
|
935
1254
|
| GET | `/api/drives/list` | Lazy-load directory entries (`path`, optional `includeHidden`) — on Windows `path` is a drive-letter path (e.g. `C:/foo`); on Linux `path` is an absolute POSIX path (e.g. `/home/user`) |
|
|
936
1255
|
| GET | `/api/session-root/list` | List the explorer's Session root (`path`, optional `includeHidden`) — like `/api/drives/list`, but a not-yet-created root returns an empty folder (`exists: false`) instead of 404, and the sibling `<path>.jsonl` transcript is appended as a child |
|
|
1256
|
+
| POST | `/api/repo/mkdir` | Create a folder in the workspace: `{ path, name, conversationId? }` where `path` is the repo-relative parent (`''` for the root); the target is resolved like `/api/files/*` (no `..`, no symlink escape). Answers `{ ok, path }`; 409 when the name is taken |
|
|
1257
|
+
| POST | `/api/repo/rename` | Rename a workspace entry in place: `{ path, newName, conversationId? }`. The root cannot be renamed; symlinks are refused. Answers `{ ok, path }` |
|
|
1258
|
+
| POST | `/api/repo/delete` | Delete a workspace entry, folders recursively: `{ path, conversationId?, dryRun? }`. `dryRun: true` answers `{ ok, kind, files, dirs, capped }` (counted below the folder, capped at 10 000) and removes nothing |
|
|
1259
|
+
| POST | `/api/drives/mkdir` | As `/api/repo/mkdir` for an absolute path (also the explorer's Session root), checked like `/api/drives/list`: `{ path, name }`. Missing parents are created with the folder |
|
|
1260
|
+
| POST | `/api/drives/rename` | As `/api/repo/rename` for an absolute path: `{ path, newName }`. `/` and drive roots are refused |
|
|
1261
|
+
| POST | `/api/drives/delete` | As `/api/repo/delete` for an absolute path: `{ path, dryRun? }`. `/` and drive roots are refused |
|
|
937
1262
|
| GET | `/api/drives/file` | Stream a file by path — Windows drive path or Linux absolute path depending on server platform |
|
|
938
1263
|
| GET | `/api/drives/files-preview` | Return structured preview JSON for a file — Windows drive path or Linux absolute path depending on server platform |
|
|
939
1264
|
| GET | `/api/git/status` | Git status for the conversation's workspace root (`branch`, `upstream`, `ahead`, `behind`, `files[]` incl. untracked); a non-repo root returns `isRepo: false` rather than an error |
|
|
@@ -959,14 +1284,15 @@ Queue metrics include `parkedCount` for turns deferred behind restart/rebind gat
|
|
|
959
1284
|
| POST | `/api/stream` | (CLI) Push in-flight assistant text stream for current pending message |
|
|
960
1285
|
| POST | `/api/relay/pause` | Pause dequeueing and drop currently queued messages |
|
|
961
1286
|
| POST | `/api/relay/resume` | Resume dequeueing after pause |
|
|
962
|
-
| POST | `/api/relay/shutdown` | Queue
|
|
1287
|
+
| POST | `/api/relay/shutdown` | Queue an authenticated relay shutdown or self-restart |
|
|
963
1288
|
| GET | `/api/relay-questions` | (CLI/UI) List relay questions by `status` (for example `pending` or `answered`) |
|
|
964
1289
|
| GET | `/api/relay-question/:id` | (CLI) Fetch a single relay question |
|
|
965
1290
|
| POST | `/api/relay-question` | (CLI) Create a relay question for the browser |
|
|
966
1291
|
| POST | `/api/relay-question/:id/answer` | (UI) Submit an answer for a relay question |
|
|
967
1292
|
| POST | `/api/relay-question/:id/timeout` | (CLI/UI) Mark a relay question timed out |
|
|
968
|
-
| POST | `/api/heartbeat` | (CLI) Keep CLI status alive |
|
|
1293
|
+
| 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 |
|
|
969
1294
|
| GET | `/api/status` | Overall status |
|
|
1295
|
+
| 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 |
|
|
970
1296
|
| GET | `/api/models` | Live/cached model catalog used by the UI picker |
|
|
971
1297
|
| POST | `/api/models/snapshot` | (CLI/relay) Publish discovered model snapshot |
|
|
972
1298
|
| POST | `/api/conversation/:id/refresh-history` | Re-import an existing conversation's history through the local Copilot SDK |
|
|
@@ -983,18 +1309,27 @@ Queue metrics include `parkedCount` for turns deferred behind restart/rebind gat
|
|
|
983
1309
|
| POST | `/api/settings/cursor` | Set/remove the Cursor API key, default model, or enabled model subset; key changes reset discovered models and effort tiers |
|
|
984
1310
|
| GET | `/api/settings/turn-ceiling` | Read the max turn duration plus slider bounds (`minMinutes`, `maxMinutes`, `stepMinutes`, `defaultMinutes`) |
|
|
985
1311
|
| POST | `/api/settings/turn-ceiling` | Set the max turn duration in minutes (`0` = no limit) |
|
|
1312
|
+
| 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
|
+
| POST | `/api/settings/subagent-record-retention` | `{ retentionDays }` → the GET body; anything but one of `choices` is `400`. Runs a retention sweep 5 s later |
|
|
986
1314
|
| POST | `/api/claude-native-session` | (Claude worker) Persist the native Agent SDK session id for resume |
|
|
987
1315
|
| POST | `/api/claude-context-usage` | (Claude and Claude Cloud workers) Report the session's context-window breakdown after a turn; `409` for a conversation of any other provider |
|
|
988
1316
|
| POST | `/api/claude-plan-usage` | (Claude and Claude Cloud workers) Report the session's structured `/usage` data — plan rate-limit windows plus session cost totals — falling back to the stable `modelUsage`/`totalCostUsd` result fields. Stored as the latest Claude snapshot; a Claude Cloud conversation's report is stored under its own snapshot key, `claude-cloud` |
|
|
989
1317
|
| POST | `/api/claude-cloud-session` | (Claude Cloud worker) Report the cloud session a conversation is bound to: `{ conversationId, cloudSessionId, sessionUrl?, lastSequence, pushedBranch?, costUsd?, model? }` → `{ ok, cloud }`. Sent after the session was created, after every `result` and on a push; emits `claude_cloud_session`. `404` without a runtime session, `409` for a conversation that is not a cloud one |
|
|
990
1318
|
| POST | `/api/cursor-plan-usage` | (Cursor worker) Report the agent's cumulative billed usage; the relay diffs it against a per-agent checkpoint and books the increase into the current billing cycle under the pool implied by the turn's model |
|
|
991
|
-
| POST | `/api/subagent-run` | (Worker) Register/update a subagent run
|
|
1319
|
+
| POST | `/api/subagent-run` | (Worker) Register/update a subagent run. A new run needs the spawning turn's `messageId`; an update to a known run may omit it (a background agent's end arrives between turns). Optional header fields: `subagentType`, `model`, `modelInherited`, `taskId`, `background`, `timelineOwner`; a post without `status` leaves the run's state alone. Emits `subagent_status` with the whole header |
|
|
1320
|
+
| POST | `/api/subagent-timeline` | (Worker) `{ conversationId, subagentRunId, entries: [{ key, kind, text, done?, toolName?, toolUseId?, metadata? }], header? }` → `{ ok, stored, truncated }`. Kinds: `thought`, `tool_call`, `tool_result`, `text`, `child`, `note`. Fenced by the run row, not by an open turn; an unknown run whose `header.parentSubagentId` is known is created under the parent's turn, anything else is `404 unknown_run`. Sends `subagent_timeline` `{ conversationId, messageId, subagentRunId, entries, truncated }`, volatile, only to the pages watching the run (`subagent_viewer_watch`) |
|
|
1321
|
+
| GET | `/api/conversation/:conversationId/subagent/:subagentRunId` | One run's record for the viewer: `?after=<seq>&limit=<n>` (default 500, max 2000) → `{ run, entries, children, nextAfter, complete }`. `entries` are those after `after` plus any still open; `run.legacy` marks a record rebuilt from the lane rows |
|
|
992
1322
|
| POST | `/api/conversation/:conversationId/subagent/:subagentRunId/cancel` | Request cancellation of one subagent run (unsupported by the Claude runtime) |
|
|
993
1323
|
| PATCH | `/api/conversation/:id/message/:messageId/share-visibility` | Hide or unhide a single message from shared views |
|
|
994
1324
|
| 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` |
|
|
995
|
-
| POST | `/api/conversation/:id/share` |
|
|
996
|
-
|
|
|
997
|
-
|
|
|
1325
|
+
| 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` |
|
|
1326
|
+
| 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 |
|
|
1327
|
+
| 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 |
|
|
1328
|
+
| GET | `/api/shares` | Every active share: `{ shares: [{ conversationId, title, archived, token, shareUrl, path, sharedSince, renewedAt, lastOpenedAt, openCount, viewerCount }], storeViewerIp, accessLog }` |
|
|
1329
|
+
| 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 }` |
|
|
1330
|
+
| 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` |
|
|
1331
|
+
| 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 |
|
|
1332
|
+
| POST | `/api/shared/:token/presence` | (No auth) Report shared-viewer presence for the watcher count |
|
|
998
1333
|
| POST | `/api/openai/images/generate` | Generate images for an OpenAI Image conversation |
|
|
999
1334
|
| POST | `/api/image-operations/:operationId/execute` | Execute a queued image generation/edit operation |
|
|
1000
1335
|
| GET | `/api/generated-image/:conversationId/:messageId/:imageId/content` | Stream a generated image blob |
|
|
@@ -1007,6 +1342,11 @@ Queue metrics include `parkedCount` for turns deferred behind restart/rebind gat
|
|
|
1007
1342
|
| POST | `/api/remote-relays/pair` | (Relay to relay) Another relay introduces itself after the user paired it there |
|
|
1008
1343
|
| 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` |
|
|
1009
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 |
|
|
1345
|
+
| GET | `/api/relay-events/subscriptions` | Every active [relay event subscription](#relay-event-subscriptions) (`{ available, subscriptions }`); refused to other relays' agents |
|
|
1346
|
+
| 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
|
+
| 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) |
|
|
1348
|
+
| 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 }` |
|
|
1349
|
+
| 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) |
|
|
1010
1350
|
| GET | `/api/remote-relays/inflight` | (Worker) Number of `remote_relay` calls running for `conversationId` |
|
|
1011
1351
|
| GET/POST | `/api/settings/remote-relays` | This relay's `publicUrl` and `inboundEnabled` (accept prompts from other relays' agents) |
|
|
1012
1352
|
| 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 |
|
|
@@ -1014,7 +1354,8 @@ Queue metrics include `parkedCount` for turns deferred behind restart/rebind gat
|
|
|
1014
1354
|
|
|
1015
1355
|
### Manual relay shutdown / self-restart
|
|
1016
1356
|
|
|
1017
|
-
- `POST /api/relay/shutdown`
|
|
1357
|
+
- `POST /api/relay/shutdown`, `POST /api/relay/shutdown/cancel` and `POST /api/queue/empty` need the
|
|
1358
|
+
token and nothing else: the browser app calls them from a phone, over a tunnel and over the LAN.
|
|
1018
1359
|
- The request body accepts:
|
|
1019
1360
|
- `reason` (optional string)
|
|
1020
1361
|
- `requestedBy` (optional string)
|
|
@@ -1035,7 +1376,7 @@ Queue metrics include `parkedCount` for turns deferred behind restart/rebind gat
|
|
|
1035
1376
|
- `restart`: boolean mirror of the action
|
|
1036
1377
|
- `requestedAt`, `reason`, `requestedBy`
|
|
1037
1378
|
- `queue`: current `pendingCount` / `processingCount` / `parkedCount`
|
|
1038
|
-
- The relay waits until
|
|
1379
|
+
- The relay waits until no turn is processing before acting; this API is not an interrupt or cancel mechanism. While the request is queued no pending message is handed to a worker (a turn started meanwhile would postpone the restart again); pending and parked rows stay in the database and run after the restart.
|
|
1039
1380
|
- Ownership after an intentional restart follows the argv role, so exactly one supervisor acts on exit 75:
|
|
1040
1381
|
- `server.js --supervised` exits 75 and relaunches under `.github/extensions/web-relay/server-lifecycle/managed-server.mjs`
|
|
1041
1382
|
- bare `node server/server.js` keeps its supervisor attached, respawns the `--relay-runtime` child, and keeps the same terminal session alive
|
|
@@ -1402,7 +1743,8 @@ service).
|
|
|
1402
1743
|
| `cloudflaredTunnel.token` | — | Tunnel token from the router panel (never logged) |
|
|
1403
1744
|
| `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 |
|
|
1404
1745
|
| `cloudflaredTunnel.extraArgs` | `[]` | Extra arguments appended to `cloudflared tunnel run` |
|
|
1405
|
-
| `tunnelMarkerHeaders` | `[]` | Extra edge-injected header names that mark tunnel traffic
|
|
1746
|
+
| `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) |
|
|
1747
|
+
| `allowedOrigins` | `[]` | Extra browser origins the Socket.IO channel accepts besides the relay's own origin |
|
|
1406
1748
|
|
|
1407
1749
|
### SDK auto-detection behavior
|
|
1408
1750
|
|
|
@@ -1584,6 +1926,17 @@ created stores an origin with `local: true` that names the conversation it was c
|
|
|
1584
1926
|
the user's **Allow** for a conversation's agent to start sessions here is a row of that table
|
|
1585
1927
|
under the relay id `self` (`remote-relay-repository.mjs`).
|
|
1586
1928
|
|
|
1929
|
+
`0011-relay-event-subscriptions.mjs` adds `relay_event_subscriptions` (what each watcher asked for,
|
|
1930
|
+
kept after it ended with `ended_at` / `end_reason`, for a week) and `relay_events` (events per watcher, with
|
|
1931
|
+
`delivered_at` and the message they went into, kept for two hours once delivered). See
|
|
1932
|
+
[Relay event subscriptions](#relay-event-subscriptions).
|
|
1933
|
+
|
|
1934
|
+
`0012-cross-relay-event-subscriptions.mjs` adds the columns both sides of a subscription across
|
|
1935
|
+
relays need (on the watching relay `remote_subscription_id`, `remote_meta_json`, `remote_sync`; on
|
|
1936
|
+
the watched one `subscriber_relay_id`, `subscriber_relay_name`, `subscriber_watcher_id`,
|
|
1937
|
+
`subscriber_watcher_title`, `last_pull_at`, `dropped_count`) and `relay_event_links` (per paired
|
|
1938
|
+
relay the fetch cursor and the link's health). See [Across paired relays](#across-paired-relays).
|
|
1939
|
+
|
|
1587
1940
|
`0007-claude-cloud.mjs` adds the storage of [Claude Cloud](#claude-cloud-provider) conversations.
|
|
1588
1941
|
It is additive and idempotent, runs on every boot, and a failure is logged and retried on the next
|
|
1589
1942
|
one:
|
|
@@ -1634,6 +1987,9 @@ working with the corresponding feature inert rather than crashing at startup.
|
|
|
1634
1987
|
| `services/claude-cloud-session-service.mjs` | What the relay keeps per cloud conversation: worker reports, the `cloud` payload field, archiving |
|
|
1635
1988
|
| `services/claude-account-usage-service.mjs` | Live Claude account usage for Check Usage (only while Claude Cloud is on) |
|
|
1636
1989
|
| `services/git-remote-service.mjs` | GitHub remote and branch of a folder, for the New Chat modal |
|
|
1990
|
+
| `services/relay-event-service.mjs` | Relay event subscriptions: subscribe / transfer, recording, coalescing, the hourly cap and digest, delivery into the watcher's conversation, stalled / all_idle / heartbeat sweeps |
|
|
1991
|
+
| `services/session-activity-tracker.mjs` | What each session is doing: `state`, tasks, task edges and the activity clock |
|
|
1992
|
+
| `../shared/relay-events.mjs` | Relay event types, limits and the text of an event message |
|
|
1637
1993
|
| `services/remote-relay-loopback.mjs` | The `remote_relay` client for this relay itself (agent sessions): hands the dispatcher's requests to the relay's own HTTP handler in-process instead of opening a connection |
|
|
1638
1994
|
| `services/claude-session-root-service.mjs` | Resolves the browsable session folder for Claude conversations |
|
|
1639
1995
|
| `services/context-usage-view.mjs` | Normalizes Copilot and Claude context data into one payload |
|
|
@@ -71,6 +71,16 @@ export const REMOTE_RELAY_TOOL_ZOD_SHAPE = {
|
|
|
71
71
|
answer: text('answer'),
|
|
72
72
|
choices: z.array(z.string()).optional().describe(FIELDS.choices.description),
|
|
73
73
|
if_busy: choice('if_busy', REMOTE_RELAY_IF_BUSY),
|
|
74
|
+
sessions: z.array(z.string()).optional().describe(FIELDS.sessions.description),
|
|
75
|
+
// Free text like effort: the relay checks the names and says which exist.
|
|
76
|
+
events: z.array(z.string()).optional().describe(FIELDS.events.description),
|
|
77
|
+
once: z.boolean().optional().describe(FIELDS.once.description),
|
|
78
|
+
match: text('match'),
|
|
79
|
+
stalled_minutes: number('stalled_minutes'),
|
|
80
|
+
heartbeat_minutes: number('heartbeat_minutes'),
|
|
81
|
+
subscription_id: text('subscription_id'),
|
|
82
|
+
watcher: text('watcher'),
|
|
83
|
+
from: text('from'),
|
|
74
84
|
};
|
|
75
85
|
|
|
76
86
|
export function createRemoteRelayToolDefinition({ api, getConversationId = () => '', dbg = () => {} } = {}) {
|