@oar-sh/oar 0.9.21 → 0.9.23

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 (82) hide show
  1. package/CHANGELOG.md +206 -0
  2. package/README.md +9 -9
  3. package/bin/oar.js +28 -0
  4. package/package.json +13 -14
  5. package/server/claude-cloud-worker/claude-cloud-session-worker.mjs +9 -1
  6. package/server/claude-worker/claude-cli-pinned-env.mjs +4 -1
  7. package/server/claude-worker/claude-sdk-adapter.mjs +149 -4
  8. package/server/claude-worker/claude-session-process.mjs +9 -4
  9. package/server/claude-worker/claude-session-worker-main.mjs +10 -1
  10. package/server/copilot-worker/copilot-sdk-session-process.mjs +1 -0
  11. package/server/copilot-worker/copilot-sdk-session-worker.mjs +10 -1
  12. package/server/cursor-worker/cursor-session-worker.mjs +9 -1
  13. package/server/db-schema.mjs +19 -0
  14. package/server/grok-worker/grok-background-tasks.mjs +1 -0
  15. package/server/grok-worker/grok-session-worker.mjs +10 -1
  16. package/server/grok-worker/grok-turn-runner.mjs +1 -0
  17. package/server/migrations/0037-relay-asks-queue-id.mjs +66 -0
  18. package/server/migrations/0038-turn-seq.mjs +92 -0
  19. package/server/public/app/activity-replay-state.mjs +160 -42
  20. package/server/public/app/api-client.js +7 -0
  21. package/server/public/app/ask-user-view.js +4 -3
  22. package/server/public/app/bootstrap.js +62 -4
  23. package/server/public/app/chat-actions-menu-state.mjs +2 -0
  24. package/server/public/app/context-usage-view.mjs +68 -0
  25. package/server/public/app/conversation-view.js +306 -49
  26. package/server/public/app/header-priority-actions.mjs +5 -1
  27. package/server/public/app/held-details.mjs +202 -0
  28. package/server/public/app/inbox-ui.js +70 -79
  29. package/server/public/app/inbox-view.mjs +35 -19
  30. package/server/public/app/journal-view.js +17 -0
  31. package/server/public/app/live-activity-cap.mjs +103 -0
  32. package/server/public/app/questions-panel-ui.js +634 -0
  33. package/server/public/app/questions-stub-view.mjs +40 -0
  34. package/server/public/app/questions-view.mjs +89 -0
  35. package/server/public/app/relay-board-view.js +12 -2
  36. package/server/public/app/relay-thoughts.mjs +15 -1
  37. package/server/public/app/session-history-ui.js +1 -0
  38. package/server/public/app/settings-modal.js +6 -0
  39. package/server/public/app/socket-handlers.js +42 -11
  40. package/server/public/app/stream-view-preference.mjs +60 -0
  41. package/server/public/app/subagent-fold.mjs +6 -3
  42. package/server/public/app/subagent-timeline-view.mjs +12 -5
  43. package/server/public/app/subagent-viewer.mjs +8 -6
  44. package/server/public/app/thoughts-preference.mjs +79 -0
  45. package/server/public/app/turn-stream-view.mjs +124 -0
  46. package/server/public/index.html +391 -214
  47. package/server/repositories/message-repository.mjs +10 -0
  48. package/server/repositories/question-repository.mjs +51 -7
  49. package/server/repositories/relay-turn-rows.mjs +56 -0
  50. package/server/routes/ask-user-routes.mjs +34 -5
  51. package/server/routes/messages-routes.mjs +46 -34
  52. package/server/routes/relay-ask-routes.mjs +15 -5
  53. package/server/routes/sessions-routes.mjs +77 -6
  54. package/server/server-runtime.mjs +199 -171
  55. package/server/services/cloudflared-tunnel-service.mjs +7 -0
  56. package/server/services/earlier-database-service.mjs +1 -1
  57. package/server/services/file-serving.mjs +13 -0
  58. package/server/services/idle-timers.mjs +184 -0
  59. package/server/services/native-addon-build-service.mjs +224 -0
  60. package/server/services/oar-cli-helpers.mjs +7 -4
  61. package/server/services/queue-wake-hook.mjs +145 -0
  62. package/server/services/queue-waker.mjs +127 -0
  63. package/server/services/relay-ask-service.mjs +64 -5
  64. package/server/services/relay-console-log.mjs +2 -3
  65. package/server/services/relay-event-service.mjs +4 -1
  66. package/server/services/remote-relay-dispatcher.mjs +23 -4
  67. package/server/services/remote-relay-loopback.mjs +3 -3
  68. package/server/services/sdk-session-sync-service.mjs +1 -1
  69. package/server/services/session-history-refresh-service.mjs +4 -0
  70. package/server/services/session-worker-supervisor-service.mjs +36 -0
  71. package/server/services/session-worker-websocket-service.mjs +143 -15
  72. package/server/services/ssh-tunnel-manager-service.mjs +7 -0
  73. package/server/services/update-install-service.mjs +32 -1
  74. package/shared/ask-user-bridge.mjs +9 -6
  75. package/shared/relay-asks.mjs +10 -0
  76. package/shared/relay-events.mjs +1 -0
  77. package/shared/worker-runtime/heartbeat.mjs +26 -0
  78. package/shared/worker-runtime/worker-keepalive.mjs +108 -0
  79. package/shared/worker-runtime/worker-websocket-link.mjs +21 -10
  80. package/server/services/package-data-rescue-preinstall.mjs +0 -22
  81. package/server/services/package-data-rescue.mjs +0 -139
  82. package/server/tty-console-bootstrap.mjs +0 -75
package/CHANGELOG.md CHANGED
@@ -3,6 +3,212 @@
3
3
  All notable changes to OAR are documented here. The format follows
