@oar-sh/oar 0.9.5 → 0.9.7

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 (119) hide show
  1. package/.github/extensions/web-relay/polling/polling-loop.mjs +12 -32
  2. package/.github/extensions/web-relay/skills/question-bridge.mjs +14 -39
  3. package/CHANGELOG.md +340 -0
  4. package/README.md +196 -26
  5. package/package.json +2 -1
  6. package/server/README.md +174 -7
  7. package/server/claude-cloud-worker/claude-cloud-event-normalizer.mjs +340 -0
  8. package/server/claude-cloud-worker/claude-cloud-session-process.mjs +1325 -0
  9. package/server/claude-cloud-worker/claude-cloud-session-worker.mjs +155 -0
  10. package/server/claude-worker/claude-remote-relay-tool.mjs +2 -0
  11. package/server/claude-worker/claude-sdk-adapter.mjs +18 -2
  12. package/server/claude-worker/claude-session-process.mjs +151 -1
  13. package/server/claude-worker/claude-session-worker.mjs +6 -0
  14. package/server/claude-worker/claude-turn-failure.mjs +92 -0
  15. package/server/claude-worker/claude-turn-publisher.mjs +52 -3
  16. package/server/claude-worker/sdk-message-normalizer.mjs +13 -1
  17. package/server/copilot-worker/copilot-model-switch.mjs +29 -3
  18. package/server/copilot-worker/copilot-question-bridge.mjs +2 -4
  19. package/server/copilot-worker/copilot-runtime-tree.mjs +91 -0
  20. package/server/copilot-worker/copilot-sdk-adapter.mjs +48 -8
  21. package/server/copilot-worker/copilot-sdk-session-process.mjs +897 -166
  22. package/server/cursor-worker/cursor-sdk-adapter.mjs +80 -13
  23. package/server/cursor-worker/cursor-turn-runner.mjs +5 -2
  24. package/server/db-schema.mjs +24 -0
  25. package/server/grok-worker/acp-client.mjs +68 -12
  26. package/server/grok-worker/grok-sdk-adapter.mjs +4 -0
  27. package/server/grok-worker/grok-turn-runner.mjs +6 -2
  28. package/server/migrations/0006-workspace-root-settings.mjs +67 -0
  29. package/server/migrations/0007-claude-cloud.mjs +82 -0
  30. package/server/public/app/agent-sessions-settings-ui.js +197 -0
  31. package/server/public/app/agent-sessions-settings.mjs +134 -0
  32. package/server/public/app/api-client.js +160 -6
  33. package/server/public/app/attachments-view.js +20 -1
  34. package/server/public/app/attribution-control.mjs +46 -0
  35. package/server/public/app/bootstrap.js +347 -31
  36. package/server/public/app/claude-cloud-conversation-ui.js +110 -0
  37. package/server/public/app/claude-cloud-settings-ui.js +374 -0
  38. package/server/public/app/claude-cloud-ui.mjs +611 -0
  39. package/server/public/app/composer-control-state.mjs +15 -3
  40. package/server/public/app/context-menu.mjs +140 -0
  41. package/server/public/app/context-tier-options.mjs +4 -0
  42. package/server/public/app/conversation-origin-ui.js +27 -0
  43. package/server/public/app/conversation-provider-indicator.mjs +22 -4
  44. package/server/public/app/conversation-view.js +178 -8
  45. package/server/public/app/cwd-picker.js +12 -0
  46. package/server/public/app/journal-view.js +704 -28
  47. package/server/public/app/markdown-strikethrough.mjs +31 -0
  48. package/server/public/app/math-dollars.mjs +89 -0
  49. package/server/public/app/model-selector-options.mjs +1 -1
  50. package/server/public/app/plan-usage-view.mjs +36 -8
  51. package/server/public/app/relay-board-inline.mjs +70 -0
  52. package/server/public/app/relay-board-view.js +157 -61
  53. package/server/public/app/relay-error-ctas.mjs +28 -5
  54. package/server/public/app/remote-relay-origin-view.mjs +120 -4
  55. package/server/public/app/router.js +49 -12
  56. package/server/public/app/settings-modal.js +33 -2
  57. package/server/public/app/settings-tabs.js +1 -1
  58. package/server/public/app/socket-handlers.js +60 -0
  59. package/server/public/app/store.js +4 -1
  60. package/server/public/app/subagent-fold.mjs +61 -0
  61. package/server/public/app/usage-limit-ui.js +225 -0
  62. package/server/public/index.html +471 -8
  63. package/server/relay-tools.md +13 -1
  64. package/server/repositories/message-repository.mjs +10 -1
  65. package/server/repositories/question-repository.mjs +1 -0
  66. package/server/repositories/remote-relay-repository.mjs +72 -1
  67. package/server/repositories/session-repository.mjs +47 -1
  68. package/server/routes/claude-cloud-routes.mjs +140 -0
  69. package/server/routes/git-routes.mjs +37 -0
  70. package/server/routes/messages-routes.mjs +202 -26
  71. package/server/routes/relay-board-routes.mjs +56 -2
  72. package/server/routes/remote-relay-routes.mjs +15 -1
  73. package/server/routes/sessions-routes.mjs +538 -72
  74. package/server/server-runtime.mjs +291 -27
  75. package/server/services/claude-account-usage-service.mjs +176 -0
  76. package/server/services/claude-cloud-repo-service.mjs +408 -0
  77. package/server/services/claude-cloud-session-service.mjs +326 -0
  78. package/server/services/claude-cloud-settings-service.mjs +269 -0
  79. package/server/services/git-remote-service.mjs +115 -0
  80. package/server/services/plan-usage-claude-cloud.mjs +211 -0
  81. package/server/services/plan-usage-claude.mjs +202 -40
  82. package/server/services/plan-usage-contract.mjs +39 -3
  83. package/server/services/plan-usage-service.mjs +82 -2
  84. package/server/services/preview-proxy-server.mjs +64 -36
  85. package/server/services/process-list-service.mjs +122 -0
  86. package/server/services/provider-reasoning-effort.mjs +3 -0
  87. package/server/services/relay-console-log.mjs +199 -0
  88. package/server/services/remote-relay-caller-context.mjs +78 -19
  89. package/server/services/remote-relay-client.mjs +9 -0
  90. package/server/services/remote-relay-dispatcher.mjs +451 -39
  91. package/server/services/remote-relay-inbound.mjs +83 -1
  92. package/server/services/remote-relay-loopback.mjs +245 -0
  93. package/server/services/remote-relay-registry-service.mjs +52 -0
  94. package/server/services/session-worker-launch-service.mjs +100 -15
  95. package/server/services/session-worker-process-service.mjs +410 -138
  96. package/server/services/session-worker-registry-service.mjs +1 -1
  97. package/server/services/session-worker-stop-service.mjs +24 -13
  98. package/server/services/session-worker-supervisor-service.mjs +89 -2
  99. package/server/services/usage-limit-pause-service.mjs +443 -0
  100. package/server/services/worker-launch-note-service.mjs +151 -0
  101. package/shared/ask-user-bridge.mjs +14 -39
  102. package/shared/claude-attribution.mjs +117 -0
  103. package/shared/claude-cloud/api-client.mjs +809 -0
  104. package/shared/claude-cloud/base-url.mjs +47 -0
  105. package/shared/claude-cloud/credentials.mjs +221 -0
  106. package/shared/claude-cloud/repo-url.mjs +111 -0
  107. package/shared/claude-usage-limit.mjs +257 -0
  108. package/shared/control-poller.mjs +69 -20
  109. package/shared/failure-note-text.mjs +38 -0
  110. package/shared/provider-routing.mjs +4 -2
  111. package/shared/question-wait.mjs +108 -0
  112. package/shared/remote-relay-contract.mjs +167 -22
  113. package/shared/remote-relay-tool-core.mjs +11 -8
  114. package/shared/steer-settle-failure.mjs +17 -0
  115. package/shared/steer-settle-markers.mjs +8 -0
  116. package/shared/worker-runtime/process-tree.mjs +483 -0
  117. package/shared/worker-runtime/relay-errors.mjs +31 -0
  118. package/shared/worker-runtime/send-and-wait-errors.mjs +4 -4
  119. package/shared/worker-runtime/turn-liveness.mjs +249 -0
