@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.
Files changed (135) hide show
  1. package/CHANGELOG.md +440 -1
  2. package/README.md +156 -972
  3. package/package.json +13 -13
  4. package/server/README.md +112 -25
  5. package/server/claude-worker/claude-remote-relay-tool.mjs +2 -0
  6. package/server/claude-worker/claude-sdk-adapter.mjs +57 -4
  7. package/server/claude-worker/claude-session-process.mjs +217 -11
  8. package/server/claude-worker/claude-session-worker.mjs +26 -1
  9. package/server/claude-worker/task-output-discovery.mjs +126 -0
  10. package/server/copilot-worker/copilot-sdk-session-process.mjs +235 -14
  11. package/server/copilot-worker/copilot-sdk-session-worker.mjs +8 -0
  12. package/server/copilot-worker/copilot-shell-output-mirror.mjs +94 -0
  13. package/server/copilot-worker/copilot-task-settle.mjs +96 -0
  14. package/server/db-schema.mjs +68 -2
  15. package/server/grok-worker/acp-client.mjs +2 -1
  16. package/server/grok-worker/acp-host-services.mjs +46 -6
  17. package/server/grok-worker/grok-background-tasks.mjs +241 -0
  18. package/server/grok-worker/grok-sdk-adapter.mjs +3 -1
  19. package/server/grok-worker/grok-session-worker.mjs +33 -1
  20. package/server/grok-worker/grok-turn-runner.mjs +11 -0
  21. package/server/migrations/0013-previews.mjs +130 -0
  22. package/server/migrations/0014-conversation-groups.mjs +89 -0
  23. package/server/migrations/0015-conversation-group-position.mjs +76 -0
  24. package/server/migrations/0016-share-access-log.mjs +110 -0
  25. package/server/migrations/0017-conversation-history-indexes.mjs +89 -0
  26. package/server/public/app/about-ui.js +73 -0
  27. package/server/public/app/about-view.mjs +234 -0
  28. package/server/public/app/action-confirmations.js +31 -14
  29. package/server/public/app/agent-exchange-fold-view.mjs +40 -101
  30. package/server/public/app/api-client.js +264 -16
  31. package/server/public/app/ask-user-view.js +17 -15
  32. package/server/public/app/background-tasks-view.mjs +285 -16
  33. package/server/public/app/bootstrap.js +253 -37
  34. package/server/public/app/chat-actions-menu-state.mjs +20 -10
  35. package/server/public/app/claude-background-command-limit-options.mjs +21 -0
  36. package/server/public/app/context-menu.mjs +90 -6
  37. package/server/public/app/conversation-group-drag.mjs +191 -0
  38. package/server/public/app/conversation-group-order.mjs +265 -0
  39. package/server/public/app/conversation-group-state.mjs +100 -0
  40. package/server/public/app/conversation-list-drag.mjs +449 -0
  41. package/server/public/app/conversation-payload-cache.mjs +357 -0
  42. package/server/public/app/conversation-turn-state.mjs +81 -0
  43. package/server/public/app/conversation-view.js +567 -94
  44. package/server/public/app/conversation-window-retention.mjs +96 -0
  45. package/server/public/app/cwd-picker.js +1 -1
  46. package/server/public/app/hidden-messages-view.mjs +312 -0
  47. package/server/public/app/host-suspend-ui.js +7 -1
  48. package/server/public/app/journal-view.js +1033 -82
  49. package/server/public/app/preview-cards.mjs +117 -31
  50. package/server/public/app/preview-command.mjs +10 -1
  51. package/server/public/app/relay-board-view.js +19 -6
  52. package/server/public/app/relay-prompt-sanitizer.mjs +11 -4
  53. package/server/public/app/relay-restart-error.mjs +16 -0
  54. package/server/public/app/settings-modal.js +150 -10
  55. package/server/public/app/settings-tabs.js +1 -1
  56. package/server/public/app/share-format.mjs +49 -0
  57. package/server/public/app/share-preview-marks.mjs +167 -0
  58. package/server/public/app/share-state.js +109 -0
  59. package/server/public/app/shared-conversations-settings.js +480 -0
  60. package/server/public/app/socket-handlers.js +138 -20
  61. package/server/public/app/store.js +121 -30
  62. package/server/public/app/subagent-row.mjs +3 -3
  63. package/server/public/app/subagent-viewer.mjs +37 -7
  64. package/server/public/app/task-console-render.mjs +237 -0
  65. package/server/public/app/task-console-watch.mjs +42 -0
  66. package/server/public/app/task-console.mjs +630 -0
  67. package/server/public/app/task-output-text.mjs +234 -0
  68. package/server/public/app/thinking-follow.js +0 -4
  69. package/server/public/app/transcript-island.mjs +73 -0
  70. package/server/public/app/transcript-jump-bottom-view.mjs +230 -26
  71. package/server/public/app/transcript-jump-bottom.mjs +170 -22
  72. package/server/public/app/transcript-reading-anchor.mjs +256 -0
  73. package/server/public/app/transcript-reload-freshness.mjs +17 -1
  74. package/server/public/app/transcript-viewport-anchor.mjs +159 -0
  75. package/server/public/index.html +625 -21
  76. package/server/relay-tools.md +3 -1
  77. package/server/relay.mjs +2 -1
  78. package/server/repositories/conversation-group-repository.mjs +444 -0
  79. package/server/repositories/message-repository.mjs +9 -0
  80. package/server/repositories/question-repository.mjs +4 -0
  81. package/server/repositories/session-repository.mjs +12 -37
  82. package/server/repositories/share-repository.mjs +322 -0
  83. package/server/repositories/share-visibility-statements.mjs +10 -0
  84. package/server/routes/about-routes.mjs +15 -0
  85. package/server/routes/cache-routes.mjs +4 -0
  86. package/server/routes/conversation-group-routes.mjs +171 -0
  87. package/server/routes/messages-routes.mjs +433 -68
  88. package/server/routes/preview-routes.mjs +9 -1
  89. package/server/routes/sessions-routes.mjs +901 -253
  90. package/server/server-runtime.mjs +392 -99
  91. package/server/services/about-service.mjs +461 -0
  92. package/server/services/background-task-store.mjs +365 -0
  93. package/server/services/background-work-view.mjs +228 -0
  94. package/server/services/cache-rebuild-service.mjs +30 -0
  95. package/server/services/cli-process-runner.mjs +4 -21
  96. package/server/services/conversation-history-page.mjs +199 -0
  97. package/server/services/delete-archive-service.mjs +4 -1
  98. package/server/services/detached-worker-watch-service.mjs +150 -0
  99. package/server/services/hidden-message-list.mjs +21 -0
  100. package/server/services/host-suspend-command.mjs +2 -1
  101. package/server/services/message-pin-list.mjs +1 -1
  102. package/server/services/preview-health-probe.mjs +8 -1
  103. package/server/services/preview-proxy-server.mjs +8 -4
  104. package/server/services/preview-registry-service.mjs +676 -26
  105. package/server/services/preview-static-handler.mjs +40 -7
  106. package/server/services/preview-store.mjs +274 -0
  107. package/server/services/rate-limit-service.mjs +12 -1
  108. package/server/services/relay-auth.mjs +183 -0
  109. package/server/services/relay-cli-launcher-service.mjs +3 -1
  110. package/server/services/relay-event-service.mjs +17 -0
  111. package/server/services/relay-prompt-sanitizer.mjs +11 -4
  112. package/server/services/remote-relay-dispatcher.mjs +187 -7
  113. package/server/services/request-boundary.mjs +183 -0
  114. package/server/services/session-activity-tracker.mjs +231 -19
  115. package/server/services/session-worker-launch-service.mjs +190 -6
  116. package/server/services/session-worker-process-service.mjs +10 -6
  117. package/server/services/share-access-log-service.mjs +123 -0
  118. package/server/services/share-path-redaction.mjs +424 -0
  119. package/server/services/status-event-service.mjs +7 -3
  120. package/server/services/task-output-rooms.mjs +259 -0
  121. package/server/services/task-output-service.mjs +562 -0
  122. package/server/services/tunnel-worker-path-guard.mjs +24 -41
  123. package/server/services/windows-autostart-service.mjs +6 -1
  124. package/server/services/windows-boot-autostart-service.mjs +207 -23
  125. package/shared/claude-background-command-limit.mjs +83 -0
  126. package/shared/preview-tool-core.mjs +15 -5
  127. package/shared/remote-relay-contract.mjs +35 -3
  128. package/shared/share-path-privacy.mjs +30 -0
  129. package/shared/task-output-text.mjs +737 -0
  130. package/shared/windows-system-tools.mjs +106 -0
  131. package/shared/worker-runtime/background-task-digest.mjs +30 -0
  132. package/shared/worker-runtime/heartbeat.mjs +24 -0
  133. package/shared/worker-runtime/kept-posts.mjs +83 -0
  134. package/shared/worker-runtime/task-output-spool.mjs +181 -0
  135. 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.10",
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": "^0.3.292",
54
- "@cursor/sdk": "^1.0.36",
55
- "@github/copilot-sdk": "^1.0.17",
56
- "better-sqlite3": "^12.10.0",
57
- "express": "^4.18.2",
58
- "qrcode-terminal": "^0.12.0",
59
- "socket.io": "^4.7.5",
60
- "uuid": "^14.0.0",
61
- "web-push": "^3.6.7",
62
- "ws": "^8.18.0",
63
- "zod": "^4.4.3"
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": "^1.0.4"
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. A queued **🌄 Restart web relay**
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
- (localhost-only). Set `OAR_HOST_SUSPEND_DRY_RUN=1` to log instead of sleeping.
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 [README](../README.md#claude-cloud); the tracker is
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`) and the session's reply to it fold into one row, `note from <relay> · <session> ·
875
- replied` (`working…`, `failed`, `stopped`, `no reply yet`), which opens to both as they render
876
- otherwise. Presentation only (`agent-exchange-fold-view.mjs` decides, `syncAgentExchangeFolds` in
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: not a turn a message was steered into (the reply `absorbed`, or another message folded
880
- into or stopped with it), not an agent prompt that was itself steered into a turn, not an answer on
881
- a continuation row, and, while the page lives, not a turn that had an open question, approval card
882
- or plan board, or ran while a message of the user's waited. A folded exchange shows open while its
883
- turn is `working…` and closes by itself once it is replied, failed or stopped
884
- (`agentExchangeFoldOpen`); the user's own toggle, kept by prompt id, always wins over that, and a
885
- jump to a message inside (search, pins, image parent) opens it.
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 a localhost-only authenticated relay shutdown or self-restart |
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 or the enabled model subset; triggers discovery and unstarted-conversation reconciliation |
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` | Create or update the conversation's read-only share token |
1260
- | GET | `/api/shared/:token` | Read-only shared conversation view (hidden messages filtered out) |
1261
- | POST | `/api/shared/:token/presence` | Report shared-viewer presence for the watcher count |
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` is localhost-only even when the relay is otherwise reachable on the network.
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 for the session-worker path guard |
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
- export function systemPromptForRelayMode(relayMode) {
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
- const append = [MEDIA_EMBED_INSTRUCTION_TEXT, MODE_SYSTEM_PROMPT_APPEND[mode]]
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 } : {}),