4
4
  [Keep a Changelog](https://keepachangelog.com/en/1.1.0/); versions follow semver.
5
5
 
6
+ ## [0.9.23] — 2026-11-07
7
+
8
+ Highlights: a dependency round. The Copilot, Claude and Cursor SDKs, zod,
9
+ express, socket.io, ws and better-sqlite3 move to newer releases:
10
+ the tree holds one zod, without CVE-2023-54404, and one express, and
11
+ prebuild-install, which is no longer maintained, is gone. OAR's package has
12
+ no install script any more, which package scanners flag. On Termux, 32-bit
13
+ ARM and FreeBSD, run **`oar build-native`** once after updating to this
14
+ version: it builds the SQLite binding, and later updates build it
15
+ themselves.
16
+
17
+ ### Added
18
+
19
+ - **`oar build-native`.** better-sqlite3 13 ships its SQLite binding for
20
+ Linux, macOS and Windows on x64 and arm64. On Android under Termux, 32-bit
21
+ ARM and FreeBSD, `oar build-native` compiles it in the installed package
22
+ (Python 3, make and a C++ compiler; on Termux `pkg install python clang
23
+ make`). The **Update** button and `oar update` run it there themselves
24
+ before the relay restarts; a build that fails ends the update in an error
25
+ and leaves the running relay alone. Elsewhere it has nothing to do, so an
26
+ installer may run it everywhere. A relay started without the binding says
27
+ which command to run and exits instead of printing a stack trace.
28
+ - **What the agent SDKs contact.** docs/providers.md lists the services the
29
+ Copilot, Claude and Cursor SDKs and their CLIs talk to, and what the
30
+ Statsig client inside the Cursor SDK does.
31
+
32
+ ### Changed
33
+
34
+ - **No install script.** OAR's package has no `preinstall` any more. From
35
+ 0.9.14 to 0.9.22 it copied the data a relay before 0.9.14 kept inside the
36
+ package out before npm deleted it. npm 12 never ran it without
37
+ `--allow-scripts`, and package scanners flag every package that has one.
38
+ A copy those versions made is still taken over at the next start.
39
+ - **`--allow-scripts=koffi`.** better-sqlite3 13 has no install script, so
40
+ a manual install with npm 12 is now
41
+ `npm install -g --allow-scripts=koffi @oar-sh/oar`; the installers, the
42
+ **Update** button and `oar update` pass it themselves.
43
+ - **No terminal console.** The optional `tty-console` dependency is gone,
44
+ and neo-blessed with it. A relay started in a terminal prints its log
45
+ lines, each with its time, as under a service; routine worker lines are
46
+ no longer held back.
47
+ - **Express 5.** The file routes take express 5's syntax and serve the
48
+ same paths. Static JavaScript is served as `text/javascript`.
49
+ - `better-sqlite3` 13.0.3 (was 12.11.1); `prebuild-install`, which is no
50
+ longer maintained, and its subtree are gone. New in the tree:
51
+ `node-addon-api` 8.9.2, the headers of the source build.
52
+ - `express` 5.3.0 (was 4.22.3), the major the MCP SDK already used: one
53
+ express instead of two, and array-flatten, mime 1 and the rest of express
54
+ 4's subtree are gone.
55
+ - `@anthropic-ai/claude-agent-sdk` 0.3.319 (was 0.3.304; bundled Claude
56
+ Code 2.1.319). The key chats (DeepSeek, Claude API) also pin the
57
+ cloud-sessions token and the two new switches that would let a folder's
58
+ settings widen which certificates Claude Code trusts.
59
+ - `@cursor/sdk` 1.3.1 (was 1.0.38). Its context-compaction summaries are
60
+ not shown yet.
61
+ - `@github/copilot-sdk` 1.0.29 (was 1.0.23; bundled Copilot CLI 1.0.107)
62
+ and `zod` 4.6.7 (was 4.4.3): one zod in the tree, without
63
+ CVE-2023-54404 (GHSA-mrhx-c7xc-m3w5).
64
+ - `socket.io` 4.8.4 (was 4.8.3) and `ws` 8.22.1 (was 8.21.3). `uuid` is
65
+ gone: Node's `crypto.randomUUID()` makes the same ids.
66
+ - package.json names its author and its bug tracker.
67
+
68
+ ### Upgrade notes
69
+
70
+ - **From 0.9.13 or older, copy the data out of the package first.** A relay
71
+ before 0.9.14 could keep its database, uploads and config inside the
72
+ package folder, which `npm install -g` deletes, and this version no longer
73
+ copies them out. Copy `data/`, `uploads/` and `config.json` from the
74
+ installed package's `server` folder into a new folder under
75
+ `rescued-package-data` in the data folder (for example
76
+ `~/.oar/data/rescued-package-data/before-update/`); the first start takes
77
+ them over. From 0.9.14 on there is nothing to do.
78
+ - **Termux, 32-bit ARM, FreeBSD: the update from 0.9.22 needs
79
+ `oar build-native` once.** The Update button and `oar update` of 0.9.22
80
+ install 0.9.23 but do not build the binding, so the relay stops at its
81
+ first start on 0.9.23: its log names the command, and the page cannot
82
+ reach the relay. In Termux run:
83
+
84
+ ```sh
85
+ pkg install python clang make
86
+ oar build-native
87
+ oar start
88
+ ```
89
+
90
+ Later updates build the binding themselves, before the relay restarts.
91
+ - **Linux needs glibc 2.34 or newer,** for example Debian 12, Ubuntu 22.04
92
+ or RHEL 9. The SQLite binding better-sqlite3 13 ships for Linux on x64
93
+ and arm64 needs it, and a binding built with `oar build-native` would not
94
+ be used there. On an older system a relay that updates to 0.9.23 stays
95
+ down at its first start and says to update the operating system; stay on
96
+ 0.9.22 until the system is updated.
97
+ - **No migration.** Going back to 0.9.22 needs no database step.
98
+
99
+ ## [0.9.22] — 2026-11-05
100
+
101
+ Highlights: questions that don't block move into the chat header. A **📓**
102
+ icon with a count opens them, urgent ones first, and the turn that asked
103
+ keeps one line, "📓 Question for you", in place of the card. Settings →
104
+ General reads as five headed sections, with three new choices: whether
105
+ thoughts start open while an agent works, how many tool lines a working
106
+ agent shows, counted per agent so a busy sub-agent no longer pushes out the
107
+ main agent's lines, and a stream view that shows a turn's thoughts and tool
108
+ calls in the order they happened. While a turn runs, the page's poll leaves
109
+ out what it already holds, and an idle relay and its workers wake far less
110
+ often. Claude sessions now ask you in a **Safety check** card, which only
111
+ you can answer, before removals and other asks Claude Code marks as
112
+ person-only.
113
+
114
+ ### Changed
115
+
116
+ - **📓 Questions in the chat header.** A conversation's questions that don't
117
+ block sit behind a 📓 icon with a count, first among the header icons (in
118
+ ︙ when the header runs out of room). It opens a list under it with the
119
+ full cards, urgent ones first with their details open, and what was
120
+ answered in the last day below; answering keeps the list open. On a phone
121
+ the list spans the width over a dimmed page, with half-width buttons. While
122
+ an urgent question waits, 📓 and the Inbox show the urgent count in red.
123
+ The Inbox's link to a session opens it with 📓 at that question. Each ask
124
+ now remembers the turn that asked it (migration 0037).
125
+ - **A question leaves one line in the turn that asked it.** The cards of
126
+ questions that don't block no longer sit at the end of the conversation;
127
+ the reply of the turn that asked shows a one-line "📓 Question for you"
128
+ instead (live while it runs, "Answered: …" once answered), and a tap opens
129
+ 📓 at that question. A question asked for another session, carried over by
130
+ a compaction or from before this version stands as one line at the end
131
+ while it waits.
132
+ - **Settings → General in headed sections.** The tab reads as five groups,
133
+ Appearance, Reading, Turns and tasks, This relay and Updates, instead of
134
+ one long column. Each relay-wide row now says it is a setting of this
135
+ relay; the per-browser rows keep "Saved in this browser."
136
+ - **Thoughts open or closed by your choice.** Settings → General →
137
+ Appearance has "Expand thoughts while an agent works" (on by default), with
138
+ a 💭 Thoughts mirror in the 🧠 modal. It decides whether thoughts start open
139
+ in the live turn, in live sub-agent bubbles and in the sub-agent viewer,
140
+ finished thoughts there included. An open live thought keeps a one-line
141
+ header to fold it by; a thought you fold or open by hand stays that way
142
+ while more text streams in. A finished reply's 💭 fold stays closed as
143
+ before.
144
+ - **The main agent's tool lines stay while sub-agents work.** The live
145
+ bubble's tool list is capped per agent now, the main agent and each
146
+ sub-agent apart, so a busy sub-agent no longer pushes the main agent's
147
+ lines out. Settings → General → Appearance has "Tool lines shown while an
148
+ agent works": 50 by default, 10 to 500 or no limit. The relay sends the
149
+ running turn's newest lines per agent as the page's setting asks (50 when
150
+ it does not say, 500 at most), and for a finished turn the main agent's
151
+ newest 200 and each sub-agent's newest 50 (it sent the first 48 of the
152
+ whole turn). A sub-agent bubble that holds as many lines as the setting
153
+ shows "50+ steps".
154
+ - **Stream view: a turn in the order it happened.** Settings → General →
155
+ Appearance has "Stream view" (off by default), with a 🧵 mirror in the 🧠
156
+ modal. When it is on, an agent turn's thoughts and tool calls show as one
157
+ list in the order they happened, live and in the finished message, with
158
+ the reply last. For Copilot, Cursor and Grok, tools and thoughts stream in
159
+ arrival order and the reply comes after them. The relay now numbers a
160
+ turn's thoughts and tool lines in one sequence (migration 0038); turns
161
+ recorded before that keep the grouped layout. A finished turn whose main
162
+ agent ran more than 200 tool lines says how many earlier ones it left out.
163
+ - **The live poll leaves out what the page already has.** While a turn
164
+ runs, the page asks for its conversation about once a second. It now
165
+ names the finished replies whose tool lines and thoughts it already holds,
166
+ and the relay leaves those out unless they changed since (a busy window:
167
+ about 6 KB per poll instead of over 200 KB). Opening a conversation,
168
+ paging back and every other load still get everything.
169
+ - **An idle relay and its workers wake far less often.** The relay no
170
+ longer polls its queue twice a second: a write that makes a message
171
+ deliverable wakes delivery at once, through triggers on the queue table.
172
+ A retry or a usage-limit hold gets a timer for the moment it ends, and a
173
+ 10 s poll stays as a backstop. A gate lifting (relay resumed, restart
174
+ cancelled, tunnel connected, a kill's grace over, a worker replacement
175
+ done) wakes delivery too. The CLI status, the share-viewer prune and the
176
+ worker prime run only while there is something for them to do. Each
177
+ worker has one keepalive instead of two 10 s timers, and it beats every
178
+ 30 s while idle, on a relay that supports it. A silent worker now counts
179
+ as gone after 90 s instead of 30 s. Workers and relays of older versions
180
+ keep working with the new ones.
181
+ - **Claude sessions ask you before removals and other asks Claude Code
182
+ marks as person-only, instead of allowing them.** Such an ask, for
183
+ example an `rm -rf` of the working folder, used to be approved by the
184
+ relay like every other ask. Now a **Safety check** card, in the session
185
+ and in the Inbox, shows Claude Code's reason
186
+ and then the whole command, each line behind `│`, with hidden or
187
+ reordering characters shown as `\u{…}`. It offers **Allow once** and
188
+ **Deny**, takes no typed reply, and only you can answer it: no agent,
189
+ of this relay or another, sees it among a session's questions or may
190
+ answer it. It allows only that one call and adds no rule. A denial, no
191
+ answer within the question timeout, a stopped turn or a relay that
192
+ cannot show the card all leave the command unrun, and a command too long
193
+ to show is refused without a card. Other asks are still allowed as
194
+ before.
195
+
196
+ ### Upgrade notes
197
+
198
+ - **Two small migrations.** 0037 adds the asking turn to each ask and 0038
199
+ the sequence number to a turn's thoughts and tool lines, with an index;
200
+ neither rewrites the rows already there. As before, the relay copies its
201
+ database to the backups folder before the first start of a new version.
202
+ - **Every session worker is replaced once after the update,** as in
203
+ 0.9.21: once its session is idle and its CLI has ended, usually some 10
204
+ minutes after its last turn. A Claude session shows the **Safety check**
205
+ card once its worker runs 0.9.22. Nothing needs to be done.
206
+ - **Mixed versions work together.** A 0.9.21 worker on a 0.9.22 relay keeps
207
+ its 10 s beat, and a 0.9.22 worker on a 0.9.21 relay beats every 10 s.
208
+ Paired relays of both versions keep working with each other.
209
+ - **No new dependency.** Going back to 0.9.21 needs no database step: it
210
+ leaves the new columns and the index alone.
211
+
6
212
  ## [0.9.21] — 2026-11-01
