@oar-sh/oar 0.9.19 → 0.9.21

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 (190) hide show
  1. package/.github/extensions/web-relay/polling/polling-loop.mjs +2 -26
  2. package/.github/extensions/web-relay/skills/question-routing-hooks.mjs +2 -26
  3. package/CHANGELOG.md +529 -0
  4. package/README.md +14 -14
  5. package/bin/oar.js +392 -98
  6. package/package.json +4 -2
  7. package/server/claude-cloud-worker/claude-cloud-event-normalizer.mjs +60 -0
  8. package/server/claude-cloud-worker/claude-cloud-session-process.mjs +258 -1
  9. package/server/claude-cloud-worker/claude-cloud-session-worker.mjs +22 -1
  10. package/server/claude-cloud-worker/claude-cloud-worker-steering.mjs +34 -0
  11. package/server/claude-worker/claude-remote-relay-tool.mjs +1 -0
  12. package/server/claude-worker/claude-session-process.mjs +10 -0
  13. package/server/claude-worker/claude-session-worker-main.mjs +8 -1
  14. package/server/claude-worker/claude-turn-publisher.mjs +163 -8
  15. package/server/claude-worker/claude-usage-totals.mjs +58 -0
  16. package/server/copilot-worker/copilot-plan-board.mjs +4 -7
  17. package/server/copilot-worker/copilot-sdk-event-normalizer.mjs +86 -3
  18. package/server/copilot-worker/copilot-sdk-session-process.mjs +131 -5
  19. package/server/copilot-worker/copilot-sdk-session-worker.mjs +8 -1
  20. package/server/cursor-worker/cursor-session-worker.mjs +12 -1
  21. package/server/cursor-worker/cursor-turn-runner.mjs +42 -6
  22. package/server/cursor-worker/cursor-worker-steering.mjs +20 -0
  23. package/server/db-schema.mjs +18 -0
  24. package/server/deepseek-worker/deepseek-claude-backend.mjs +14 -0
  25. package/server/grok-worker/grok-background-tasks.mjs +9 -0
  26. package/server/grok-worker/grok-session-worker.mjs +12 -1
  27. package/server/grok-worker/grok-turn-runner.mjs +47 -6
  28. package/server/grok-worker/grok-worker-steering.mjs +23 -0
  29. package/server/migrations/0001-add-structured-answer.mjs +5 -7
  30. package/server/migrations/0002-recent-workspace-roots-path-key.mjs +5 -7
  31. package/server/migrations/0003-push-subscriptions.mjs +5 -7
  32. package/server/migrations/0004-steer-settle-markers.mjs +5 -7
  33. package/server/migrations/0005-remote-relays.mjs +5 -7
  34. package/server/migrations/0006-workspace-root-settings.mjs +5 -7
  35. package/server/migrations/0007-claude-cloud.mjs +5 -7
  36. package/server/migrations/0008-message-pins.mjs +5 -7
  37. package/server/migrations/0009-subagent-timeline.mjs +5 -7
  38. package/server/migrations/0010-subagent-timeline-pruning.mjs +5 -7
  39. package/server/migrations/0011-relay-event-subscriptions.mjs +5 -7
  40. package/server/migrations/0012-cross-relay-event-subscriptions.mjs +5 -7
  41. package/server/migrations/0013-previews.mjs +5 -7
  42. package/server/migrations/0014-conversation-groups.mjs +5 -7
  43. package/server/migrations/0015-conversation-group-position.mjs +5 -7
  44. package/server/migrations/0016-share-access-log.mjs +5 -7
  45. package/server/migrations/0017-conversation-history-indexes.mjs +5 -7
  46. package/server/migrations/0018-background-task-rows.mjs +5 -7
  47. package/server/migrations/0019-relay-event-subscription-options.mjs +5 -7
  48. package/server/migrations/0020-conversation-may-create-sessions.mjs +5 -7
  49. package/server/migrations/0021-conversation-group-agent-marks.mjs +5 -7
  50. package/server/migrations/0022-relay-schedules.mjs +5 -7
  51. package/server/migrations/0023-relay-asks.mjs +5 -7
  52. package/server/migrations/0024-deepseek-usage-totals.mjs +5 -7
  53. package/server/migrations/0025-deepseek-usage-requests-daily.mjs +5 -7
  54. package/server/migrations/0026-deepseek-usage-served-models.mjs +5 -7
  55. package/server/migrations/0027-deepseek-usage-checkpoint-runs.mjs +5 -7
  56. package/server/migrations/0028-deepseek-usage-checkpoint-request-runs.mjs +5 -7
  57. package/server/migrations/0029-deepseek-balance-rises.mjs +5 -7
  58. package/server/migrations/0030-deepseek-usage-actuals.mjs +5 -7
  59. package/server/migrations/0031-queue-provider.mjs +5 -7
  60. package/server/migrations/0032-runtime-session-billing-switch.mjs +5 -7
  61. package/server/migrations/0033-claude-api-usage-checkpoints.mjs +5 -7
  62. package/server/migrations/0034-claude-api-usage-totals.mjs +5 -7
  63. package/server/migrations/0035-conversation-plan-only.mjs +87 -0
  64. package/server/migrations/0036-token-usage.mjs +267 -0
  65. package/server/public/app/about-ui.js +1 -1
  66. package/server/public/app/about-view.mjs +106 -3
  67. package/server/public/app/action-confirmations.js +1 -1
  68. package/server/public/app/api-client.js +6 -3
  69. package/server/public/app/ask-user-view.js +20 -6
  70. package/server/public/app/attachments-view.js +1 -1
  71. package/server/public/app/back-guard.mjs +31 -5
  72. package/server/public/app/bootstrap.js +611 -70
  73. package/server/public/app/chat-actions-menu-state.mjs +139 -4
  74. package/server/public/app/chat-header-meta.mjs +27 -0
  75. package/server/public/app/claude-billing-toggle-ui.js +2 -1
  76. package/server/public/app/claude-cloud-conversation-ui.js +2 -2
  77. package/server/public/app/composer-control-state.mjs +12 -2
  78. package/server/public/app/composer-settings-label.mjs +90 -0
  79. package/server/public/app/composer-settings-ui.js +442 -0
  80. package/server/public/app/context-menu.mjs +36 -5
  81. package/server/public/app/context-usage-view.mjs +16 -0
  82. package/server/public/app/conversation-deep-link.mjs +15 -11
  83. package/server/public/app/conversation-group-drag.mjs +12 -2
  84. package/server/public/app/conversation-group-order.mjs +9 -2
  85. package/server/public/app/conversation-group-state.mjs +47 -10
  86. package/server/public/app/conversation-origin-ui.js +17 -1
  87. package/server/public/app/conversation-view.js +20 -2
  88. package/server/public/app/cwd-menu-placement.mjs +18 -6
  89. package/server/public/app/cwd-picker.js +36 -6
  90. package/server/public/app/emoji-view.js +1 -1
  91. package/server/public/app/font-scaling.js +26 -0
  92. package/server/public/app/header-priority-actions.mjs +234 -0
  93. package/server/public/app/hidden-messages-view.mjs +4 -0
  94. package/server/public/app/inbox-ui.js +50 -2
  95. package/server/public/app/inbox-view.mjs +141 -5
  96. package/server/public/app/journal-view.js +311 -58
  97. package/server/public/app/phone-landscape-ui.js +225 -0
  98. package/server/public/app/phone-landscape.mjs +99 -0
  99. package/server/public/app/phone-typing-state.mjs +104 -0
  100. package/server/public/app/pinned-messages-view.mjs +5 -0
  101. package/server/public/app/plan-board-shared.mjs +49 -0
  102. package/server/public/app/plan-usage-view.mjs +1 -1
  103. package/server/public/app/push-settings.js +2 -2
  104. package/server/public/app/pwa-install.js +24 -3
  105. package/server/public/app/relay-board-inline.mjs +4 -1
  106. package/server/public/app/relay-board-view.js +38 -10
  107. package/server/public/app/relay-event-subscriptions-ui.js +71 -22
  108. package/server/public/app/relay-event-view.mjs +42 -0
  109. package/server/public/app/relay-restart-ui.js +2 -2
  110. package/server/public/app/relay-schedule-view.mjs +32 -6
  111. package/server/public/app/relay-schedules-ui.js +69 -20
  112. package/server/public/app/remote-relay-origin-view.mjs +41 -23
  113. package/server/public/app/session-history-ui.js +250 -0
  114. package/server/public/app/session-history.mjs +599 -0
  115. package/server/public/app/settings-modal.js +61 -6
  116. package/server/public/app/settings-tabs.js +12 -0
  117. package/server/public/app/share-state.js +1 -1
  118. package/server/public/app/shared-conversations-settings.js +2 -2
  119. package/server/public/app/socket-handlers.js +15 -4
  120. package/server/public/app/status-view.mjs +18 -2
  121. package/server/public/app/store.js +87 -16
  122. package/server/public/app/subagent-viewer.mjs +27 -2
  123. package/server/public/app/tab-strip-fade.mjs +101 -0
  124. package/server/public/app/tap-actions.js +66 -0
  125. package/server/public/app/usage-limit-ui.js +63 -2
  126. package/server/public/app/viewer-history.mjs +13 -2
  127. package/server/public/index.html +2258 -1585
  128. package/server/public/sw.js +8 -0
  129. package/server/relay-heap-guard.mjs +14 -1
  130. package/server/relay-self-restart.mjs +83 -2
  131. package/server/relay-tools.md +5 -3
  132. package/server/repositories/message-repository.mjs +54 -1
  133. package/server/repositories/remote-relay-repository.mjs +10 -0
  134. package/server/repositories/session-repository.mjs +25 -1
  135. package/server/routes/claude-cloud-routes.mjs +19 -0
  136. package/server/routes/messages-routes.mjs +144 -1
  137. package/server/routes/relay-ask-routes.mjs +138 -8
  138. package/server/routes/relay-board-routes.mjs +55 -40
  139. package/server/routes/sessions-routes.mjs +144 -14
  140. package/server/server-runtime.mjs +322 -51
  141. package/server/services/about-service.mjs +81 -53
  142. package/server/services/cache-rebuild-service.mjs +7 -0
  143. package/server/services/claude-billing-swap-service.mjs +7 -20
  144. package/server/services/claude-cloud-session-service.mjs +47 -5
  145. package/server/services/conversation-preferences-service.mjs +31 -0
  146. package/server/services/database-file.mjs +413 -0
  147. package/server/services/db-backup-service.mjs +809 -0
  148. package/server/services/deepseek-usage-import.mjs +6 -9
  149. package/server/services/delete-archive-service.mjs +3 -0
  150. package/server/services/earlier-database-service.mjs +69 -126
  151. package/server/services/oar-cli-helpers.mjs +214 -12
  152. package/server/services/oar-cli-menu.mjs +1 -0
  153. package/server/services/oar-state-migration-service.mjs +33 -13
  154. package/server/services/package-data-rescue.mjs +4 -2
  155. package/server/services/plan-usage-claude-api.mjs +14 -15
  156. package/server/services/plan-usage-contract.mjs +6 -2
  157. package/server/services/plan-usage-deepseek.mjs +1 -1
  158. package/server/services/plan-usage-tokens.mjs +248 -0
  159. package/server/services/push-dispatch-service.mjs +134 -9
  160. package/server/services/relay-cli-launcher-service.mjs +3 -0
  161. package/server/services/relay-event-service.mjs +30 -0
  162. package/server/services/relay-question-context.mjs +11 -0
  163. package/server/services/relay-restart-picture.mjs +67 -22
  164. package/server/services/remote-inbox-watch-service.mjs +204 -0
  165. package/server/services/remote-relay-caller-context.mjs +80 -7
  166. package/server/services/remote-relay-dispatcher.mjs +101 -19
  167. package/server/services/restart-resume-service.mjs +5 -2
  168. package/server/services/session-activity-tracker.mjs +1 -1
  169. package/server/services/session-launch-mutex-service.mjs +23 -0
  170. package/server/services/session-worker-launch-service.mjs +12 -3
  171. package/server/services/session-worker-registry-service.mjs +36 -0
  172. package/server/services/session-worker-stop-service.mjs +27 -6
  173. package/server/services/session-worker-websocket-service.mjs +40 -16
  174. package/server/services/token-usage-service.mjs +430 -0
  175. package/server/services/update-install-service.mjs +13 -0
  176. package/server/services/usage-limit-pause-service.mjs +8 -1
  177. package/server/services/worker-recycle-service.mjs +772 -0
  178. package/server/services/worker-replacement.mjs +197 -0
  179. package/server/services/worker-scope.mjs +77 -0
  180. package/shared/build-id.mjs +128 -0
  181. package/shared/plan-board-actions.mjs +130 -0
  182. package/shared/plan-only.mjs +43 -0
  183. package/shared/relay-asks.mjs +3 -0
  184. package/shared/relay-events.mjs +38 -4
  185. package/shared/remote-relay-contract.mjs +66 -11
  186. package/shared/turn-token-usage.mjs +144 -0
  187. package/shared/worker-runtime/heartbeat.mjs +5 -0
  188. package/shared/worker-runtime/simple-steering-state.mjs +38 -0
  189. package/shared/worker-runtime/worker-websocket-link.mjs +23 -0
  190. package/shared/worker-runtime/working-directory.mjs +1 -1