@@ -6,7 +6,7 @@ import {
6
6
  } from "../runtime/send-and-wait-errors.mjs";
7
7
  import { getActiveSession } from "../runtime/session-registry.mjs";
8
8
  import { DEFAULT_QUESTION_TIMEOUT_MS } from "../../../../shared/question-timeout.mjs";
9
- import { QUESTION_TIMEOUT_CONTINUATION_TEXT } from "../../../../shared/question-timeout.mjs";
9
+ import { waitForRelayQuestion } from "../../../../shared/question-wait.mjs";
10
10
  import { EMPTY_TURN_COMPLETION_NOTE } from "../../../../shared/empty-turn-completion.mjs";
11
11
  import { stripPromptContextPrefix } from "../skills/prompt-context.mjs";
12
12
 
@@ -473,37 +473,17 @@ export function createPollingLoop({
473
473
  return null;
474
474
  };
475
475
 
476
- async function waitForRelayQuestionAnswer(questionId, timeoutMs = DEFAULT_QUESTION_TIMEOUT_MS, pollIntervalMs = 1500) {
477
- const started = Date.now();
478
- while (true) {
479
- const { question } = await api("GET", `/api/relay-question/${questionId}`);
480
- if (!question) throw new Error("Relay question missing");
481
- if (question.status === "answered") {
482
- return {
483
- answer: String(question.answer || "").trim(),
484
- structuredAnswer: question.structuredAnswer && typeof question.structuredAnswer === "object"
485
- ? question.structuredAnswer
486
- : null,
487
- timedOut: false,
488
- };
489
- }
490
- if (question.status === "timed_out" || question.status === "cancelled") {
491
- return {
492
- answer: QUESTION_TIMEOUT_CONTINUATION_TEXT,
493
- structuredAnswer: null,
494
- timedOut: true,
495
- };
496
- }
497
- if (Date.now() - started >= timeoutMs) {
498
- await api("POST", `/api/relay-question/${questionId}/timeout`, {}).catch(() => {});
499
- return {
500
- answer: QUESTION_TIMEOUT_CONTINUATION_TEXT,
501
- structuredAnswer: null,
502
- timedOut: true,
503
- };
504
- }
505
- await sleep(pollIntervalMs);
506
- }
476
+ // The wait survives a relay that is briefly not there
477
+ // (shared/question-wait.mjs).
478
+ function waitForRelayQuestionAnswer(questionId, timeoutMs = DEFAULT_QUESTION_TIMEOUT_MS, pollIntervalMs = 1500) {
479
+ return waitForRelayQuestion({
480
+ api,
481
+ questionId,
482
+ deadlineMs: timeoutMs,
483
+ pollMs: pollIntervalMs,
484
+ sleep,
485
+ dbg,
486
+ });
507
487
  }
508
488
 
509
489
  async function processPendingSdkSessionDeletes() {
@@ -1,5 +1,5 @@
1
1
  import { getActiveSession } from '../runtime/session-registry.mjs';
2
- import { QUESTION_TIMEOUT_CONTINUATION_TEXT } from "../../../../shared/question-timeout.mjs";
2
+ import { createRelayQuestion, waitForRelayQuestion } from "../../../../shared/question-wait.mjs";
3
3
  import { extractRequestedSchema } from "../../../../shared/question-schema.mjs";
4
4
 
5
5
  export function createQuestionBridge({
@@ -49,39 +49,18 @@ export function createQuestionBridge({
49
49
  return Number.isFinite(fallback) && fallback >= 0 ? fallback : 0;
50
50
  }
51
51
 
52
- async function waitForRelayQuestionAnswer(questionId, timeoutMs = null) {
53
- const started = Date.now();
54
- const effectiveTimeoutMs = resolveQuestionWaitTimeoutMs(timeoutMs);
55
-
56
- while (true) {
57
- const { question } = await api("GET", `/api/relay-question/${questionId}`);
58
- if (!question) throw new Error("Relay question missing");
59
- if (question.status === "answered") {
60
- return {
61
- answer: String(question.answer || ""),
62
- structuredAnswer: question.structuredAnswer && typeof question.structuredAnswer === "object"
63
- ? question.structuredAnswer
64
- : null,
65
- timedOut: false,
66
- };
67
- }
68
- if (question.status === "timed_out" || question.status === "cancelled") {
69
- return {
70
- answer: QUESTION_TIMEOUT_CONTINUATION_TEXT,
71
- structuredAnswer: null,
72
- timedOut: true,
73
- };
74
- }
75
- if (Date.now() - started >= effectiveTimeoutMs) {
76
- await api("POST", `/api/relay-question/${questionId}/timeout`, {}).catch(() => {});
77
- return {
78
- answer: QUESTION_TIMEOUT_CONTINUATION_TEXT,
79
- structuredAnswer: null,
80
- timedOut: true,
81
- };
82
- }
83
- await sleep(questionPollMs);
84
- }
52
+ // The wait survives a relay that is briefly not there
53
+ // (shared/question-wait.mjs).
54
+ function waitForRelayQuestionAnswer(questionId, timeoutMs = null) {
55
+ return waitForRelayQuestion({
56
+ api,
57
+ questionId,
58
+ deadlineMs: resolveQuestionWaitTimeoutMs(timeoutMs),
59
+ pollMs: questionPollMs,
60
+ sleep,
61
+ trimAnswer: false,
62
+ dbg,
63
+ });
85
64
  }
86
65
 
87
66
  async function forwardRelayQuestion(request) {
@@ -112,11 +91,7 @@ export function createQuestionBridge({
112
91
  request: serializeRequest(request),
113
92
  };
114
93
 
115
- const created = await api("POST", "/api/relay-question", questionPayload);
116
- const questionId = created?.question?.id;
117
- if (!questionId) {
118
- throw new Error("Relay question could not be created");
119
- }
94
+ const questionId = await createRelayQuestion({ api, payload: questionPayload, sleep, dbg });
120
95
 
121
96
  dbg(
122
97
  "relay question created",
package/CHANGELOG.md CHANGED
@@ -5,6 +5,346 @@ All notable changes to OAR are documented here. The format follows
5
5
 
6
6
  ## [Unreleased]
7
7
 
8
+ ## [0.9.7] — 2026-10-03
9
+
10
+ Highlights: Claude Cloud chats (experimental, off by default) run Claude Code
11
+ in a sandbox at Anthropic on a GitHub repository; conversations can be
12
+ archived and have a context menu in the list; agents can start and use
13
+ sessions on the relay they run on; commits of Claude sessions say "Open
14
+ Agent Relay"; Check Usage opens at once.
15
+
16
+ ### Added
17
+
18
+ - **Archive conversations, and a context menu in the list.** Every
19
+ conversation row has an **Archive** button beside Delete (and the chat's
20
+ `⋯` menu an **Archive conversation** entry). An archived chat leaves the
21
+ list and its session worker is stopped; the 🗄 toggle at the top of the
22
+ sidebar shows the archived chats, each with **Unarchive**. A Claude Cloud
23
+ chat's cloud session is archived and unarchived at Anthropic with it. A
24
+ message into an archived chat (an agent's, say) brings it back by itself;
25
+ the composer of an archived chat says so instead of sending. A right-click
26
+ on a row — a long press on a phone — opens a menu with Open, Edit title,
27
+ Stop turn, Kill session, Archive/Unarchive and Delete.
28
+ - **Commits made from OAR say so.** Claude sessions now end their commit
29
+ messages with `Co-authored-by: Open Agent Relay (<model>) <no-reply@oar.sh>`
30
+ — the model's name in the parentheses, e.g. `Claude Fable 5.1` — and pull
31
+ request bodies with `🤖 Generated with [Open Agent Relay](https://oar.sh)`,
32
+ instead of Claude Code's own lines. Settings → Providers → Claude has the
33
+ choice: **OAR** (the default), **vanilla** (Claude Code's own attribution)
34
+ or **off** (none); a repo folder can override it from the 🧠 context modal
35
+ of any of its Claude sessions. A change reaches running sessions with their
36
+ next message. Claude Cloud chats follow the same setting: the sandbox gets
37
+ it with the first message and again when it changes, and in OAR mode the
38
+ `Claude-Session` link is left out of the commit as well. Other providers
39
+ are not touched: their tools have no such setting.
40
+ - **Claude Cloud (experimental): chats that run in a sandbox at Anthropic.**
41
+ A new provider, off by default. A Claude Cloud chat runs Claude Code in
42
+ Anthropic's cloud, on its own clone of a GitHub repository instead of on
43
+ the relay host. It is billed to the Claude account the host's Claude CLI
44
+ is logged in to, and its results come back as branches the agent pushes.
45
+ Switch it on in Settings → Providers → Claude Cloud and choose the cloud
46
+ environment and a default model there; it needs the Claude CLI logged in
47
+ with a claude.ai account on the relay host, GitHub connected to that
48
+ account, and the Claude GitHub app allowed on the repository.
49
+ - **New Chat** fills **Repository** and **Branch** from the folder you
50
+ pick, offers the repositories the Claude GitHub app can reach (the ones
51
+ used here before first) and the branches of the picked repository, lets
52
+ you type both, and warns about commits that are not pushed, changes
53
+ that are not committed and a repository the app cannot reach.
54
+ - **In the chat** the reply streams in live, the agent's questions and
55
+ permission prompts arrive as cards, **Stop** interrupts the turn, images
56
+ can be attached, and the model can be changed between turns. A line
57
+ above the composer links the session on claude.ai and every branch the
58
+ agent pushed to its comparison on GitHub, and shows the cost so far.
59
+ - **Turns** survive a restart of the worker or the relay without sending
60
+ your message twice. A turn stays open while the agent's background work
61
+ runs, a turn the session starts by itself (after long background work,
62
+ or from a message sent on claude.ai) appears as a reply of its own, and
63
+ a turn refused at the usage limit is paused until the reset like a
64
+ local Claude turn.
65
+ - **Commits** follow the relay's attribution setting (see above), and
66
+ deleting or archiving the chat archives the cloud session.
67
+ - **Not there:** relay modes, a reasoning effort, steering during a turn,
68
+ files other than images and anything else from the relay host, and the
69
+ relay's tools and previews inside the sandbox.
70
+ - **Before you switch it on:** the provider uses the Claude CLI's stored
71
+ login to call the Anthropic API endpoints the CLI itself uses. These
72
+ are not a documented public API and may change. OAR only reads the
73
+ token: it never stores or logs it and sends it nowhere but to
74
+ Anthropic; when it has run out, OAR lets the CLI refresh it. While the
75
+ provider is off the relay fetches nothing with that login, except for
76
+ cloud chats you created before. See "Claude Cloud" in the README.
77
+ `scripts/claude-cloud-live-check.mjs` checks a running relay's Claude
78
+ Cloud end to end.
79
+ - **Check Usage: live Claude limits and a Claude Cloud card.** With Claude
80
+ Cloud switched on, the Claude card reads the 5-hour, weekly and per-model
81
+ limits live from the account when you open the modal, instead of showing
82
+ the reading of your last Claude turn, and a limit that is getting close
83
+ carries Anthropic's warning. The card also shows which product the week
84
+ went to (Claude Code, chats and the others) and the organisation's prepaid
85
+ credits. The new Claude Cloud card shows the account's cloud credit (used,
86
+ left, and when it expires) and what the cloud chats on this relay have
87
+ cost as Anthropic reports it, in total and for the chat you opened the
88
+ modal from; a credit that is offered but not yet claimed is mentioned with
89
+ a link. The limit-reset vouchers (full reset, 5-hour reset) stay
90
+ claude.ai-only: no endpoint the CLI's login reaches carries them, so the
91
+ card links to the usage page instead. With Claude Cloud off, the Claude
92
+ card is what it was.
93
+ - **Agent sessions: an agent can hand parts of its task to other sessions on
94
+ the same relay.** The `remote_relay` tool now also works on the relay the
95
+ agent runs on: it starts sessions there, waits for them and reads their
96
+ replies, and each one is an ordinary conversation in your list. Off by
97
+ default; switch it on in Settings → Relays → Agent sessions (**Agents may
98
+ start and use sessions on this relay**). No relay has to be paired. The
99
+ first time the agent of a conversation starts a session, a card asks you
100
+ once for that conversation. At most 4 sessions it started may be at work at
101
+ the same time, a session that an agent created cannot start sessions
102
+ itself, and an agent cannot prompt, stop or archive the conversation it
103
+ runs in. A session can use any provider that is enabled on the relay
104
+ (image chats excepted); a Claude Cloud chat is started with the new `repo`
105
+ and `branch` arguments, and an agent on a paired relay can now start a
106
+ cloud chat here the same way. Sessions an agent starts use this relay's
107
+ accounts, show **via agent** with the title of the conversation they came
108
+ from in the sidebar and the header (a click opens it), and are never
109
+ archived automatically. The new
110
+ **Longest wait per tool call** slider in the same place sets how long one
111
+ call may wait for a reply: 10 minutes as before, up to 60; it applies to
112
+ waits on paired relays too. See "Agent sessions" in the README.
113
+
114
+ ### Changed
115
+
116
+ - **Check Usage opens at once.** The modal shows the reading it showed last
117
+ time immediately and reads only the open tab's provider live (the card says
118
+ "updating…" meanwhile), instead of fetching every provider and showing a
119
+ spinner until the slowest answered. A tab you switch to is read live when
120
+ its reading is older than a minute; **Refresh** reads the open tab only.
121
+ The relay keeps the last live answer of every provider, so the other cards
122
+ stay filled (`GET /api/usage?providers=…`).
123
+ - **Windows: the relay no longer stops while it reads the process list.**
124
+ Before every launch, every kill and every check of a worker that went
125
+ quiet, the relay reads the list of all processes through PowerShell. It
126
+ did so synchronously: half a second to a second and a half on an idle
127
+ machine, several seconds under load, during which the relay answered
128
+ nothing — under CI load that was enough for the tunnel to report 502 for
129
+ the whole relay. The list is now read off the relay's thread; callers that
130
+ ask at the same time share one read, and a read is reused for 1.5 s. A
131
+ worker that went quiet is checked by its pid first and by the list last.
132
+ `GET /api/status` → `sessionWorker.processList` shows when the list was
133
+ last read, how long it took and whether it failed; a read slower than two
134
+ seconds is logged. A kill or a workspace relaunch that cannot read the
135
+ list answers 409 with the reason instead of failing with 500 or stopping
136
+ blind.
137
+
138
+ ### Fixed
139
+
140
+ - **A reply that names an error code is a reply.** A reply a session
141
+ worker published was read as a failed turn when its one paragraph led up
142
+ to "Error code: relay.…" — a short report on a test that had failed with
143
+ that code, for instance. Workers always send the failure as a record, so
144
+ their replies are no longer read for one; the page's fix buttons under a
145
+ failure follow the same rule and no longer appear under a reply that
146
+ mentions a code in a quotation, a code span, a table or a list.
147
+ - **Two prices in one paragraph were shown as a formula.** In a reply such
148
+ as `this run cost $4.20, the earlier one $3.80`, everything between the
149
+ two dollar signs was rendered as maths. A dollar sign now starts a formula
150
+ only when it is written the way inline maths is: no space after the
151
+ opening sign, no space before the closing one, and no digit right after
152
+ it. Prices stay text; `$x^2$` is still maths. A wide inline formula also
153
+ no longer makes the whole message scroll sideways: it stays inside the
154
+ bubble and scrolls within itself.
155
+
156
+ - **Windows: no worker could start once an agent had run a command with an
157
+ arrow or a bullet in it.** Before every launch the relay reads the list of
158
+ all processes through PowerShell. Under the console's OEM code page
159
+ PowerShell wrote characters such as `→` and `•` as control bytes, the list
160
+ stopped being valid JSON, and every launch on the relay failed: each session
161
+ you wrote to turned yellow after six tries and stayed so, for every
162
+ provider, until the relay was restarted. The list is now written as UTF-8, a
163
+ control byte that still gets through is tolerated, a list the relay cannot
164
+ read no longer stops a launch, and a session whose launches were exhausted
165
+ is tried once more every minute instead of waiting for a restart.
166
+ - **A session whose worker cannot be started now says so.** Until now such a
167
+ session showed a yellow dot and nothing else. The relay now writes one note
168
+ into the conversation — why the worker could not be started, in one
169
+ sentence plus the cause — and keeps that same note up to date: it tries
170
+ again every minute for ten minutes, then stops and the note gets a
171
+ **Retry** button for once the cause is fixed; when the worker starts, the
172
+ note says so. Your message stays queued the whole time, and a queued
173
+ restart of the relay no longer waits for a message the relay has stopped
174
+ trying to deliver.
175
+ - **The Claude usage card showed extra-usage credits 100 times too high.**
176
+ Claude reports these amounts in the currency's minor units (cents), and
177
+ the card showed them as dollars: 12.50 used of a 50.00 limit read as 1250
178
+ of 5000. The percentage was right.
179
+
180
+ ## [0.9.6] — 2026-09-29
181
+
182
+ Highlights: a Claude turn pauses at the usage limit and carries on after the
183
+ reset, a command that prints nothing no longer fails its turn, and agents on
184
+ paired relays can work with each other for a whole session.
185
+
186
+ ### Added
187
+
188
+ - The relay keeps a copy of its console in `relay-console.log` in its log
189
+ folder (`server/logs` unless you moved it), with a time stamp per line. The
190
+ file is rotated at 5 MB and three older copies are kept. The relay's token is
191
+ never written to it. Set `OAR_NO_CONSOLE_LOG=1` to switch it off.
192
+ - **Claude usage limit:** a Claude turn that runs into the subscription's
193
+ usage limit is paused instead of failed. Your message closes with a note,
194
+ the relay queues a message of its own ("continue where you left off") and
195
+ sends it a minute after the limit resets. A banner above the composer says
196
+ when, with **Resume now** and **Cancel**. The pause survives a relay
197
+ restart. A reset more than six hours away (a weekly limit) waits for you to
198
+ resume it.
199
+ - A banner warns in Claude conversations when the usage passes 90 % of a
200
+ limit, with the time of the reset, and can be hidden until the next one.
201
+
202
+ ### Changed
203
+
204
+ - **Remote relays:** agents on paired relays can now work with each other for
205
+ a whole session. A relay you mentioned stays in reach whatever prompts arrive
206
+ later, and a relay whose agent wrote to a conversation is open to it, so the
207
+ agent that was asked can answer, ask back or report later. Until now a prompt
208
+ from another relay's agent took the tool away for the rest of the turn, also
209
+ in a turn you had started yourself. A prompt that crossed two relays is still
210
+ not passed on to a third relay you did not unlock; reading and waiting are no
211
+ longer limited. After 30 prompts to other relays' agents without a message
212
+ from you, the next one asks you first.
213
+ - Subagents are folded to their header inside the running turn, so a turn with
214
+ many of them stays short to scroll. The header says how many steps the
215
+ subagent has taken and shows the latest one, as far as there is room. A tap
216
+ on the header unfolds a subagent and folds it again; what you chose by hand
217
+ stays. The Stop button of a finished subagent is gone.
218
+ - **Copilot, Cursor, Grok:** how long a turn may stay quiet now depends on what
219
+ the agent is doing: 2 minutes with nothing in flight, 5 minutes during a model
220
+ request, 30 minutes while a tool runs. Until now every silence of 2 minutes
221
+ failed the turn, including a command that simply printed nothing. The failure
222
+ note says how long the silence was and what was running. The three windows can
223
+ be set with `OAR_TURN_STALL_IDLE_MS`, `OAR_TURN_STALL_MODEL_MS` and
224
+ `OAR_TURN_STALL_TOOL_MS`.
225
+ - **Copilot:** a turn kept past 2 minutes of silence is checked: a runtime that
226
+ no longer answers fails the turn at once. The watchdog also covers the start of
227
+ a session, where a runtime that did not answer used to hold the message without
228
+ any failure, and a Stop during that start now takes effect at once.
229
+ - **Cursor:** a stalled run is cancelled, so the next message does not wait for
230
+ it.
231
+ - A plan board is part of the reply it belongs to: its content and its buttons
232
+ are shown inside the agent's reply bubble (inside the running turn's bubble
233
+ until the reply is there) instead of on a card of their own at the end of the
234
+ transcript. When the reply already is the plan, the plan is shown once, with
235
+ the buttons below it. After you chose, the plan stays and the bubble says
236
+ what was chosen.
237
+ - The choice on a plan board also sets the mode of the session: after
238
+ "Implement in autopilot" the composer says Autopilot, after "Stop here and
239
+ prompt myself" it says Agent, and the next message carries the work on. The
240
+ session used to stay in Plan, so the next message planned again. "Stop here"
241
+ leaves the mode as it is.
242
+
243
+ ### Fixed
244
+
245
+ - A session worker that was idle and whose process has ended (stopped from
246
+ outside, or crashed between turns) is no longer listed as ready with its old
247
+ process id. The next message started a new worker before as well; the list
248
+ now says what is there, about a minute and a half after the process ended.
249
+ - A reply that quotes or describes a failure note (an agent reporting on a
250
+ failed test, for example) is no longer taken for a failure itself. The relay
251
+ cut such a reply off at the quoted error code, put its own advice behind it
252
+ and stored the turn as failed. Where a failure is known from its text alone,
253
+ that text is now kept whole.
254
+ - A single `~` in a reply ("about", as in "~35 s … ~7 min") no longer strikes
255
+ out the text up to the next one. Strikethrough needs two tildes on each side:
256
+ `~~text~~`. The page now names the version of its Markdown library (15.0.12,
257
+ the one it was served until now) instead of taking whatever the CDN hands out.
258
+ - **Copilot:** a command the agent had started no longer runs on after its
259
+ runtime is gone. When the runtime died or was killed in the middle of a turn,
260
+ the turn failed as it should, but the command kept running with nobody left
261
+ to read its result. The relay now stops what such a runtime leaves behind
262
+ (first gently, then by force), and the failure note says that a command that
263
+ was still running is being stopped, as far as that is known at that moment:
264
+ the note comes at once and does not wait for the list of processes. A command
265
+ the agent started to outlive the session (`detach`) is left alone, and so are
266
+ the commands of a runtime that shuts down in good order.
267
+ - **Grok:** a quiet-turn window that is set for the model request or for a tool
268
+ is kept when the shared watchdog is switched off by the environment, and the
269
+ watchdog looks often enough for the shortest window that is set.
270
+ - **Windows:** stopping or deleting a session could end the worker of another
271
+ session as well. Windows hands the process id of an ended process out again,
272
+ and a worker whose creator was long gone could so look like a child of the
273
+ session that was being stopped. A process older than its parent is no longer
274
+ taken for its child, and the worker of another session never is.
275
+ - **Copilot:** a message sent while a command is running no longer cuts the
276
+ command short. It waits until the running tool call has ended and is steered
277
+ in then; the composer reads "Queue" for as long. Sent at once, the message
278
+ made the runtime push the command to the background: the turn ended early
279
+ with the answer to the new message alone, or failed with "No response was
280
+ returned", and the command's result arrived later in a turn of its own.
281
+ - A plan board's action ("Implement in autopilot" and the like) queues its
282
+ follow-up again. It failed with an error and left the board marked as acted
283
+ on, without the message that carries the plan out.
284
+ - A finished subagent no longer turns back to "Running" in the running turn
285
+ when another line of activity arrives or the page reloads.
286
+ - **Previews:** the first page load after a dev server was restarted no longer
287
+ ends in "The upstream connection failed: ECONNRESET". The preview lane keeps
288
+ its connections to the dev server, and the one it had kept was closed by the
289
+ restart. A request without a body is now sent once more on a connection of
290
+ its own: it reaches the restarted server, or the page says that nothing is
291
+ listening on the port.
292
+ - Links in the file viewer and in plan boards have a colour that can be read in
293
+ dark mode; they used the browser's default blue.
294
+ - A question or approval card no longer answers itself when the relay is
295
+ briefly unreachable, for example while it restarts. One failed check used to
296
+ end the wait: the agent was told to use its own judgement, or the approval
297
+ was refused, while the card stayed on screen. The wait now lasts through the
298
+ outage, for every provider.
299
+ - **Copilot:** a turn that fails is reported at once. The failure used to wait
300
+ for the runtime to finish stopping, which took three minutes on a runtime that
301
+ had stopped answering; stopping it is now bounded, and the next message waits
302
+ for it before a new runtime starts.
303
+ - **Copilot:** a failed turn keeps what the agent had written, above the failure
304
+ note. The note says how many tool calls ran and that their changes are still in
305
+ place, and it no longer advises restarting the relay. A message steered into
306
+ the failed turn gets a short marker with Resend instead of a second copy of
307
+ the failure.
308
+ - **Copilot:** a slow relay no longer makes a busy runtime look silent.
309
+ - **Copilot:** a reply the relay could not take (it was restarting, the
310
+ connection was reset) is offered again for up to five minutes. One failed
311
+ attempt used to put the message back in the queue, which threw the finished
312
+ answer away and ran the prompt a second time. A reply that cannot be saved at
313
+ all ends as a failure that says so, and nothing is run twice.
314
+ - **Copilot:** what the agent does by itself after a background task (a
315
+ continuation) is kept while the relay restarts. Its row was given up after
316
+ about a second, and the whole reply with it.
317
+ - **Copilot:** a relay that accepts live output and does not answer no longer
318
+ holds the turn: a live update waits 10 seconds at most, and after one that
319
+ ran out, live updates are skipped for 30 seconds. Tool lines skipped then are
320
+ missing from the transcript; the reply is not affected.
321
+ - **Copilot:** Stop works on a turn that is stuck. A runtime that does not
322
+ answer the Stop within 10 seconds is ended with the turn, and a turn the user
323
+ stopped ends as stopped even when the runtime died or went silent after it.
324
+ - **Copilot:** a runtime that exits, or stops answering, while the agent works
325
+ by itself after a background task is cleared away; the next message used to
326
+ fail on it. A lost connection during a model change is reported as that, not
327
+ as "pick another model".
328
+ - **Copilot:** a failed turn tells its failure once. A message sent into the
329
+ turn that the agent had not started on gets the short marker with Resend,
330
+ like one it had taken in.
331
+ - **Copilot:** background agents and shells are no longer closed with the
332
+ runtime because its list of them could not be read.
333
+ - **Stop button:** Stop keeps working for a message that waits on a question
334
+ card while the agent starts and finishes other work, and for a second message
335
+ stopped while the same turn goes on. It used to do nothing in both cases.
336
+ - **Copilot:** a turn the runtime itself ended with an error no longer tells
337
+ you to restart the relay. The worker starts a fresh runtime by itself; a
338
+ restart repairs nothing and costs every other running session.
339
+ - The note under a failed turn no longer advises restarting the relay: a restart
340
+ repairs nothing there and interrupts every other running session. It now says
341
+ to send the message again and to include the error code in a report.
342
+ - **Claude:** a turn the Claude account refuses (billing, an expired sign-in,
343
+ access the organisation has switched off) says to check the account or to sign
344
+ in again in Settings → Providers → Claude, with a button that opens the panel,
345
+ instead of "Retry the message". Its error code names the failure, for example
346
+ `relay.claude-billing-error`; it used to read `relay.claude-success`.
347
+
8
348
  ## [0.9.5] — 2026-09-27
9
349
 
10
350
  Highlights: a Claude reply on the wrong row no longer blocks the conversation,