7
213
 
8
214
  Highlights: the chat screen is more compact. The mode, model, effort,
package/README.md CHANGED
@@ -45,29 +45,29 @@ flowchart LR
45
45
  <table>
46
46
  <tr>
47
47
  <td width="42%" rowspan="4" valign="top">
48
- <a href="https://oar.sh/assets/shots/cfmail-mobile.png?v=0.9.21"><picture><source media="(prefers-color-scheme: light)" srcset="https://oar.sh/assets/shots/cfmail-mobile.light.png?v=0.9.21"><img src="https://oar.sh/assets/shots/cfmail-mobile.png?v=0.9.21" alt="OAR on a phone: a finished agent turn in a conversation, with the composer below"></picture></a>
48
+ <a href="https://oar.sh/assets/shots/cfmail-mobile.png?v=0.9.23"><picture><source media="(prefers-color-scheme: light)" srcset="https://oar.sh/assets/shots/cfmail-mobile.light.png?v=0.9.23"><img src="https://oar.sh/assets/shots/cfmail-mobile.png?v=0.9.23" alt="OAR on a phone: a finished agent turn in a conversation, with the composer below"></picture></a>
49
49
  <br><sub>On a phone: the same conversations, installable as an app.</sub>
50
50
  </td>
51
51
  <td width="58%" valign="top">