@@ -8,6 +8,7 @@ import { getActiveSession } from "../runtime/session-registry.mjs";
8
8
  import { DEFAULT_QUESTION_TIMEOUT_MS } from "../../../../shared/question-timeout.mjs";
9
9
  import { waitForRelayQuestion } from "../../../../shared/question-wait.mjs";
10
10
  import { EMPTY_TURN_COMPLETION_NOTE } from "../../../../shared/empty-turn-completion.mjs";
11
+ import { planBoardActionsFromCli } from "../../../../shared/plan-board-actions.mjs";
11
12
  import { stripPromptContextPrefix } from "../skills/prompt-context.mjs";
12
13
  import {
13
14
  isOpenAIImageTerminalError,
@@ -319,34 +320,9 @@ function extractToolCallInputObject(value, toolName, depth = 0) {
319
320
  }
320
321
 
321
322
  function normalizePlanBoardActions(rawActions, recommendedAction = "") {
322
- const ids = Array.isArray(rawActions)
323
- ? rawActions
324
- .map((entry) => {
325
- if (typeof entry === "string") return entry.trim().toLowerCase();
326
- if (entry && typeof entry === "object") {
327
- return String(entry.id || entry.actionId || entry.value || "").trim().toLowerCase();
328
- }
329
- return "";
330
- })
331
- .filter(Boolean)
332
- : [];
333
- const sourceIds = ids.length ? ids : ["autopilot", "interactive", "exit_only"];
334
- const seen = new Set();
335
- const deduped = [];
336
- for (const id of sourceIds) {
337
- if (seen.has(id)) continue;
338
- seen.add(id);
339
- deduped.push(id);
340
- }
341
323
  const recommended = String(recommendedAction || "").trim().toLowerCase();
342
324
  return {
343
- actions: deduped.map((id) => {
344
- if (id === "autopilot_fleet") return { id, label: "Implement with autopilot fleet", mode: "autopilot" };
345
- if (id === "autopilot") return { id, label: "Implement in autopilot", mode: "autopilot" };
346
- if (id === "interactive") return { id, label: "Stop here and prompt myself", mode: "agent" };
347
- if (id === "exit_only") return { id, label: "Stop here", mode: "agent" };
348
- return { id, label: id.replace(/[_-]+/g, " "), mode: null };
349
- }),
325
+ actions: planBoardActionsFromCli(rawActions),
350
326
  recommendedAction: recommended || null,
351
327
  };
352
328
  }
@@ -6,6 +6,7 @@ import {
6
6
  flatAnswerToStructured,
7
7
  summarizeStructuredAnswer,
8
8
  } from "../../../../shared/question-schema.mjs";
9
+ import { planBoardActionsFromCli } from "../../../../shared/plan-board-actions.mjs";
9
10
 
10
11
  function extractSubagentRunIdFromRequest(request) {
11
12
  const agentId = request?.agentId || request?.data?.agentId || null;
@@ -57,34 +58,9 @@ export function createQuestionRoutingHooks({
57
58
  const allowToolUse = { permissionDecision: "allow" };
58
59
 
59
60
  function normalizePlanBoardActions(rawActions, recommendedAction = "") {
60
- const ids = Array.isArray(rawActions)
61
- ? rawActions
62
- .map((entry) => {
63
- if (typeof entry === "string") return entry.trim().toLowerCase();
64
- if (entry && typeof entry === "object") {
65
- return String(entry.id || entry.actionId || entry.value || "").trim().toLowerCase();
66
- }
67
- return "";
68
- })
69
- .filter(Boolean)
70
- : [];
71
- const sourceIds = ids.length ? ids : ["autopilot", "interactive", "exit_only"];
72
- const seen = new Set();
73
- const deduped = [];
74
- for (const id of sourceIds) {
75
- if (seen.has(id)) continue;
76
- seen.add(id);
77
- deduped.push(id);
78
- }
79
61
  const recommended = String(recommendedAction || "").trim().toLowerCase();
80
62
  return {
81
- actions: deduped.map((id) => {
82
- if (id === "autopilot_fleet") return { id, label: "Implement with autopilot fleet", mode: "autopilot" };
83
- if (id === "autopilot") return { id, label: "Implement in autopilot", mode: "autopilot" };
84
- if (id === "interactive") return { id, label: "Stop here and prompt myself", mode: "agent" };
85
- if (id === "exit_only") return { id, label: "Stop here", mode: "agent" };
86
- return { id, label: id.replace(/[_-]+/g, " "), mode: null };
87
- }),
63
+ actions: planBoardActionsFromCli(rawActions),
88
64
  recommendedAction: recommended || null,
89
65
  };
90
66
  }
package/CHANGELOG.md CHANGED
@@ -3,6 +3,535 @@
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.21] — 2026-11-01
7
+
8
+ Highlights: the chat screen is more compact. The mode, model, effort,
9
+ context window and billing of a chat move behind **⚙** in a slim label row
10
+ above the message box, such as `🔒 Opus 5.5 · high · Auto`. The header menu
11
+ is now **︙**, and the header's icon buttons stay there while there is room
12
+ and move into it when there is not. Spacing is about halved across the app,
13
+ with controls and hit areas at 28 px. Back and forward walk through the
14
+ conversations you opened, with `?conv=<id>` in the address bar, and back
15
+ closes what is open on top first. A pinch, Ctrl/⌘+wheel or Text scaling
16
+ zooms the whole app, not only the text. A phone held sideways keeps the
17
+ phone's layout. A session group folds while one of its chats is open, the
18
+ filter row folds or unfolds all groups at once, and a group's name takes
19
+ its sessions' colour. The origin, watched and scheduled pills of a sidebar
20
+ row become one **ⓘ**. After a deploy, the relay replaces each session worker
21
+ still on the old code once its session is idle, so a restart no longer
22
+ leaves old worker code running.
23
+
24
+ ### Added
25
+
26
+ - **Chat settings behind ⚙ in the composer.** A slim label row above the
27
+ message box holds **⚙**, a label that says what the next message runs
28
+ with (model · effort · mode, such as `🔒 Opus 5.5 · high · Auto`, with the
29
+ context window added when it is not the model's default), the billing pill
30
+ of a Claude chat, the file browser, **📎** and, on a desktop, **🙂**. **⚙**,
31
+ or a tap on the label, opens the settings: **Mode**, **Model** with the
32
+ lock note under it, **Effort**, **Context window**, **Billing** and
33
+ **🧠 Context usage**. A tap on one part of the label opens them at that
34
+ field, and the **Subscription** or **API key** pill at **Billing**. On a
35
+ desktop they open as a popover above the row, on a phone as a sheet from
36
+ the bottom; **✕**, Escape, a tap outside or back closes them. An image
37
+ chat shows **Quality** and **Image size** in place of Effort and Context
38
+ window; a Claude Cloud chat has only **Model**. The label reads **Auto**
39
+ for the autopilot mode (the settings keep the full name) and **Plan 🔒**
40
+ on a plan-only session.
41
+ - **Back and forward through conversations.** Each conversation you open is
42
+ one step in the browser's history, with `?conv=<id>` in the address bar,
43
+ so back and forward walk through them, and a reload or a copied address
44
+ opens that conversation. The tab's title is the conversation's title.
45
+ Back first closes what is open on top: the session list on a phone, the
46
+ Inbox or Status view, the composer's settings, the **︙** menu, a row's
47
+ menu, dialogs, the git, tmux and annotate views, the watch and schedule
48
+ panels, the emoji picker and the Change CWD folder list. On a phone, back
49
+ from the first conversation you opened shows the session list, then the
50
+ installed app's *Press back again to close the app* as before.
51
+ - A link to a conversation the relay does not know (deleted, or a wrong
52
+ id) clears the view with a notice and keeps the last conversation; back
53
+ and forward step over it from then on, and nothing opens by itself until
54
+ you pick a conversation. A link to an archived conversation shows its
55
+ title.
56
+ - No address is written before the first conversation shows, so a link
57
+ survives an early tap. A jump from the Inbox to a question replaces the
58
+ history entry instead of adding one.
59
+ - A notification tap still opens `?push_conv=<id>`; the address then
60
+ reads `?conv=<id>`.
61
+ - **Zoom scales the whole app.** A pinch, Ctrl+wheel (⌘+wheel on a Mac) or
62
+ **⚙️ Settings → General → Text scaling** now zooms text, buttons,
63
+ spacing, the sidebar, menus and dialogs alike, so the layout keeps its
64
+ proportions; at 100 % nothing changes. Popovers and sheets are placed
65
+ anew on a change. The new-chat dialog's selects and inputs follow the
66
+ zoom too (they stayed at the browser's fixed 13.33 px).
67
+ - **A phone held sideways keeps the phone's layout.** The header is one
68
+ line. While the keyboard is up, the composer's label row folds into a
69
+ floating **⚙** that opens the settings and keeps the keyboard up, and the
70
+ background-task line into a floating **🤖**; both come back when the
71
+ keyboard goes down. Picking a conversation closes the session list and
72
+ does not bring up the keyboard (a long press does not pick). The **︙**
73
+ menu gains entries for the watch list, the schedules and the origin.
74
+ - **Fold a group with its chat open, and fold all.** A group folds even
75
+ while one of its sessions is open; that session stays under the folded
76
+ head until you open another one, and opening a member of a folded group
77
+ leaves it folded. A drop on that kept row is a drop on the folded head.
78
+ While the list has groups, the filter row has a fold-all button: **➖**
79
+ folds every group, **➕** unfolds them all once all are folded. It counts
80
+ as your own choice for each group and is disabled while the list is
81
+ filtered. The Archived view keeps its folds apart from the live list.
82
+ - **A group's name takes its sessions' colour:** red while one waits for
83
+ you, bright green while one works, dark green while one is idle with its
84
+ session ready, else the plain title colour. Archived heads stay muted.
85
+ - **ⓘ on sidebar rows.** The origin, watched and scheduled pills of a row
86
+ fold into one **ⓘ** in its second line. Its menu lists each with its
87
+ action: *Started by an agent in "…"* (or *on relay "…"*), which opens
88
+ that conversation or the session on the other relay, *Watched by …* with
89
+ the watchers' names, and *Scheduled · next …* (or *N schedules · next
90
+ …*). A member under the head row of the session that started it leaves
91
+ the origin out.
92
+ - **Session workers are recycled after a deploy.** Every worker states the
93
+ build id (version and commit) it started on; a worker on another build than
94
+ the relay, or one too old to say, is stopped and relaunched by the relay
95
+ once it is idle, the way **Change CWD → 🚀 Set new CWD and (re)launch**
96
+ does, so a restart no longer leaves old worker code running. An archived conversation's worker is
97
+ stopped for good. `/api/status` and the About modal show which build each
98
+ worker runs.
99
+ - Idle means no running turn, open question card, compaction, background
100
+ task or message on its way in, and the worker's CLI has ended. A CLI
101
+ ends by itself about 10 minutes after the last delivery or turn, so a
102
+ session is usually replaced some 10 minutes after its last turn. A
103
+ worker too old to say whether its CLI runs is replaced after 10 minutes
104
+ without a delivery or turn end. At most two are replaced at a time, one
105
+ start every 3 seconds, and each session once per relay build.
106
+ - Nothing is lost: the new worker resumes the native session, and a
107
+ message sent meanwhile waits and goes to the new worker. A Cursor
108
+ session is the exception for background shells and tasks, which its SDK
109
+ does not report: only the 10-minute quiet window protects them.
110
+ - **About OAR** shows *Workers: 14, all on this build* or *Workers: 14,
111
+ 3 on an older build*. `/api/status` gives the relay's `buildId` and,
112
+ per worker, `buildId`, `startedAt` and `stale`, and the recycle's
113
+ `staleCount`, replacements and the sessions it waits on with the reason.
114
+ relay_tool's session items carry `worker { kind, buildId, stale }`.
115
+ The relay log has `[worker-recycle]` lines for each replacement and,
116
+ at most once a minute per session, what it waits for.
117
+ - A Claude Cloud worker opens its event stream from the stored cursor at
118
+ start, so turns the cloud session starts by itself are followed after a
119
+ replacement too. New worker route `GET /api/claude-cloud-session`.
120
+ - The troubleshooting guide has a section, *Workers after a deploy*.
121
+
122
+ ### Changed
123
+
124
+ - **The header menu is ︙, and the icon buttons move into it when the
125
+ header is narrow.** **📍** pins and **🙈** hidden messages (while there
126
+ are any), **🔍** search, **🧠** context usage, **📊** Check Usage,
127
+ **☀️/🌙** Day/Night and **⛶** fullscreen stay in the header while there is
128
+ room and move into **︙** under their full names otherwise, the last ones
129
+ first. The menu stays inside the window, is capped in height and scrolls.
130
+ Fullscreen is disabled where the browser has none. Relay refusals, log
131
+ lines and help texts that named the `⋯` menu now name **︙**.
132
+ - **The open-question banner sits between the title and the header
133
+ buttons,** so **︙** always stays at the right edge. Where room is short
134
+ (a phone held sideways, a narrow desktop window) it shrinks to a **❓ N**
135
+ chip; on a wider header it takes at most 40 % of the row and ends in "…".
136
+ A long title ends in "…" too. When even the chip, **☰** and **︙** do not
137
+ fit next to the title, as at 150 % zoom with the sidebar open, the banner
138
+ takes a row of its own; on a phone held upright it always does.
139
+ - **About half the spacing, and 28 px controls.** Paddings, margins and
140
+ gaps in menus, the sidebar, dialogs, Settings, cards, bubbles and panels
141
+ are about halved, and controls that were 30 to 46 px high are 28 px, also
142
+ dialog buttons and tabs. Every hit area is at least 28 px at 100 %: the
143
+ row's **ⓘ**, fold, archive and delete, the live reply's **Stop** and
144
+ **💭 📃 🛠️** follow buttons, the Previews tab's **Open**, **Copy link**
145
+ and **Close**, the phone's header chips and the
146
+ question banner, the sidebar head buttons, Settings fields, selects,
147
+ checkboxes, radios and sliders, the repo browser's toolbar, the group
148
+ picker, the dialogs' **×** and **↻**, the Relays **Add**, the Sharing
149
+ row buttons, **Unsubscribe** and **Cancel**. Small labels and links get
150
+ an invisible 28 px hit area: the repo browser's Focus and crumbs, tree
151
+ rows, Sharing row titles, DeepSeek **Advanced**, the plan-only label, the
152
+ waiting pill, the agent-exchange and activity folds and the header chips.
153
+ - **On a phone the lines under the title are one row** that scrolls
154
+ sideways, with short labels (`↗ title`, `👁 3`, `⏰ 04:15`, the folder's
155
+ last name), and the open-question banner gets a row of its own.
156
+ - **Dialogs on a phone keep their title** with a compact **×**; the header
157
+ buttons no longer stretch across the width. Select Models puts its three
158
+ buttons under the title.
159
+ - **A group's head.** One chevron **›** that turns down while the group is
160
+ open replaces ▾/▸; focus returns to it only after a keyboard toggle, with
161
+ a focus ring in the accent colour. A head line without a head row reads
162
+ "<last active> · N sessions", and its pill shows only what waits, runs or
163
+ failed. A long head drops its message count whole, then its date, instead
164
+ of cutting them. A mouse click on **ⓘ** hands focus back without a ring.
165
+ - **Sidebar rows.** The title and the "1 open" pill share one line and only
166
+ the title is cut short; the title and meta line keep clear of the archive
167
+ and delete buttons. Archive and delete show on keyboard focus too. The
168
+ filter row's archive button reads **🗄️**, and the buttons close up to the
169
+ right when **×** or **➕** is hidden. The sidebar head follows the zoom and
170
+ wraps instead of cutting off **Inbox** or **Install**.
171
+ - **The Claude usage warning above the composer is one line,** such as
172
+ `⚠ Claude 94 % weekly · resets Tue 04:00`; a tap shows the full sentence.
173
+ - **A sub-agent's Stop is red and asks first** (**⏹ Stop agent**).
174
+ - **On a phone a background task's title has its own full-width line.**
175
+ - **Images in messages have no card around them.**
176
+ - **Editing a chat's title on a desktop takes the whole header row** while
177
+ you type; the title lines, the banner and the header buttons come back
178
+ when you are done.
179
+ - **Settings takes the focus when it opens, and Escape closes it.** The
180
+ Settings and Plan usage tab strips fade at an edge while more tabs lie
181
+ beyond it, and keep the selected tab in view. The Features help wraps
182
+ long identifiers.
183
+ - **Pack guard ceilings.** The npm tarball may hold 610 entries and 9.5 MiB
184
+ unpacked (590 and 9 MiB before); 0.9.20 had used 99.3 % of the bytes.
185
+
186
+ ### Fixed
187
+
188
+ - **The archive button on a sidebar row is visible on touch screens.** It
189
+ was invisible there but could still be tapped.
190
+ - **A ︙ entry tapped on a phone no longer closes what it opened.** The tap's
191
+ follow-up click landed on the menu's backdrop or the new dialog and closed
192
+ it again: About, Kill session, Check Usage, Context, Restart relay and
193
+ Search, and the watch and schedule panels; Git changes did not open a
194
+ diff.
195
+ - **Contrast in the light theme.** Primary buttons read at 5.19:1, red
196
+ buttons at 4.67:1, the sidebar's "1 open" pill at 6.47:1, Settings help
197
+ links at 6.85:1, the offline banner at 5.6:1 and the **☰** status tones
198
+ at 4.9:1 or more. A hovered accent button gets a darker fill (6.36:1)
199
+ instead of turning paler.
200
+ - **Status and the Inbox, closed with nothing to go back to,** show their
201
+ empty state instead of a blank pane.
202
+ - **The OpenAI, Grok and Cursor fields in Settings look like the others,**
203
+ and **Use a different token** and **Empty queue** are real buttons.
204
+ - **The ⚙ popover fits its width under zoom,** the full origin title shows,
205
+ and focus is kept when the list or header is drawn anew.
206
+
207
+ ### Upgrade notes
208
+
209
+ - **Every session worker is replaced once after the update.** Workers from
210
+ 0.9.20 state no build id, so 30 seconds after the relay first hears of
211
+ one it counts as on an older build and is replaced once its session is
212
+ idle and its CLI has ended, usually some 10 minutes after its last turn.
213
+ About shows *M on an older build* until then. Nothing needs to be done.
214
+ Each replacement costs one cold start on the session's next message.
215
+ - **No migration and no new dependency.** Going back to 0.9.20 needs no
216
+ database step.
217
+
218
+ ## [0.9.20] — 2026-10-29
219
+
220
+ Highlights: every plan card gets an "Approve plan" button that approves the
221
+ plan without starting the implementation and keeps the session in plan mode,
222
+ and a session can be locked in plan mode (`planOnly`) so agents cannot take
223
+ it out; plan cards are in the Inbox. Check Usage shows the tokens of every
224
+ turn for every provider: input, output, cache writes and cache reads, for
225
+ today, this month and all time, and by model. The relay backs up its database
226
+ before a new version's migrations and keeps the last two copies; `oar backup`
227
+ takes one by hand. A new database is `oar.db`; an existing install keeps
228
+ running on `copilot.db`, nothing is renamed. Under the systemd service a
229
+ restart hands the relay to systemd with its new code, waits for running turns
230
+ and keeps session workers, and `oar service` works in a git checkout. A paired
231
+ relay's approval cards in the Inbox say what they are about, a card a turn
232
+ there waits on pushes once to your devices, and the badge is current within
233
+ about 20 s. An open fold in the Inbox stays open.
234
+
235
+ ### Added
236
+
237
+ - **"Approve plan" on every plan card.** It is the first button of every
238
+ provider's card (the Copilot CLI's own list gets it too), also on cards that
239
+ were waiting when the relay was updated. It marks the plan approved and
240
+ queues exactly "Plan approved by the user. Do not implement. Finish only the
241
+ plan-only steps you were given, then stop." That one turn runs in agent mode
242
+ (plan mode blocks writes, and the plan doc may need a commit); the session
243
+ stays in plan mode, so the next message plans again. Pressed while the plan
244
+ turn still runs, the follow-up waits for it and runs as its own turn.
245
+ - **Plan-only sessions (`planOnly`).** An optional lock, set only when asked:
246
+ relay_tool `create_session` with `planOnly: true` (no mode named: it starts
247
+ in plan; another mode is refused), `update_session` with `planOnly: true`
248
+ (with `mode: "plan"` unless the session is in plan already), or the owner's
249
+ switch "Plan only: agents can't take this session out of plan mode" in the
250
+ context modal, for every session. Plan mode alone never sets it.
251
+ - An agent's `send` or `update_session` that names another mode gets HTTP
252
+ 409, code `plan_only`: "This session is plan-only; agents can't change its
253
+ mode. Clear planOnly first, or ask the user." Nothing is queued or changed
254
+ (an update_session dry run refuses before any Allow card). A send without
255
+ a mode runs in the session's saved mode. An agent's mode change through the preferences route
256
+ is refused (409), and so is any planOnly change there (403).
257
+ - `update_session` with `planOnly: false` counts only in the same call that
258
+ names a mode other than plan (`{planOnly: false, mode: "agent"}`). Alone,
259
+ or with `mode: "plan"`, it is ignored, and on a plan-only session the answer says
260
+ "planOnly unchanged: clear it in the same call as the new mode", so a client that
261
+ sends every property as false cannot drop the lock. The owner's switch
262
+ clears it any time.
263
+ - The owner still changes the mode freely; the mode select shows "Plan 🔒".
264
+ On such a session the card recommends "Approve plan" and asks "This
265
+ session is plan-only. <label> leaves plan mode. Go on?" before "Implement
266
+ in autopilot", "Implement with autopilot fleet" and "Stop here and prompt
267
+ myself".
268
+ - list_sessions and read_session show `planOnly: true`. A compaction keeps
269
+ the lock; a turn cut by a usage limit or a restart goes on in plan, except
270
+ the "Approve plan" turn itself. A plan-only conversation's own agent counts
271
+ as a plan-mode caller during that turn too.
272
+ - Migration 0035 adds `conversations.plan_only`.
273
+ - A relay on 0.9.19 still sends its caller's mode with every `send`: a send
274
+ from a non-plan 0.9.19 caller to a plan-only session is refused. A
275
+ `create_session` with planOnly to a 0.9.19 relay creates the session
276
+ without the lock and says so (`planOnlyWarning`).
277
+ - **Relay event `board_settled`.** A session's subscribers hear when one of
278
+ its plan cards is settled, "Stop here" included, with `boardId`,
279
+ `boardType`, `action`, `actionLabel`, `title` and `queuedMessageId`. The
280
+ line reads:
281
+
282
+ ```
283
+ [relay event] session "<title>" (<id>): board_settled "<card title>" → "<label>" (action <action>, board_id <board id>, queued message <message id>)
284
+ ```
285
+
286
+ For "Stop here" it ends `(action exit_only, board_id <board id>, nothing
287
+ queued)`. It is one of the default event types, also for subscriptions
288
+ stored with the 0.9.19 default; `match` applies to its action, label and
289
+ card title.
290
+ - **Plan cards in the Inbox.** For each session that is neither deleted nor
291
+ archived, its newest pending plan card, with the plan folded and the card's
292
+ own buttons (and the plan-only question); plan cards settled in the last
293
+ day show what was chosen. They count in the badge. A paired relay's Inbox
294
+ shows their title only, with "Open there".
295
+ - **Other relays' approval cards are easier to notice.** A paired relay's
296
+ approval card in the Inbox says what it is about: "New session · the
297
+ session waits", then "relay B · New session requested by 'X': title,
298
+ model" (or loop guard, write, session settings, watch, start sessions),
299
+ still with "Open there" only. The agent's prompt, answer or question never
300
+ leaves its relay. Every relay now checks its paired relays about every
301
+ 20 s, even with no browser open, so the badge is current within about
302
+ 20 s. A card a turn there waits on (an approval, or a question the turn is
303
+ blocked on) pushes once in total: its own relay pushes as before, and when
304
+ that reached none of your devices, the first paired relay with push on
305
+ that checks it pushes instead (`Approval waiting on relay "B"`), with no
306
+ answer buttons; the tap opens the session there. It follows the device's
307
+ "questions" toggle and notification settings: with previews off, an
308
+ approval's body carries no summary, only its kind ("New session · the
309
+ session waits"); the title names the session only with the device's
310
+ include-title setting on.
311
+ - `GET /api/relay-asks/remote/inbox`: approval items gain `variant` and
312
+ `summary`; with `?push=1` (the caller can push), pending approval and
313
+ ask_user items gain `notice` (`here|wait|you|other`), and `you` gains
314
+ `claimed` (`now|before`). A 0.9.19 relay ignores both and never pushes
315
+ another relay's card. New socket event `inbox_remote_changed`. A push's
316
+ data may carry `url`. Claims are in memory; no migration.
317
+ - **The relay backs up its database before a new version's migrations.**
318
+ At the first start of each new version (in a git checkout: of each new
319
+ commit) it copies the database into `backups/` in the state root
320
+ (`~/.oar/backups`, `%APPDATA%\oar\backups`; `server/backups/` in a
321
+ checkout) before the migrations run, as one checked file named after the
322
+ version that ran on the data, such as
323
+ `oar-0.9.19-20261103-091500-pre-update.db`. It keeps the last two
324
+ (`dbBackups.keep` in `config.json`; `dbBackups.enabled: false` turns it off,
325
+ `dbBackups.dir` moves the folder) and deletes only files it wrote itself:
326
+ hand-made backups, `MIGRATION.md` and the `backups/<time>/` folders of
327
+ earlier moves stay as they are, and `oar doctor` lists them as not rotated.
328
+ When the disk would keep less than max(1 GiB, 5 %) free, or the folder holds
329
+ a `DO_NOT_TOUCH` file, the copy is skipped with a warning in the log, in
330
+ About, in `oar doctor` and in the in-app update's log, and the start goes on.
331
+ Plain restarts take no copy. While it runs, `oar start` says the relay is
332
+ backing up its database. A start while `oar backup` writes a copy waits
333
+ for it (up to 15 minutes; `oar start` says so and leaves it running), then
334
+ makes its own in any case.
335
+ - **`oar backup [--reason <text>]`.** Copies the database while the relay
336
+ runs, into the same folder with the same name and rotation
337
+ (`oar-<version>-<time>-manual.db`, or the reason given).
338
+ - **About and `oar doctor` show the database file** by its full path with its
339
+ size, and the backups: the last one, how many are kept, and the folder.
340
+
341
+ - **Tokens per turn for every provider, in Check Usage.** Every tab gets a
342
+ **Tokens** section (today, this month, all time) and **Tokens by model,
343
+ this month**, with fresh input, output, cache writes and cache reads apart
344
+ and the share of input the cache served. A figure a provider does not report
345
+ reads "—" with the reason in the section's note; 0 means reported zero.
346
+ Copilot turns on your own OpenAI key show as their own group, "Tokens — own
347
+ OpenAI key". **About OAR** shows each provider's tokens of all time,
348
+ Claude, Claude API and DeepSeek included. Each turn's tokens are kept per
349
+ run and model with the chat and go when it is deleted; the daily sums stay.
350
+ The Claude API and DeepSeek days from before the upgrade are copied in once,
351
+ with cache writes inside input. Both sections open expanded. Two model ids
352
+ with one name (a dated and an undated Claude id) are one row, their tokens
353
+ added up; pointing at the row shows the ids.
354
+ - Detail rows in `GET /api/usage` may carry a `title`: on a merged Tokens
355
+ by model row, `Model ids: <id>, <id>`. Those rows are sorted by name.
356
+ - New worker route `POST /api/turn-token-usage` and tables `turn_token_usage`
357
+ and `token_usage_days` (migration 0036). Every worker posts after each
358
+ turn: Claude, Claude API, DeepSeek, Claude cloud, Copilot (plan and own
359
+ key), Cursor and Grok.
360
+ - `OAR_DEBUG_TOKEN_USAGE=1` makes the Copilot worker log each model call's
361
+ raw token counts (no ids).
362
+
363
+ ### Changed
364
+
365
+ - **No plan summary in a card's follow-up.** "Implement in autopilot",
366
+ "Implement with autopilot fleet" and "Stop here and prompt myself" queue
367
+ only their line; the plan is no longer repeated after it.
368
+ - **relay_tool `send` without `mode` leaves the target's mode alone.** It
369
+ used to run the target's turn in the calling session's mode (a plan-mode
370
+ agent put the sessions it prompted into plan mode). Name `mode` when a turn
371
+ should run in a given one. `create_session` without a mode still takes the
372
+ calling session's.
373
+ - **Switching between plan mode and a writing mode during a running turn
374
+ queues the message instead of steering it in.** A steer switched the running
375
+ turn to the message's mode, so a plan turn could gain write access halfway
376
+ through, or a writing turn lose it. Such a message now waits and runs as its
377
+ own turn once the current one ends; the composer reads "Queue" for it.
378
+ Messages in a mode on the same side (agent, ask and autopilot with each
379
+ other, or plan with plan) steer in as before.
380
+ - **A new database is `oar.db`.** The relay's database file gets the
381
+ product's name: a database created from nothing is `oar.db` (with
382
+ `oar.db-wal` and `oar.db-shm`). An existing install keeps running on
383
+ `copilot.db`; nothing is renamed, and `oar doctor` and About show which file
384
+ a relay runs on. A database brought over from elsewhere (an earlier
385
+ install's package, `oar setup --migrate-from`) keeps its own name.
386
+ - **`oar.db` and `copilot.db` in one data folder.** When
387
+ `oar.db` and `copilot.db` are both there, the start reads both. A
388
+ `copilot.db` that is not newer, is empty, or holds only what a start of an
389
+ earlier version writes (no conversation, message or other work; a few
390
+ start-up settings) is set aside as
391
+ `copilot.db.older-<time>`, never deleted, and the relay runs on `oar.db`.
392
+ A `copilot.db` with newer work, or each newer somewhere, stops the start
393
+ (exit code 78) with both files named and nothing changed: move the one not
394
+ to go on with into a folder of your own, then start again. An `oar.db-wal`
395
+ or `oar.db-shm` without `oar.db` stops the start too (a database renamed to
396
+ `oar.db` later would take its pages in, losing the rows written since):
397
+ move it into a folder of your own.
398
+ - **Migrations and `npm run deepseek:import` run by hand never create an
399
+ empty database.** Without a path they take the relay's database in
400
+ `server/data` (`oar.db`, else `copilot.db`); a path given must exist.
401
+ - **`oar uninstall --purge` says it deletes the database backups** with the
402
+ state root.
403
+ - **The DeepSeek card's "Tokens by model" is now "Priced tokens by model"**:
404
+ the cache hits, misses and output its spend estimate prices, beside the
405
+ Tokens sections every tab gets.
406
+ - **The Claude API card's "Tokens by model" is replaced** by the Tokens
407
+ sections every tab gets, which show cache writes apart instead of inside
408
+ "other input". Its spend by model and the cap stay.
409
+ - **Cache writes are no longer lost** for the Claude family, Copilot, Cursor
410
+ and Grok: the token booking keeps them apart. The older bookings (the Claude
411
+ API spend, the DeepSeek estimate) still count them as misses, unchanged.
412
+ - **The Claude API spend no longer counts a turn twice** when Claude Code
413
+ reported one of its results without per-model usage: the next result's
414
+ growth used to include that turn again.
415
+ - **The systemd service restarts the relay with new code, waits for running
416
+ turns, and keeps session workers.** A relay under the systemd user service
417
+ hands a restart (**🌄 Restart web relay**, the **Update** button,
418
+ `oar update`, `oar restart`) to systemd once its turns are done, which
419
+ starts a fresh supervisor and runtime from the code on disk; before, only
420
+ the runtime was relaunched and the supervisor kept its old code. `oar stop`
421
+ and `oar restart` under the service now go through the relay too and wait
422
+ for running turns (the unit stays enabled after a stop); `--now` goes
423
+ through `systemctl --user` at once, as both did before, and so does a relay
424
+ that does not answer. Without the service, `--now` asks the relay not to
425
+ wait, as **Restart now** does. The relay starts its session workers in
426
+ scopes of their own (`systemd-run --user --scope`), so a stop or restart of
427
+ the service no longer ends them. Detached helpers the relay runs for a
428
+ while (a provider CLI being installed, a provider login) still end with a
429
+ full restart of the service. A stop that comes in while the restart is
430
+ handed to systemd wins: no restart follows. On a host with cgroup v1 next to
431
+ an unused v2 hierarchy (a bare `0::/` line) the relay still finds its unit.
432
+ - **The unit restarts a failing relay with a backoff and never gives up.**
433
+ The relay's supervisor relaunches the first two crashes itself; then
434
+ systemd starts it again after 2 seconds, with longer waits up to 5 minutes
435
+ (systemd before 254: every 10 seconds). The unit has `KillMode=mixed`, so
436
+ no runtime of a dead supervisor is left holding the port.
437
+ - **`oar service` works in a git checkout on Linux.**
438
+ `node bin/oar.js service install` writes a unit that runs
439
+ `node server/server.js` in the checkout, on its own config, data and logs;
440
+ `node bin/oar.js start|stop|restart|status` go through it. A hand-made unit
441
+ counts as the checkout's service only when its `ExecStart` runs
442
+ `<checkout>/server/server.js`.
443
+ - **Hand-made units are left as they are.** A unit OAR did not write for this
444
+ install (edited or written by hand, or another install's, such as the
445
+ global install beside a checkout) is no longer written over by
446
+ `oar service install` or `oar setup --start`, or deleted by
447
+ `oar service remove`: they name it and change nothing. `--replace` replaces
448
+ or removes it, keeping a copy as `oar.service.bak`. `oar service status`
449
+ says whose the unit is, lists the drop-ins in `oar.service.d/` (your own
450
+ settings, never touched), and says when lingering is off.
451
+
452
+ ### Fixed
453
+
454
+ - **Archiving or deleting a chat whose session worker is gone no longer
455
+ fails with "Couldn't stop this conversation's session".** The relay still
456
+ tried to stop the worker it last knew of; on Windows that stop could fail
457
+ on its own and answered 409 `worker-stop-failed` until a later try. A
458
+ worker whose process has ended counts as stopped. When a stop does fail,
459
+ the relay log says why.
460
+ - **An open fold in the Inbox no longer closes by itself.** Every change of
461
+ a question and the minute's check of paired relays drew the Inbox anew,
462
+ with every **Details** and plan fold closed. An open fold now stays open
463
+ until you close it or its item leaves the list; the same for a question's
464
+ **Details** on its card in the session.
465
+
466
+ ### Upgrade notes
467
+
468
+ - **Under the systemd service, run `oar service install` after updating**
469
+ (`node bin/oar.js service install` in a checkout) to get the new unit. A
470
+ hand-made unit is reported and kept; `oar service install --replace` puts
471
+ OAR's in its place, keeping a copy as `oar.service.bak` (drop-ins in
472
+ `oar.service.d/` stay and keep applying).
473
+ - **A checkout's hand-made unit that only runs in the checkout is not its
474
+ service.** A unit with `WorkingDirectory=<checkout>` whose `ExecStart` does
475
+ not name `<checkout>/server/server.js` (`npm start`, a relative
476
+ `server/server.js`) is kept as hand-made: `node bin/oar.js start` and
477
+ `restart` refuse, and `status` says it is not for this checkout. Run
478
+ `node bin/oar.js service install --replace` to put OAR's unit in its place,
479
+ keeping a copy as `oar.service.bak`.
480
+ - **The supervisor of 0.9.19 keeps running until the service restarts once
481
+ by systemd:** at an idle moment, run `oar restart --now`. That restart ends
482
+ running turns once; after it, restarts wait for them.
483
+ - **Session workers started before that restart may stop at it, once.** They
484
+ were started inside the service; those started afterwards run in scopes of
485
+ their own.
486
+ - **Nothing to do for the new database name.** A relay that ran on
487
+ `copilot.db` goes on running on it. To give it the new name, by hand and
488
+ with the relay stopped: take a backup (`oar backup --reason pre-rename`),
489
+ stop the relay fully (`oar stop`, or the systemd service or autostart, so
490
+ nothing starts it again), check that no program holds the files (Linux:
491
+ `fuser copilot.db*`; Windows: close database browsers and Explorer previews),
492
+ make sure no `oar.db-wal` or `oar.db-shm` is there already (move one that
493
+ is into a folder of your own), rename `copilot.db`, and `copilot.db-wal`
494
+ and `copilot.db-shm` where they are there, to `oar.db`, `oar.db-wal`,
495
+ `oar.db-shm`, and start again.
496
+ `oar doctor` then shows `oar.db`. Scripts of your own that open
497
+ `copilot.db` by path need the new name first.
498
+ - **Going back to 0.9.19 on `copilot.db` needs no step.** Migrations 0035 and
499
+ 0036 only add the column `conversations.plan_only`, the tables
500
+ `turn_token_usage` and `token_usage_days` and an index; 0.9.19 opens the
501
+ database as it is and ignores them, so a plan-only lock does not hold on
502
+ 0.9.19, and the tokens kept so far are there again at the next 0.9.20
503
+ start. Turns run on 0.9.19 book no tokens.
504
+ - **Going back to a version before 0.9.20 on `oar.db`** (a new 0.9.20 install,
505
+ or one renamed by hand): stop the relay fully, make sure no
506
+ `copilot.db-wal` or `copilot.db-shm` is there already (move one that is
507
+ into a folder of your own), rename `oar.db`, `oar.db-wal` and `oar.db-shm`
508
+ (those there) to `copilot.db`, `copilot.db-wal`, `copilot.db-shm`, then
509
+ start the earlier version. A relay still on
510
+ `copilot.db` needs no step. Skipped, the earlier version starts on a new,
511
+ empty `copilot.db`; back on 0.9.20 that one is set aside if nothing was done
512
+ on it, and the start stops with both names if something was.
513
+ - **The first 0.9.20 start backs up the database.** After `oar update` or a
514
+ `git pull` its name carries the running version and says
515
+ `pre-update-from-unknown`, such as
516
+ `oar-0.9.20-20261103-091500-pre-update-from-unknown.db`, though it holds the
517
+ data of the version before: earlier versions did not record which version
518
+ ran on the data. After an in-app update it carries the version before. The
519
+ time in a backup's name is UTC. It needs
520
+ free space for one more copy of the database; `oar doctor` shows the
521
+ result. Backups made by hand before (`copilot-pre-*`, `main-*`,
522
+ `oar-pre-*`) stay where they are and are never counted or deleted.
523
+ - **After going back to an earlier version and up again, run `oar backup`
524
+ before the upgrade:** versions before 0.9.20 do not record that they ran,
525
+ so the second upgrade sees no version change and takes no copy.
526
+ - **To restore a backup:** stop the relay, move the live database files
527
+ (`oar.db*` or `copilot.db*`) into a folder of your own, copy the backup into
528
+ the data folder (it is one file, no `-wal` or `-shm`), and start the version
529
+ the data is of. Name it as that version runs on it: `copilot.db` for a
530
+ version before 0.9.20, else the name the relay ran on. A
531
+ `pre-update-from-unknown` backup holds the data of the version before the
532
+ one in its name. Open a backup only read-only
533
+ (`sqlite3 'file:<backup>?immutable=1'`).
534
+
6
535
  ## [0.9.19] — 2026-10-27
7
536
 
8
537
  Highlights: a relay that runs out of memory is restarted instead of staying