52
- <a href="https://oar.sh/assets/shots/cfmail-agents.png?v=0.9.21"><picture><source media="(prefers-color-scheme: light)" srcset="https://oar.sh/assets/shots/cfmail-agents.light.png?v=0.9.21"><img src="https://oar.sh/assets/shots/cfmail-agents.png?v=0.9.21" alt="Background subagents running in parallel in the task panel, each with live token counts, elapsed time, and its own Stop button"></picture></a>
52
+ <a href="https://oar.sh/assets/shots/cfmail-agents.png?v=0.9.23"><picture><source media="(prefers-color-scheme: light)" srcset="https://oar.sh/assets/shots/cfmail-agents.light.png?v=0.9.23"><img src="https://oar.sh/assets/shots/cfmail-agents.png?v=0.9.23" alt="Background subagents running in parallel in the task panel, each with live token counts, elapsed time, and its own Stop button"></picture></a>
53
53
  <br><sub>Agents working in parallel, each with live token counts and its own Stop.</sub>
54
54
  </td>
55
55
  </tr>
56
56
  <tr>
57
57
  <td valign="top">
58
- <a href="https://oar.sh/assets/shots/cfmail-question.png?v=0.9.21"><picture><source media="(prefers-color-scheme: light)" srcset="https://oar.sh/assets/shots/cfmail-question.light.png?v=0.9.21"><img src="https://oar.sh/assets/shots/cfmail-question.png?v=0.9.21" alt="A question card in the middle of a turn: the agent offers several choices and a free-text reply"></picture></a>
58
+ <a href="https://oar.sh/assets/shots/cfmail-question.png?v=0.9.23"><picture><source media="(prefers-color-scheme: light)" srcset="https://oar.sh/assets/shots/cfmail-question.light.png?v=0.9.23"><img src="https://oar.sh/assets/shots/cfmail-question.png?v=0.9.23" alt="A question card in the middle of a turn: the agent offers several choices and a free-text reply"></picture></a>
59
59
  <br><sub>When the agent needs a decision, it asks, and waits for your answer.</sub>
60
60
  </td>
61
61
  </tr>
62
62
  <tr>
63
63
  <td valign="top">
64
- <a href="https://oar.sh/assets/shots/pingcf-thoughts.png?v=0.9.21"><picture><source media="(prefers-color-scheme: light)" srcset="https://oar.sh/assets/shots/pingcf-thoughts.light.png?v=0.9.21"><img src="https://oar.sh/assets/shots/pingcf-thoughts.png?v=0.9.21" alt="An expanded Thoughts block showing the agent's reasoning above its reply"></picture></a>
64
+ <a href="https://oar.sh/assets/shots/pingcf-thoughts.png?v=0.9.23"><picture><source media="(prefers-color-scheme: light)" srcset="https://oar.sh/assets/shots/pingcf-thoughts.light.png?v=0.9.23"><img src="https://oar.sh/assets/shots/pingcf-thoughts.png?v=0.9.23" alt="An expanded Thoughts block showing the agent's reasoning above its reply"></picture></a>
65
65
  <br><sub>Read the reasoning, not just the answer.</sub>
66
66
  </td>
67
67
  </tr>
68
68
  <tr>
69
69
  <td valign="top">
70
- <a href="https://oar.sh/assets/shots/pagewright-watching.png?v=0.9.21"><picture><source media="(prefers-color-scheme: light)" srcset="https://oar.sh/assets/shots/pagewright-watching.light.png?v=0.9.21"><img src="https://oar.sh/assets/shots/pagewright-watching.png?v=0.9.21" alt="A session watching two other sessions: a finished reply arrives as a compact relay-event row, the watching 2 sessions list is open with Unsubscribe, and a request to also watch a session on a paired relay waits on an Allow / Deny card"></picture></a>
70
+ <a href="https://oar.sh/assets/shots/pagewright-watching.png?v=0.9.23"><picture><source media="(prefers-color-scheme: light)" srcset="https://oar.sh/assets/shots/pagewright-watching.light.png?v=0.9.23"><img src="https://oar.sh/assets/shots/pagewright-watching.png?v=0.9.23" alt="A session watching two other sessions: a finished reply arrives as a compact relay-event row, the watching 2 sessions list is open with Unsubscribe, and a request to also watch a session on a paired relay waits on an Allow / Deny card"></picture></a>
71
71
  <br><sub>Agents that watch agents: woken by events, approving nothing.</sub>
72
72
  </td>
73
73
  </tr>
@@ -118,11 +118,11 @@ API keys are entered in **⚙️ Settings** and stay on the relay host. The Clau
118
118
  <table>
119
119
  <tr>
120
120
  <td width="62%" valign="top">
121
- <a href="https://oar.sh/assets/shots/cfmail-steering.png?v=0.9.21"><picture><source media="(prefers-color-scheme: light)" srcset="https://oar.sh/assets/shots/cfmail-steering.light.png?v=0.9.21"><img src="https://oar.sh/assets/shots/cfmail-steering.png?v=0.9.21" alt="A running Claude turn with a message steered into it mid-turn, the reply streaming with its tool calls, and the Steer button in the composer"></picture></a>
121
+ <a href="https://oar.sh/assets/shots/cfmail-steering.png?v=0.9.23"><picture><source media="(prefers-color-scheme: light)" srcset="https://oar.sh/assets/shots/cfmail-steering.light.png?v=0.9.23"><img src="https://oar.sh/assets/shots/cfmail-steering.png?v=0.9.23" alt="A running Claude turn with a message steered into it mid-turn, the reply streaming with its tool calls, and the Steer button in the composer"></picture></a>
122
122
  <br><sub>Send a message while the agent works, and it steers the running turn.</sub>
123
123
  </td>
124
124
  <td width="38%" valign="top">
125
- <a href="https://oar.sh/assets/shots/usage-copilot-compact.png?v=0.9.21"><picture><source media="(prefers-color-scheme: light)" srcset="https://oar.sh/assets/shots/usage-copilot-compact.light.png?v=0.9.21"><img src="https://oar.sh/assets/shots/usage-copilot-compact.png?v=0.9.21" alt="The GitHub Copilot card in Check Usage: premium requests and plan quota with reset dates, and the cost of the last turn"></picture></a>
125
+ <a href="https://oar.sh/assets/shots/usage-copilot-compact.png?v=0.9.23"><picture><source media="(prefers-color-scheme: light)" srcset="https://oar.sh/assets/shots/usage-copilot-compact.light.png?v=0.9.23"><img src="https://oar.sh/assets/shots/usage-copilot-compact.png?v=0.9.23" alt="The GitHub Copilot card in Check Usage: premium requests and plan quota with reset dates, and the cost of the last turn"></picture></a>
126
126
  <br><sub>What each plan has left, in one place.</sub>
127
127
  </td>
128
128
  </tr>
@@ -139,7 +139,7 @@ API keys are entered in **⚙️ Settings** and stay on the relay host. The Clau
139
139
  - **Share** a conversation as a read-only link, hide single messages and the host's paths from it, and see when it was opened ([Sharing](docs/web-ui.md#sharing)).
140
140
  - **On your phone**: install OAR as an app and get push notifications; back walks through the conversations you opened, a pinch zooms the whole app, and held sideways it keeps a phone's layout ([Notifications and the app](docs/web-ui.md#notifications-and-the-app), [back and forward](docs/web-ui.md#back-and-forward), [zoom](docs/web-ui.md#zoom), [a phone held sideways](docs/web-ui.md#a-phone-held-sideways)).
141
141
 
142
- The full tour is in [Using the web UI](docs/web-ui.md); every setting and config key is in [Settings and configuration](docs/configuration.md). What changed in each release is in the [changelog](CHANGELOG.md), with [0.9.21](CHANGELOG.md#0921--2026-11-01) as the latest.
142
+ The full tour is in [Using the web UI](docs/web-ui.md); every setting and config key is in [Settings and configuration](docs/configuration.md). What changed in each release is in the [changelog](CHANGELOG.md), with [0.9.23](CHANGELOG.md#0923--2026-11-07) as the latest.
143
143
 
144
144
  ## Remote access
145
145
 
@@ -190,7 +190,7 @@ flowchart LR
190
190
 
191
191
  You stay in charge: per paired relay you decide whether agents may only read, also prompt, or do everything, and in the `ask` and `plan` modes every write asks you first. A subscription only notifies; it approves nothing.
192
192
 
193
- <p><a href="https://oar.sh/assets/shots/pagewright-cross-relay.png?v=0.9.21"><picture><source media="(prefers-color-scheme: light)" srcset="https://oar.sh/assets/shots/pagewright-cross-relay.light.png?v=0.9.21"><img src="https://oar.sh/assets/shots/pagewright-cross-relay.png?v=0.9.21" alt="A session watching two sessions on a paired relay: a reply arrives as a relay-event row, and after a short outage the events the other relay kept arrive as one replayed row" width="720"></picture></a></p>
193
+ <p><a href="https://oar.sh/assets/shots/pagewright-cross-relay.png?v=0.9.23"><picture><source media="(prefers-color-scheme: light)" srcset="https://oar.sh/assets/shots/pagewright-cross-relay.light.png?v=0.9.23"><img src="https://oar.sh/assets/shots/pagewright-cross-relay.png?v=0.9.23" alt="A session watching two sessions on a paired relay: a reply arrives as a relay-event row, and after a short outage the events the other relay kept arrive as one replayed row" width="720"></picture></a></p>
194
194
 
195
195
  How pairing, permissions and limits work is in [Agents working together](docs/agents-together.md).
196
196
 
package/bin/oar.js CHANGED
@@ -14,6 +14,7 @@ import {
14
14
  } from '../server/services/oar-state-migration-service.mjs';
15
15
  import { readConfiguredDataDir, resolveDataDir, resolveUploadDir } from '../server/services/data-dir-resolution.mjs';
16
16
  import { RESCUE_DIR_NAME, TAKEOVER_PROGRESS_FILE } from '../server/services/earlier-database-service.mjs';
17
+ import { NATIVE_BUILD_COMMAND, buildBetterSqlite3, needsSourceBuild } from '../server/services/native-addon-build-service.mjs';
17
18
  import { DB_FILE_NAME, DB_FILE_NAMES, LEGACY_DB_FILE_NAME, resolveDatabaseFile } from '../server/services/database-file.mjs';
18
19
  import { BACKUP_PROGRESS_FILE, BACKUP_WAIT_FILE, backupDatabase, configWarnings, describeBackups, sanitizeReason } from '../server/services/db-backup-service.mjs';
19
20
  import { defaultIsProcessAlive, lockHolderMayRun, processIdentity } from '../server/services/relay-singleton-guard.mjs';
@@ -2407,6 +2408,15 @@ async function runUpdate({ argv = [], env = process.env, logger = console, fetch
2407
2408
  return { code: 1, reason: 'npm-failed' };
2408
2409
  }
2409
2410
  logger.log(`[oar] Installed @oar-sh/oar@${target}.`);
2411
+ // No prebuilt SQLite binding here: build it before the relay restarts on
2412
+ // the new version (native-addon-build-service.mjs).
2413
+ if (needsSourceBuild()) {
2414
+ const build = await buildBetterSqlite3({ packageRoot, env: npm.env, spawnImpl, logger, onOutput: (text) => process.stdout.write(text) });
2415
+ if (!build.ok) {
2416
+ logger.error(`[oar] The relay keeps running the version it has loaded. Run ${NATIVE_BUILD_COMMAND}, then restart it.`);
2417
+ return { code: 1, reason: 'native-build-failed' };
2418
+ }
2419
+ }
2410
2420
 
2411
2421
  // A running relay picks the new code up on restart; ask for the queue-idle
2412
2422
  // deferred one so no in-flight turn is cut off.
@@ -2446,6 +2456,23 @@ async function runUpdate({ argv = [], env = process.env, logger = console, fetch
2446
2456
  return { code: 0, reason: 'updated' };
2447
2457
  }
2448
2458
 
2459
+ /**
2460
+ * `oar build-native [--force]`: the SQLite binding of better-sqlite3 where no
2461
+ * prebuilt one ships (Termux, 32-bit ARM, FreeBSD); elsewhere nothing to do,
2462
+ * so an installer may run it everywhere. `--force` builds anyway, which only
2463
+ * tests the toolchain where a prebuilt binding ships: better-sqlite3 loads
2464
+ * that one first.
2465
+ */
2466
+ async function runBuildNative({ argv = [], env = process.env, logger = console, spawnImpl = spawn } = {}) {
2467
+ const force = argv.includes('--force');
2468
+ if (!force && !needsSourceBuild()) {
2469
+ logger.log(`[oar] better-sqlite3 ships a prebuilt SQLite binding for ${process.platform}-${process.arch}; nothing to build.`);
2470
+ return { code: 0, reason: 'prebuilt' };
2471
+ }
2472
+ const result = await buildBetterSqlite3({ packageRoot: resolvePackageRoot(), force, env, spawnImpl, logger, onOutput: (text) => process.stdout.write(text) });
2473
+ return result.ok ? { code: 0, reason: 'built' } : { code: 1, reason: 'build-failed' };
2474
+ }
2475
+
2449
2476
  /** `oar` without a command: the menu in a terminal, the status and the usage elsewhere. */
2450
2477
  async function runMenuCommand(options = {}) {
2451
2478
  const ctx = cliContext(options);
@@ -2495,6 +2522,7 @@ const COMMANDS = {
2495
2522
  doctor: runDoctor,
2496
2523
  backup: runBackup,
2497
2524
  update: runUpdate,
2525
+ 'build-native': runBuildNative,
2498
2526
  help: async ({ logger = console } = {}) => {
2499
2527
  logger.log(usageText());
2500
2528
  return { code: 0, reason: 'help' };
package/package.json CHANGED
@@ -1,14 +1,18 @@
1
1
  {
2
2
  "name": "@oar-sh/oar",
3
- "version": "0.9.21",
3
+ "version": "0.9.23",
4
4
  "type": "module",
5
5
  "description": "OAR — Open Agent Relay: drive your local coding agents from any browser",
6
6
  "license": "MIT",
7
+ "author": "Simon",
7
8
  "homepage": "https://oar.sh",
8
9
  "repository": {
9
10
  "type": "git",
10
11
  "url": "git+https://github.com/oar-sh/oar.git"
11
12
  },
13
+ "bugs": {
14
+ "url": "https://github.com/oar-sh/oar/issues"
15
+ },
12
16
  "engines": {
13
17
  "node": ">=22.13"
14
18
  },
@@ -41,7 +45,6 @@
41
45
  "LICENSE"
42
46
  ],
43
47
  "scripts": {
44
- "preinstall": "node server/services/package-data-rescue-preinstall.mjs",
45
48
  "start": "node server/server.js",
46
49
  "copilot:relay": "gh copilot -- --allow-all",
47
50
  "test": "node --import ./scripts/node-test-keep-alive.mjs --import ./scripts/node-test-worktree-guard.mjs --test \"server/**/*.test.mjs\" \"shared/**/*.test.mjs\" \".github/extensions/web-relay/**/*.test.mjs\" \"scripts/**/*.test.mjs\"",
@@ -55,20 +58,16 @@
55
58
  "test:e2e:raw": "node ./node_modules/@playwright/test/cli.js test --config tests/playwright.config.mjs"
56
59
  },
57
60
  "dependencies": {
58
- "@anthropic-ai/claude-agent-sdk": "0.3.304",
59
- "@cursor/sdk": "1.0.38",
60
- "@github/copilot-sdk": "1.0.23",
61
- "better-sqlite3": "12.11.1",
62
- "express": "4.22.3",
61
+ "@anthropic-ai/claude-agent-sdk": "0.3.319",
62
+ "@cursor/sdk": "1.3.1",
63
+ "@github/copilot-sdk": "1.0.29",
64
+ "better-sqlite3": "13.0.3",
65
+ "express": "5.3.0",
63
66
  "qrcode-terminal": "0.12.0",
64
- "socket.io": "4.8.3",
65
- "uuid": "14.0.1",
67
+ "socket.io": "4.8.4",
66
68
  "web-push": "3.6.7",
67
- "ws": "8.21.3",
68
- "zod": "4.4.3"
69
- },
70
- "optionalDependencies": {
71
- "tty-console": "1.0.4"
69
+ "ws": "8.22.1",
70
+ "zod": "4.6.7"
72
71
  },
73
72
  "devDependencies": {
74
73
  "@playwright/test": "^1.60.0",
@@ -14,6 +14,7 @@ import {
14
14
  import { createApiClient } from '../../shared/worker-runtime/api-client.mjs';
15
15
  import { createWorkerWebSocketLink } from '../../shared/worker-runtime/worker-websocket-link.mjs';
16
16
  import { createHeartbeatController } from '../../shared/worker-runtime/heartbeat.mjs';
17
+ import { createWorkerKeepalive } from '../../shared/worker-runtime/worker-keepalive.mjs';
17
18
  import { resolveBuildId } from '../../shared/build-id.mjs';
18
19
  import { installWorkerLogFile } from '../../shared/worker-runtime/worker-log-file.mjs';
19
20
  import { createControlPoller } from '../../shared/control-poller.mjs';
@@ -124,8 +125,13 @@ async function main() {
124
125
  getSteeringState: () => buildClaudeCloudSteeringState(runner),
125
126
  });
126
127
 
128
+ // One timer for the heartbeat and the socket's readiness refresh: 10 s
129
+ // while working, 30 s while idle on a relay that allows it.
130
+ let keepalive = null;
127
131
  const wsLink = createWorkerWebSocketLink({
128
132
  serverUrl,
133
+ readyRefreshMs: 0,
134
+ onRelayFrame: () => keepalive?.nudge(),
129
135
  token,
130
136
  dbg,
131
137
  buildId,
@@ -142,10 +148,12 @@ async function main() {
142
148
  }
143
149
  },
144
150
  });
151
+ keepalive = createWorkerKeepalive({ heartbeat, link: wsLink, busyMs: HEARTBEAT_MS });
145
152
 
146
153
  const shutdown = async (signal) => {
147
154
  dbg(`shutting down (${signal})`);
148
155
  try { wsLink.stop(); } catch {}
156
+ try { keepalive.stop(); } catch {}
149
157
  try { heartbeat.stopHeartbeat(); } catch {}
150
158
  try { controlPoller.stop(); } catch {}
151
159
  // The cloud session is left as it is: a turn in flight keeps running
@@ -162,7 +170,7 @@ async function main() {
162
170
  });
163
171
 
164
172
  dbg(`starting session=${sdkSessionId.slice(0, 8)} server=${serverUrl} model=${defaultModel || 'default'} build=${buildId || 'unknown'}`);
165
- heartbeat.startHeartbeat();
173
+ keepalive.start();
166
174
  wsLink.start();
167
175
  // Not awaited: deliveries wait for it inside the runner, and a relay that
168
176
  // cannot say which cloud session this is leaves the binding to the first
@@ -22,7 +22,7 @@
22
22
  * SSL_CERT_FILE and SSL_CERT_DIR: '' empties OpenSSL's and Node's system
23
23
  * store, so those two are pinned only when the worker sets them.
24
24
  *
25
- * Source: the names Claude Code 2.1.304 (the bundled CLI) reads, by a grep
25
+ * Source: the names Claude Code 2.1.319 (the bundled CLI) reads, by a grep
26
26
  * of the binary; the CLI's own lists of endpoints (with their selection
27
27
  * switches and companions) and credentials are in it as they are there. A
28
28
  * deny-list, because a settings layer can only pin names known in advance.
@@ -82,6 +82,7 @@ export const CLAUDE_CLI_PINNED_ENV = Object.freeze([
82
82
  'CLAUDE_CODE_OAUTH_REFRESH_TOKEN',
83
83
  'CLAUDE_CODE_OAUTH_SCOPES',
84
84
  'CLAUDE_CODE_OAUTH_CLIENT_ID',
85
+ 'CLAUDE_CODE_SESSIONS_OAUTH_TOKEN',
85
86
  'ANTHROPIC_PROFILE',
86
87
  'ANTHROPIC_IDENTITY_TOKEN',
87
88
  'ANTHROPIC_IDENTITY_TOKEN_FILE',
@@ -106,6 +107,8 @@ export const CLAUDE_CLI_PINNED_ENV = Object.freeze([
106
107
  'NODE_TLS_REJECT_UNAUTHORIZED',
107
108
  'NODE_EXTRA_CA_CERTS',
108
109
  'CLAUDE_CODE_CERT_STORE',
110
+ 'CLAUDE_CODE_DISABLE_LOOPED_CROSS_CERT_DROP',
111
+ 'CLAUDE_CODE_DISABLE_SELF_NAMED_CERT_DROP',
109
112
  'CLAUDE_CODE_CLIENT_CERT',
110
113
  'CLAUDE_CODE_CLIENT_KEY',
111
114
  'CLAUDE_CODE_CLIENT_KEY_PASSPHRASE',
@@ -2,6 +2,7 @@ import { createSdkMcpServer, query } from '@anthropic-ai/claude-agent-sdk';
2
2
 
3
3
  import { formatClaudeBackgroundCommandLimitLabel } from '../../shared/claude-background-command-limit.mjs';
4
4
  import { parseThinkingDisplay } from '../../shared/claude-thinking.mjs';
5
+ import { PERSON_ONLY_QUESTION_SOURCE } from '../../shared/relay-asks.mjs';
5
6
  import { MEDIA_EMBED_INSTRUCTION_TEXT } from '../../shared/media-embed-instructions.mjs';
6
7
  import { REMOTE_RELAY_TOOL_CALL_TIMEOUT_MS } from '../../shared/remote-relay-tool-core.mjs';
7
8
  import { createPreviewToolDefinition } from './claude-preview-tool.mjs';
@@ -631,11 +632,151 @@ export async function readPlanUsage(turn, dbg = () => {}, { timeoutMs = 10000 }
631
632
  }
632
633
  }
633
634
 
635
+ // Asks Claude Code marks as person-only: a removal of a workspace or system
636
+ // directory, a target it cannot resolve. No rule, mode or automatic approval
637
+ // may answer them, so allow-all parity stops here. The SDK does not forward
638
+ // the wire's `classifier_approvable`, so the safety-check kinds count only
639
+ // together with `suppressAlwaysAllowRule`; a sensitive-file safety check
640
+ // (an edit under .git/ or .claude/) carries no such flag and stays allowed.
641
+ // A user's `permissions.ask` rule (`matchedAskRule`) changes nothing: the
642
+ // flags decide, and an ask that rule forced without them stays allowed.
643
+ const PERSON_ONLY_REASON_TYPES = new Set(['safetyCheck', 'subcommandResults']);
644
+ export const PERSON_ONLY_ALLOW_CHOICE = 'Allow once';
645
+ export const PERSON_ONLY_DENY_CHOICE = 'Deny';
646
+ export { PERSON_ONLY_QUESTION_SOURCE };
647
+ // The card's first line is fixed text: the relay's console log prints the
648
+ // first 60 characters of a question, and the command must not reach it.
649
+ const PERSON_ONLY_QUESTION_LEAD = 'Claude Code marks this as person-only: no rule or automatic approval may allow it. Allow it once?';
650
+ const PERSON_ONLY_RATIONALE = 'Claude Code needs a person to approve this before it runs. Nothing runs until you choose.';
651
+ // The card shows the whole command or nothing: a cut could hide the removal.
652
+ export const PERSON_ONLY_COMMAND_MAX = 8000;
653
+ export const PERSON_ONLY_TOO_LONG_MESSAGE = 'Not run: too long to show for approval; issue the removal as a short command of its own.';
654
+ const PERSON_ONLY_REASON_MAX = 400;
655
+ const PERSON_ONLY_TYPED_ANSWER_MAX = 500;
656
+
657
+ export function isPersonOnlyAsk(options) {
658
+ if (!options || typeof options !== 'object') return false;
659
+ if (options.defaultToNo === true) return true;
660
+ return PERSON_ONLY_REASON_TYPES.has(options.decisionReasonType)
661
+ && options.suppressAlwaysAllowRule === true;
662
+ }
663
+
664
+ // Characters that hide or reorder text: C0 and C1 controls, bidi controls,
665
+ // zero-width characters and the Unicode line separators. On the card each
666
+ // one shows as a visible \u{XXXX} escape (the body is pre-wrap plain text).
667
+ const INVISIBLE_CHARACTERS = /[\u0000-\u001f\u007f-\u009f\u061c\u200b-\u200f\u2028\u2029\u202a-\u202e\u2066-\u2069\ufeff]/g;
668
+
669
+ function escapeInvisible(text) {
670
+ return String(text ?? '').replace(INVISIBLE_CHARACTERS, (char) => (
671
+ `\\u{${char.codePointAt(0).toString(16).toUpperCase().padStart(4, '0')}}`
672
+ ));
673
+ }
674
+
675
+ // The CLI's reason may carry ANSI colour (sdk.d.ts): drop well-formed
676
+ // sequences, then escape what is left, newlines included (one line).
677
+ function reasonLine(value) {
678
+ const text = String(value ?? '')
679
+ .replace(/\u001b\][^\u0007\u001b]*(?:\u0007|\u001b\\)/g, '')
680
+ .replace(/\u001b\[[0-9;?]*[ -/]*[@-~]/g, '')
681
+ .trim();
682
+ return capText(escapeInvisible(text), PERSON_ONLY_REASON_MAX);
683
+ }
684
+
685
+ function capText(text, max) {
686
+ if (text.length <= max) return text;
687
+ return `${text.slice(0, max)}… (${text.length - max} more characters)`;
688
+ }
689
+
690
+ function personOnlySubject(input) {
691
+ if (typeof input?.command === 'string') return { label: 'command', text: input.command };
692
+ for (const key of ['file_path', 'notebook_path', 'path']) {
693
+ if (typeof input?.[key] === 'string') return { label: 'path', text: input[key] };
694
+ }
695
+ let json = '';
696
+ try { json = JSON.stringify(input ?? {}); } catch { json = ''; }
697
+ return { label: 'input', text: json };
698
+ }
699
+
700
+ // Each line escaped and behind "│ ", each line break marked "⏎", so no line
701
+ // of the command can pass for the card's own text.
702
+ function subjectLines(text) {
703
+ return String(text ?? '').split('\n').map(escapeInvisible);
704
+ }
705
+
706
+ function subjectTooLong(input) {
707
+ return subjectLines(personOnlySubject(input).text).join('\n').length > PERSON_ONLY_COMMAND_MAX;
708
+ }
709
+
710
+ export function personOnlyQuestionText(toolName, input, decisionReason) {
711
+ const subject = personOnlySubject(input);
712
+ const lines = subjectLines(subject.text);
713
+ const parts = [PERSON_ONLY_QUESTION_LEAD];
714
+ const reason = reasonLine(decisionReason);
715
+ if (reason) parts.push(`Reason: ${reason}`);
716
+ const count = lines.length > 1 ? `, ${lines.length} lines` : '';
717
+ parts.push(`${toolName || 'Tool'} ${subject.label}${count}:\n│ ${lines.join(' ⏎\n│ ')}`);
718
+ return parts.join('\n\n');
719
+ }
720
+
721
+ function personOnlyDenyMessage(decisionReason, why) {
722
+ const reason = reasonLine(decisionReason);
723
+ return `Not run: Claude Code marks this as person-only${reason ? ` (${reason})` : ''}, and ${why}. `
724
+ + 'Do not work around it by splitting, scripting or re-issuing it through another tool.';
725
+ }
726
+
727
+ // Allow/Deny card over the question bridge, no typed replies. Only the exact
728
+ // "Allow once" allows, and never with `updatedPermissions`: the next such ask
729
+ // asks again.
730
+ async function askPersonOnly({ askUserBridge, toolName, input, options, signal, dbg }) {
731
+ const reason = options?.decisionReason;
732
+ const kind = options?.decisionReasonType || 'defaultToNo';
733
+ const deny = (why, outcome) => {
734
+ dbg('person-only ask', toolName, kind, outcome);
735
+ return { behavior: 'deny', message: personOnlyDenyMessage(reason, why) };
736
+ };
737
+ if (signal?.aborted) return deny('the turn was stopped', 'aborted');
738
+ if (subjectTooLong(input)) {
739
+ dbg('person-only ask', toolName, kind, 'too-long');
740
+ return { behavior: 'deny', message: PERSON_ONLY_TOO_LONG_MESSAGE };
741
+ }
742
+ if (!askUserBridge) return deny('the relay could not show the question', 'no-bridge');
743
+ const question = personOnlyQuestionText(toolName, input, reason);
744
+ let result;
745
+ try {
746
+ result = await askUserBridge.handleAskUserQuestion({
747
+ questions: [{
748
+ question,
749
+ header: 'Safety check',
750
+ options: [{ label: PERSON_ONLY_ALLOW_CHOICE }, { label: PERSON_ONLY_DENY_CHOICE }],
751
+ }],
752
+ }, {
753
+ signal,
754
+ source: PERSON_ONLY_QUESTION_SOURCE,
755
+ rationale: PERSON_ONLY_RATIONALE,
756
+ allowFreeform: false,
757
+ });
758
+ } catch (error) {
759
+ if (signal?.aborted) return deny('the turn was stopped', 'aborted');
760
+ dbg('person-only ask bridge failed', error?.message || String(error));
761
+ return deny('the relay could not show the question', 'bridge-failed');
762
+ }
763
+ if (signal?.aborted) return deny('the turn was stopped', 'aborted');
764
+ if (result?.timedOut) return deny('no answer came before the timeout', 'timed-out');
765
+ const answer = String(result?.answers?.[question] ?? '').trim();
766
+ if (answer === PERSON_ONLY_ALLOW_CHOICE) {
767
+ dbg('person-only ask', toolName, kind, 'allowed');
768
+ return { behavior: 'allow', updatedInput: input };
769
+ }
770
+ if (!answer || answer === PERSON_ONLY_DENY_CHOICE) return deny('the user denied it', 'denied');
771
+ return deny(`the user denied it: ${capText(escapeInvisible(answer), PERSON_ONLY_TYPED_ANSWER_MAX)}`, 'denied-other');
772
+ }
773
+
634
774
  /**
635
775
  * Build the `canUseTool` callback for one turn. Everything is auto-allowed
636
- * (parity with the Copilot workers' `--allow-all` posture); the only
637
- * interceptions are AskUserQuestion (bridged to relay question cards) and
638
- * ExitPlanMode (surfaces the plan_ready board).
776
+ * (parity with the Copilot workers' `--allow-all` posture) except the asks
777
+ * Claude Code marks as person-only (an Allow/Deny relay card, see
778
+ * isPersonOnlyAsk); the other interceptions are AskUserQuestion (bridged to
779
+ * relay question cards) and ExitPlanMode (surfaces the plan_ready board).
639
780
  *
640
781
  * ExitPlanMode is DENIED after the board is posted: allowing it tells the CLI
641
782
  * the user approved the plan, and the same turn rolls straight into
@@ -658,8 +799,12 @@ export function createCanUseTool({
658
799
  onExitPlanMode,
659
800
  dbg = () => {},
660
801
  } = {}) {
661
- return async function canUseTool(toolName, input, { signal } = {}) {
802
+ return async function canUseTool(toolName, input, options = {}) {
803
+ const signal = options?.signal;
662
804
  const normalizedToolName = String(toolName || '').trim();
805
+ if (isPersonOnlyAsk(options)) {
806
+ return askPersonOnly({ askUserBridge, toolName: normalizedToolName, input, options, signal, dbg });
807
+ }
663
808
  try {
664
809
  if (normalizedToolName === 'AskUserQuestion' && askUserBridge) {
665
810
  const { answers } = await askUserBridge.handleAskUserQuestion(input, { signal });