@oar-sh/oar 0.9.19 → 0.9.20

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (119) hide show
  1. package/.github/extensions/web-relay/polling/polling-loop.mjs +2 -26
  2. package/.github/extensions/web-relay/skills/question-routing-hooks.mjs +2 -26
  3. package/CHANGELOG.md +317 -0
  4. package/README.md +10 -10
  5. package/bin/oar.js +391 -97
  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 +27 -0
  9. package/server/claude-worker/claude-remote-relay-tool.mjs +1 -0
  10. package/server/claude-worker/claude-session-process.mjs +3 -0
  11. package/server/claude-worker/claude-turn-publisher.mjs +163 -8
  12. package/server/claude-worker/claude-usage-totals.mjs +58 -0
  13. package/server/copilot-worker/copilot-plan-board.mjs +4 -7
  14. package/server/copilot-worker/copilot-sdk-event-normalizer.mjs +86 -3
  15. package/server/copilot-worker/copilot-sdk-session-process.mjs +57 -4
  16. package/server/cursor-worker/cursor-turn-runner.mjs +42 -6
  17. package/server/db-schema.mjs +18 -0
  18. package/server/deepseek-worker/deepseek-claude-backend.mjs +14 -0
  19. package/server/grok-worker/grok-turn-runner.mjs +44 -6
  20. package/server/migrations/0001-add-structured-answer.mjs +5 -7
  21. package/server/migrations/0002-recent-workspace-roots-path-key.mjs +5 -7
  22. package/server/migrations/0003-push-subscriptions.mjs +5 -7
  23. package/server/migrations/0004-steer-settle-markers.mjs +5 -7
  24. package/server/migrations/0005-remote-relays.mjs +5 -7
  25. package/server/migrations/0006-workspace-root-settings.mjs +5 -7
  26. package/server/migrations/0007-claude-cloud.mjs +5 -7
  27. package/server/migrations/0008-message-pins.mjs +5 -7
  28. package/server/migrations/0009-subagent-timeline.mjs +5 -7
  29. package/server/migrations/0010-subagent-timeline-pruning.mjs +5 -7
  30. package/server/migrations/0011-relay-event-subscriptions.mjs +5 -7
  31. package/server/migrations/0012-cross-relay-event-subscriptions.mjs +5 -7
  32. package/server/migrations/0013-previews.mjs +5 -7
  33. package/server/migrations/0014-conversation-groups.mjs +5 -7
  34. package/server/migrations/0015-conversation-group-position.mjs +5 -7
  35. package/server/migrations/0016-share-access-log.mjs +5 -7
  36. package/server/migrations/0017-conversation-history-indexes.mjs +5 -7
  37. package/server/migrations/0018-background-task-rows.mjs +5 -7
  38. package/server/migrations/0019-relay-event-subscription-options.mjs +5 -7
  39. package/server/migrations/0020-conversation-may-create-sessions.mjs +5 -7
  40. package/server/migrations/0021-conversation-group-agent-marks.mjs +5 -7
  41. package/server/migrations/0022-relay-schedules.mjs +5 -7
  42. package/server/migrations/0023-relay-asks.mjs +5 -7
  43. package/server/migrations/0024-deepseek-usage-totals.mjs +5 -7
  44. package/server/migrations/0025-deepseek-usage-requests-daily.mjs +5 -7
  45. package/server/migrations/0026-deepseek-usage-served-models.mjs +5 -7
  46. package/server/migrations/0027-deepseek-usage-checkpoint-runs.mjs +5 -7
  47. package/server/migrations/0028-deepseek-usage-checkpoint-request-runs.mjs +5 -7
  48. package/server/migrations/0029-deepseek-balance-rises.mjs +5 -7
  49. package/server/migrations/0030-deepseek-usage-actuals.mjs +5 -7
  50. package/server/migrations/0031-queue-provider.mjs +5 -7
  51. package/server/migrations/0032-runtime-session-billing-switch.mjs +5 -7
  52. package/server/migrations/0033-claude-api-usage-checkpoints.mjs +5 -7
  53. package/server/migrations/0034-claude-api-usage-totals.mjs +5 -7
  54. package/server/migrations/0035-conversation-plan-only.mjs +87 -0
  55. package/server/migrations/0036-token-usage.mjs +267 -0
  56. package/server/public/app/about-view.mjs +87 -3
  57. package/server/public/app/api-client.js +2 -1
  58. package/server/public/app/bootstrap.js +55 -2
  59. package/server/public/app/composer-control-state.mjs +12 -2
  60. package/server/public/app/context-usage-view.mjs +16 -0
  61. package/server/public/app/conversation-view.js +17 -2
  62. package/server/public/app/inbox-ui.js +32 -0
  63. package/server/public/app/inbox-view.mjs +141 -5
  64. package/server/public/app/plan-board-shared.mjs +49 -0
  65. package/server/public/app/plan-usage-view.mjs +1 -1
  66. package/server/public/app/relay-board-inline.mjs +4 -1
  67. package/server/public/app/relay-board-view.js +38 -10
  68. package/server/public/app/relay-event-view.mjs +4 -0
  69. package/server/public/app/socket-handlers.js +10 -2
  70. package/server/public/index.html +14 -0
  71. package/server/public/sw.js +8 -0
  72. package/server/relay-heap-guard.mjs +14 -1
  73. package/server/relay-self-restart.mjs +83 -2
  74. package/server/relay-tools.md +4 -3
  75. package/server/repositories/message-repository.mjs +31 -1
  76. package/server/repositories/remote-relay-repository.mjs +10 -0
  77. package/server/repositories/session-repository.mjs +6 -1
  78. package/server/routes/messages-routes.mjs +87 -0
  79. package/server/routes/relay-ask-routes.mjs +138 -8
  80. package/server/routes/relay-board-routes.mjs +55 -40
  81. package/server/routes/sessions-routes.mjs +103 -7
  82. package/server/server-runtime.mjs +132 -11
  83. package/server/services/about-service.mjs +59 -0
  84. package/server/services/cache-rebuild-service.mjs +7 -0
  85. package/server/services/conversation-preferences-service.mjs +31 -0
  86. package/server/services/database-file.mjs +413 -0
  87. package/server/services/db-backup-service.mjs +809 -0
  88. package/server/services/deepseek-usage-import.mjs +6 -9
  89. package/server/services/delete-archive-service.mjs +3 -0
  90. package/server/services/earlier-database-service.mjs +69 -126
  91. package/server/services/oar-cli-helpers.mjs +214 -12
  92. package/server/services/oar-cli-menu.mjs +1 -0
  93. package/server/services/oar-state-migration-service.mjs +33 -13
  94. package/server/services/package-data-rescue.mjs +4 -2
  95. package/server/services/plan-usage-claude-api.mjs +14 -15
  96. package/server/services/plan-usage-contract.mjs +6 -2
  97. package/server/services/plan-usage-deepseek.mjs +1 -1
  98. package/server/services/plan-usage-tokens.mjs +248 -0
  99. package/server/services/push-dispatch-service.mjs +134 -9
  100. package/server/services/relay-cli-launcher-service.mjs +3 -0
  101. package/server/services/relay-event-service.mjs +30 -0
  102. package/server/services/relay-question-context.mjs +11 -0
  103. package/server/services/relay-restart-picture.mjs +67 -22
  104. package/server/services/remote-inbox-watch-service.mjs +204 -0
  105. package/server/services/remote-relay-caller-context.mjs +80 -7
  106. package/server/services/remote-relay-dispatcher.mjs +40 -5
  107. package/server/services/restart-resume-service.mjs +5 -2
  108. package/server/services/session-worker-launch-service.mjs +12 -3
  109. package/server/services/session-worker-stop-service.mjs +27 -6
  110. package/server/services/token-usage-service.mjs +430 -0
  111. package/server/services/update-install-service.mjs +13 -0
  112. package/server/services/usage-limit-pause-service.mjs +8 -1
  113. package/server/services/worker-scope.mjs +77 -0
  114. package/shared/plan-board-actions.mjs +130 -0
  115. package/shared/plan-only.mjs +43 -0
  116. package/shared/relay-asks.mjs +3 -0
  117. package/shared/relay-events.mjs +38 -4
  118. package/shared/remote-relay-contract.mjs +66 -11
  119. package/shared/turn-token-usage.mjs +144 -0
@@ -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,323 @@
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.20] — 2026-10-29
7
+
8
+ Highlights: every plan card gets an "Approve plan" button that approves the
9
+ plan without starting the implementation and keeps the session in plan mode,
10
+ and a session can be locked in plan mode (`planOnly`) so agents cannot take
11
+ it out; plan cards are in the Inbox. Check Usage shows the tokens of every
12
+ turn for every provider: input, output, cache writes and cache reads, for
13
+ today, this month and all time, and by model. The relay backs up its database
14
+ before a new version's migrations and keeps the last two copies; `oar backup`
15
+ takes one by hand. A new database is `oar.db`; an existing install keeps
16
+ running on `copilot.db`, nothing is renamed. Under the systemd service a
17
+ restart hands the relay to systemd with its new code, waits for running turns
18
+ and keeps session workers, and `oar service` works in a git checkout. A paired
19
+ relay's approval cards in the Inbox say what they are about, a card a turn
20
+ there waits on pushes once to your devices, and the badge is current within
21
+ about 20 s. An open fold in the Inbox stays open.
22
+
23
+ ### Added
24
+
25
+ - **"Approve plan" on every plan card.** It is the first button of every
26
+ provider's card (the Copilot CLI's own list gets it too), also on cards that
27
+ were waiting when the relay was updated. It marks the plan approved and
28
+ queues exactly "Plan approved by the user. Do not implement. Finish only the
29
+ plan-only steps you were given, then stop." That one turn runs in agent mode
30
+ (plan mode blocks writes, and the plan doc may need a commit); the session
31
+ stays in plan mode, so the next message plans again. Pressed while the plan
32
+ turn still runs, the follow-up waits for it and runs as its own turn.
33
+ - **Plan-only sessions (`planOnly`).** An optional lock, set only when asked:
34
+ relay_tool `create_session` with `planOnly: true` (no mode named: it starts
35
+ in plan; another mode is refused), `update_session` with `planOnly: true`
36
+ (with `mode: "plan"` unless the session is in plan already), or the owner's
37
+ switch "Plan only: agents can't take this session out of plan mode" in the
38
+ context modal, for every session. Plan mode alone never sets it.
39
+ - An agent's `send` or `update_session` that names another mode gets HTTP
40
+ 409, code `plan_only`: "This session is plan-only; agents can't change its
41
+ mode. Clear planOnly first, or ask the user." Nothing is queued or changed
42
+ (an update_session dry run refuses before any Allow card). A send without
43
+ a mode runs in the session's saved mode. An agent's mode change through the preferences route
44
+ is refused (409), and so is any planOnly change there (403).
45
+ - `update_session` with `planOnly: false` counts only in the same call that
46
+ names a mode other than plan (`{planOnly: false, mode: "agent"}`). Alone,
47
+ or with `mode: "plan"`, it is ignored, and on a plan-only session the answer says
48
+ "planOnly unchanged: clear it in the same call as the new mode", so a client that
49
+ sends every property as false cannot drop the lock. The owner's switch
50
+ clears it any time.
51
+ - The owner still changes the mode freely; the mode select shows "Plan 🔒".
52
+ On such a session the card recommends "Approve plan" and asks "This
53
+ session is plan-only. <label> leaves plan mode. Go on?" before "Implement
54
+ in autopilot", "Implement with autopilot fleet" and "Stop here and prompt
55
+ myself".
56
+ - list_sessions and read_session show `planOnly: true`. A compaction keeps
57
+ the lock; a turn cut by a usage limit or a restart goes on in plan, except
58
+ the "Approve plan" turn itself. A plan-only conversation's own agent counts
59
+ as a plan-mode caller during that turn too.
60
+ - Migration 0035 adds `conversations.plan_only`.
61
+ - A relay on 0.9.19 still sends its caller's mode with every `send`: a send
62
+ from a non-plan 0.9.19 caller to a plan-only session is refused. A
63
+ `create_session` with planOnly to a 0.9.19 relay creates the session
64
+ without the lock and says so (`planOnlyWarning`).
65
+ - **Relay event `board_settled`.** A session's subscribers hear when one of
66
+ its plan cards is settled, "Stop here" included, with `boardId`,
67
+ `boardType`, `action`, `actionLabel`, `title` and `queuedMessageId`. The
68
+ line reads:
69
+
70
+ ```
71
+ [relay event] session "<title>" (<id>): board_settled "<card title>" → "<label>" (action <action>, board_id <board id>, queued message <message id>)
72
+ ```
73
+
74
+ For "Stop here" it ends `(action exit_only, board_id <board id>, nothing
75
+ queued)`. It is one of the default event types, also for subscriptions
76
+ stored with the 0.9.19 default; `match` applies to its action, label and
77
+ card title.
78
+ - **Plan cards in the Inbox.** For each session that is neither deleted nor
79
+ archived, its newest pending plan card, with the plan folded and the card's
80
+ own buttons (and the plan-only question); plan cards settled in the last
81
+ day show what was chosen. They count in the badge. A paired relay's Inbox
82
+ shows their title only, with "Open there".
83
+ - **Other relays' approval cards are easier to notice.** A paired relay's
84
+ approval card in the Inbox says what it is about: "New session · the
85
+ session waits", then "relay B · New session requested by 'X': title,
86
+ model" (or loop guard, write, session settings, watch, start sessions),
87
+ still with "Open there" only. The agent's prompt, answer or question never
88
+ leaves its relay. Every relay now checks its paired relays about every
89
+ 20 s, even with no browser open, so the badge is current within about
90
+ 20 s. A card a turn there waits on (an approval, or a question the turn is
91
+ blocked on) pushes once in total: its own relay pushes as before, and when
92
+ that reached none of your devices, the first paired relay with push on
93
+ that checks it pushes instead (`Approval waiting on relay "B"`), with no
94
+ answer buttons; the tap opens the session there. It follows the device's
95
+ "questions" toggle and notification settings: with previews off, an
96
+ approval's body carries no summary, only its kind ("New session · the
97
+ session waits"); the title names the session only with the device's
98
+ include-title setting on.
99
+ - `GET /api/relay-asks/remote/inbox`: approval items gain `variant` and
100
+ `summary`; with `?push=1` (the caller can push), pending approval and
101
+ ask_user items gain `notice` (`here|wait|you|other`), and `you` gains
102
+ `claimed` (`now|before`). A 0.9.19 relay ignores both and never pushes
103
+ another relay's card. New socket event `inbox_remote_changed`. A push's
104
+ data may carry `url`. Claims are in memory; no migration.
105
+ - **The relay backs up its database before a new version's migrations.**
106
+ At the first start of each new version (in a git checkout: of each new
107
+ commit) it copies the database into `backups/` in the state root
108
+ (`~/.oar/backups`, `%APPDATA%\oar\backups`; `server/backups/` in a
109
+ checkout) before the migrations run, as one checked file named after the
110
+ version that ran on the data, such as
111
+ `oar-0.9.19-20261103-091500-pre-update.db`. It keeps the last two
112
+ (`dbBackups.keep` in `config.json`; `dbBackups.enabled: false` turns it off,
113
+ `dbBackups.dir` moves the folder) and deletes only files it wrote itself:
114
+ hand-made backups, `MIGRATION.md` and the `backups/<time>/` folders of
115
+ earlier moves stay as they are, and `oar doctor` lists them as not rotated.
116
+ When the disk would keep less than max(1 GiB, 5 %) free, or the folder holds
117
+ a `DO_NOT_TOUCH` file, the copy is skipped with a warning in the log, in
118
+ About, in `oar doctor` and in the in-app update's log, and the start goes on.
119
+ Plain restarts take no copy. While it runs, `oar start` says the relay is
120
+ backing up its database. A start while `oar backup` writes a copy waits
121
+ for it (up to 15 minutes; `oar start` says so and leaves it running), then
122
+ makes its own in any case.
123
+ - **`oar backup [--reason <text>]`.** Copies the database while the relay
124
+ runs, into the same folder with the same name and rotation
125
+ (`oar-<version>-<time>-manual.db`, or the reason given).
126
+ - **About and `oar doctor` show the database file** by its full path with its
127
+ size, and the backups: the last one, how many are kept, and the folder.
128
+
129
+ - **Tokens per turn for every provider, in Check Usage.** Every tab gets a
130
+ **Tokens** section (today, this month, all time) and **Tokens by model,
131
+ this month**, with fresh input, output, cache writes and cache reads apart
132
+ and the share of input the cache served. A figure a provider does not report
133
+ reads "—" with the reason in the section's note; 0 means reported zero.
134
+ Copilot turns on your own OpenAI key show as their own group, "Tokens — own
135
+ OpenAI key". **About OAR** shows each provider's tokens of all time,
136
+ Claude, Claude API and DeepSeek included. Each turn's tokens are kept per
137
+ run and model with the chat and go when it is deleted; the daily sums stay.
138
+ The Claude API and DeepSeek days from before the upgrade are copied in once,
139
+ with cache writes inside input. Both sections open expanded. Two model ids
140
+ with one name (a dated and an undated Claude id) are one row, their tokens
141
+ added up; pointing at the row shows the ids.
142
+ - Detail rows in `GET /api/usage` may carry a `title`: on a merged Tokens
143
+ by model row, `Model ids: <id>, <id>`. Those rows are sorted by name.
144
+ - New worker route `POST /api/turn-token-usage` and tables `turn_token_usage`
145
+ and `token_usage_days` (migration 0036). Every worker posts after each
146
+ turn: Claude, Claude API, DeepSeek, Claude cloud, Copilot (plan and own
147
+ key), Cursor and Grok.
148
+ - `OAR_DEBUG_TOKEN_USAGE=1` makes the Copilot worker log each model call's
149
+ raw token counts (no ids).
150
+
151
+ ### Changed
152
+
153
+ - **No plan summary in a card's follow-up.** "Implement in autopilot",
154
+ "Implement with autopilot fleet" and "Stop here and prompt myself" queue
155
+ only their line; the plan is no longer repeated after it.
156
+ - **relay_tool `send` without `mode` leaves the target's mode alone.** It
157
+ used to run the target's turn in the calling session's mode (a plan-mode
158
+ agent put the sessions it prompted into plan mode). Name `mode` when a turn
159
+ should run in a given one. `create_session` without a mode still takes the
160
+ calling session's.
161
+ - **Switching between plan mode and a writing mode during a running turn
162
+ queues the message instead of steering it in.** A steer switched the running
163
+ turn to the message's mode, so a plan turn could gain write access halfway
164
+ through, or a writing turn lose it. Such a message now waits and runs as its
165
+ own turn once the current one ends; the composer reads "Queue" for it.
166
+ Messages in a mode on the same side (agent, ask and autopilot with each
167
+ other, or plan with plan) steer in as before.
168
+ - **A new database is `oar.db`.** The relay's database file gets the
169
+ product's name: a database created from nothing is `oar.db` (with
170
+ `oar.db-wal` and `oar.db-shm`). An existing install keeps running on
171
+ `copilot.db`; nothing is renamed, and `oar doctor` and About show which file
172
+ a relay runs on. A database brought over from elsewhere (an earlier
173
+ install's package, `oar setup --migrate-from`) keeps its own name.
174
+ - **`oar.db` and `copilot.db` in one data folder.** When
175
+ `oar.db` and `copilot.db` are both there, the start reads both. A
176
+ `copilot.db` that is not newer, is empty, or holds only what a start of an
177
+ earlier version writes (no conversation, message or other work; a few
178
+ start-up settings) is set aside as
179
+ `copilot.db.older-<time>`, never deleted, and the relay runs on `oar.db`.
180
+ A `copilot.db` with newer work, or each newer somewhere, stops the start
181
+ (exit code 78) with both files named and nothing changed: move the one not
182
+ to go on with into a folder of your own, then start again. An `oar.db-wal`
183
+ or `oar.db-shm` without `oar.db` stops the start too (a database renamed to
184
+ `oar.db` later would take its pages in, losing the rows written since):
185
+ move it into a folder of your own.
186
+ - **Migrations and `npm run deepseek:import` run by hand never create an
187
+ empty database.** Without a path they take the relay's database in
188
+ `server/data` (`oar.db`, else `copilot.db`); a path given must exist.
189
+ - **`oar uninstall --purge` says it deletes the database backups** with the
190
+ state root.
191
+ - **The DeepSeek card's "Tokens by model" is now "Priced tokens by model"**:
192
+ the cache hits, misses and output its spend estimate prices, beside the
193
+ Tokens sections every tab gets.
194
+ - **The Claude API card's "Tokens by model" is replaced** by the Tokens
195
+ sections every tab gets, which show cache writes apart instead of inside
196
+ "other input". Its spend by model and the cap stay.
197
+ - **Cache writes are no longer lost** for the Claude family, Copilot, Cursor
198
+ and Grok: the token booking keeps them apart. The older bookings (the Claude
199
+ API spend, the DeepSeek estimate) still count them as misses, unchanged.
200
+ - **The Claude API spend no longer counts a turn twice** when Claude Code
201
+ reported one of its results without per-model usage: the next result's
202
+ growth used to include that turn again.
203
+ - **The systemd service restarts the relay with new code, waits for running
204
+ turns, and keeps session workers.** A relay under the systemd user service
205
+ hands a restart (**🌄 Restart web relay**, the **Update** button,
206
+ `oar update`, `oar restart`) to systemd once its turns are done, which
207
+ starts a fresh supervisor and runtime from the code on disk; before, only
208
+ the runtime was relaunched and the supervisor kept its old code. `oar stop`
209
+ and `oar restart` under the service now go through the relay too and wait
210
+ for running turns (the unit stays enabled after a stop); `--now` goes
211
+ through `systemctl --user` at once, as both did before, and so does a relay
212
+ that does not answer. Without the service, `--now` asks the relay not to
213
+ wait, as **Restart now** does. The relay starts its session workers in
214
+ scopes of their own (`systemd-run --user --scope`), so a stop or restart of
215
+ the service no longer ends them. Detached helpers the relay runs for a
216
+ while (a provider CLI being installed, a provider login) still end with a
217
+ full restart of the service. A stop that comes in while the restart is
218
+ handed to systemd wins: no restart follows. On a host with cgroup v1 next to
219
+ an unused v2 hierarchy (a bare `0::/` line) the relay still finds its unit.
220
+ - **The unit restarts a failing relay with a backoff and never gives up.**
221
+ The relay's supervisor relaunches the first two crashes itself; then
222
+ systemd starts it again after 2 seconds, with longer waits up to 5 minutes
223
+ (systemd before 254: every 10 seconds). The unit has `KillMode=mixed`, so
224
+ no runtime of a dead supervisor is left holding the port.
225
+ - **`oar service` works in a git checkout on Linux.**
226
+ `node bin/oar.js service install` writes a unit that runs
227
+ `node server/server.js` in the checkout, on its own config, data and logs;
228
+ `node bin/oar.js start|stop|restart|status` go through it. A hand-made unit
229
+ counts as the checkout's service only when its `ExecStart` runs
230
+ `<checkout>/server/server.js`.
231
+ - **Hand-made units are left as they are.** A unit OAR did not write for this
232
+ install (edited or written by hand, or another install's, such as the
233
+ global install beside a checkout) is no longer written over by
234
+ `oar service install` or `oar setup --start`, or deleted by
235
+ `oar service remove`: they name it and change nothing. `--replace` replaces
236
+ or removes it, keeping a copy as `oar.service.bak`. `oar service status`
237
+ says whose the unit is, lists the drop-ins in `oar.service.d/` (your own
238
+ settings, never touched), and says when lingering is off.
239
+
240
+ ### Fixed
241
+
242
+ - **Archiving or deleting a chat whose session worker is gone no longer
243
+ fails with "Couldn't stop this conversation's session".** The relay still
244
+ tried to stop the worker it last knew of; on Windows that stop could fail
245
+ on its own and answered 409 `worker-stop-failed` until a later try. A
246
+ worker whose process has ended counts as stopped. When a stop does fail,
247
+ the relay log says why.
248
+ - **An open fold in the Inbox no longer closes by itself.** Every change of
249
+ a question and the minute's check of paired relays drew the Inbox anew,
250
+ with every **Details** and plan fold closed. An open fold now stays open
251
+ until you close it or its item leaves the list; the same for a question's
252
+ **Details** on its card in the session.
253
+
254
+ ### Upgrade notes
255
+
256
+ - **Under the systemd service, run `oar service install` after updating**
257
+ (`node bin/oar.js service install` in a checkout) to get the new unit. A
258
+ hand-made unit is reported and kept; `oar service install --replace` puts
259
+ OAR's in its place, keeping a copy as `oar.service.bak` (drop-ins in
260
+ `oar.service.d/` stay and keep applying).
261
+ - **A checkout's hand-made unit that only runs in the checkout is not its
262
+ service.** A unit with `WorkingDirectory=<checkout>` whose `ExecStart` does
263
+ not name `<checkout>/server/server.js` (`npm start`, a relative
264
+ `server/server.js`) is kept as hand-made: `node bin/oar.js start` and
265
+ `restart` refuse, and `status` says it is not for this checkout. Run
266
+ `node bin/oar.js service install --replace` to put OAR's unit in its place,
267
+ keeping a copy as `oar.service.bak`.
268
+ - **The supervisor of 0.9.19 keeps running until the service restarts once
269
+ by systemd:** at an idle moment, run `oar restart --now`. That restart ends
270
+ running turns once; after it, restarts wait for them.
271
+ - **Session workers started before that restart may stop at it, once.** They
272
+ were started inside the service; those started afterwards run in scopes of
273
+ their own.
274
+ - **Nothing to do for the new database name.** A relay that ran on
275
+ `copilot.db` goes on running on it. To give it the new name, by hand and
276
+ with the relay stopped: take a backup (`oar backup --reason pre-rename`),
277
+ stop the relay fully (`oar stop`, or the systemd service or autostart, so
278
+ nothing starts it again), check that no program holds the files (Linux:
279
+ `fuser copilot.db*`; Windows: close database browsers and Explorer previews),
280
+ make sure no `oar.db-wal` or `oar.db-shm` is there already (move one that
281
+ is into a folder of your own), rename `copilot.db`, and `copilot.db-wal`
282
+ and `copilot.db-shm` where they are there, to `oar.db`, `oar.db-wal`,
283
+ `oar.db-shm`, and start again.
284
+ `oar doctor` then shows `oar.db`. Scripts of your own that open
285
+ `copilot.db` by path need the new name first.
286
+ - **Going back to 0.9.19 on `copilot.db` needs no step.** Migrations 0035 and
287
+ 0036 only add the column `conversations.plan_only`, the tables
288
+ `turn_token_usage` and `token_usage_days` and an index; 0.9.19 opens the
289
+ database as it is and ignores them, so a plan-only lock does not hold on
290
+ 0.9.19, and the tokens kept so far are there again at the next 0.9.20
291
+ start. Turns run on 0.9.19 book no tokens.
292
+ - **Going back to a version before 0.9.20 on `oar.db`** (a new 0.9.20 install,
293
+ or one renamed by hand): stop the relay fully, make sure no
294
+ `copilot.db-wal` or `copilot.db-shm` is there already (move one that is
295
+ into a folder of your own), rename `oar.db`, `oar.db-wal` and `oar.db-shm`
296
+ (those there) to `copilot.db`, `copilot.db-wal`, `copilot.db-shm`, then
297
+ start the earlier version. A relay still on
298
+ `copilot.db` needs no step. Skipped, the earlier version starts on a new,
299
+ empty `copilot.db`; back on 0.9.20 that one is set aside if nothing was done
300
+ on it, and the start stops with both names if something was.
301
+ - **The first 0.9.20 start backs up the database.** After `oar update` or a
302
+ `git pull` its name carries the running version and says
303
+ `pre-update-from-unknown`, such as
304
+ `oar-0.9.20-20261103-091500-pre-update-from-unknown.db`, though it holds the
305
+ data of the version before: earlier versions did not record which version
306
+ ran on the data. After an in-app update it carries the version before. The
307
+ time in a backup's name is UTC. It needs
308
+ free space for one more copy of the database; `oar doctor` shows the
309
+ result. Backups made by hand before (`copilot-pre-*`, `main-*`,
310
+ `oar-pre-*`) stay where they are and are never counted or deleted.
311
+ - **After going back to an earlier version and up again, run `oar backup`
312
+ before the upgrade:** versions before 0.9.20 do not record that they ran,
313
+ so the second upgrade sees no version change and takes no copy.
314
+ - **To restore a backup:** stop the relay, move the live database files
315
+ (`oar.db*` or `copilot.db*`) into a folder of your own, copy the backup into
316
+ the data folder (it is one file, no `-wal` or `-shm`), and start the version
317
+ the data is of. Name it as that version runs on it: `copilot.db` for a
318
+ version before 0.9.20, else the name the relay ran on. A
319
+ `pre-update-from-unknown` backup holds the data of the version before the
320
+ one in its name. Open a backup only read-only
321
+ (`sqlite3 'file:<backup>?immutable=1'`).
322
+
6
323
  ## [0.9.19] — 2026-10-27
7
324
 
8
325
  Highlights: a relay that runs out of memory is restarted instead of staying
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.19"><picture><source media="(prefers-color-scheme: light)" srcset="https://oar.sh/assets/shots/cfmail-mobile.light.png?v=0.9.19"><img src="https://oar.sh/assets/shots/cfmail-mobile.png?v=0.9.19" 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.20"><picture><source media="(prefers-color-scheme: light)" srcset="https://oar.sh/assets/shots/cfmail-mobile.light.png?v=0.9.20"><img src="https://oar.sh/assets/shots/cfmail-mobile.png?v=0.9.20" 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.19"><picture><source media="(prefers-color-scheme: light)" srcset="https://oar.sh/assets/shots/cfmail-agents.light.png?v=0.9.19"><img src="https://oar.sh/assets/shots/cfmail-agents.png?v=0.9.19" 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.20"><picture><source media="(prefers-color-scheme: light)" srcset="https://oar.sh/assets/shots/cfmail-agents.light.png?v=0.9.20"><img src="https://oar.sh/assets/shots/cfmail-agents.png?v=0.9.20" 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.19"><picture><source media="(prefers-color-scheme: light)" srcset="https://oar.sh/assets/shots/cfmail-question.light.png?v=0.9.19"><img src="https://oar.sh/assets/shots/cfmail-question.png?v=0.9.19" 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.20"><picture><source media="(prefers-color-scheme: light)" srcset="https://oar.sh/assets/shots/cfmail-question.light.png?v=0.9.20"><img src="https://oar.sh/assets/shots/cfmail-question.png?v=0.9.20" 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.19"><picture><source media="(prefers-color-scheme: light)" srcset="https://oar.sh/assets/shots/pingcf-thoughts.light.png?v=0.9.19"><img src="https://oar.sh/assets/shots/pingcf-thoughts.png?v=0.9.19" 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.20"><picture><source media="(prefers-color-scheme: light)" srcset="https://oar.sh/assets/shots/pingcf-thoughts.light.png?v=0.9.20"><img src="https://oar.sh/assets/shots/pingcf-thoughts.png?v=0.9.20" 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.19"><picture><source media="(prefers-color-scheme: light)" srcset="https://oar.sh/assets/shots/pagewright-watching.light.png?v=0.9.19"><img src="https://oar.sh/assets/shots/pagewright-watching.png?v=0.9.19" 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.20"><picture><source media="(prefers-color-scheme: light)" srcset="https://oar.sh/assets/shots/pagewright-watching.light.png?v=0.9.20"><img src="https://oar.sh/assets/shots/pagewright-watching.png?v=0.9.20" 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>
@@ -86,7 +86,7 @@ flowchart LR
86
86
 
87
87
  You need Node.js 22.13 or newer (the macOS and Linux installer brings its own when yours is missing or older) and an account with the agent you want to use; see [Prerequisites](docs/install.md#prerequisites). There are also installs [with npm](docs/install.md#npm) and [from a git checkout](docs/install.md#from-a-git-checkout).
88
88
 
89
- `oar` alone opens a menu. `oar start`, `stop`, `restart` and `status` control the relay, `oar service install` starts it at boot or sign-in (a systemd user service on Linux, the sign-in autostart on Windows), `oar doctor` shows what it uses, and `oar update` installs a new release (as does **⚙️ Settings → General → Check for updates**). All the details are in [Installing and running OAR](docs/install.md): [the `oar` command](docs/install.md#the-oar-command), [where OAR keeps its files](docs/install.md#where-oar-keeps-its-files), and [updating](docs/install.md#updating).
89
+ `oar` alone opens a menu. `oar start`, `stop`, `restart` and `status` control the relay, `oar service install` starts it at boot or sign-in and keeps it running (a systemd user service on Linux, also for a git checkout; the sign-in autostart on Windows), `oar doctor` shows what it uses, and `oar update` installs a new release (as does **⚙️ Settings → General → Check for updates**). All the details are in [Installing and running OAR](docs/install.md): [the `oar` command](docs/install.md#the-oar-command), [where OAR keeps its files](docs/install.md#where-oar-keeps-its-files), and [updating](docs/install.md#updating).
90
90
 
91
91
  <a id="providers"></a>
92
92
  <a id="claude-cloud"></a>
@@ -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.19"><picture><source media="(prefers-color-scheme: light)" srcset="https://oar.sh/assets/shots/cfmail-steering.light.png?v=0.9.19"><img src="https://oar.sh/assets/shots/cfmail-steering.png?v=0.9.19" 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.20"><picture><source media="(prefers-color-scheme: light)" srcset="https://oar.sh/assets/shots/cfmail-steering.light.png?v=0.9.20"><img src="https://oar.sh/assets/shots/cfmail-steering.png?v=0.9.20" 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.19"><picture><source media="(prefers-color-scheme: light)" srcset="https://oar.sh/assets/shots/usage-copilot-compact.light.png?v=0.9.19"><img src="https://oar.sh/assets/shots/usage-copilot-compact.png?v=0.9.19" 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.20"><picture><source media="(prefers-color-scheme: light)" srcset="https://oar.sh/assets/shots/usage-copilot-compact.light.png?v=0.9.20"><img src="https://oar.sh/assets/shots/usage-copilot-compact.png?v=0.9.20" 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 ([Notifications and the app](docs/web-ui.md#notifications-and-the-app)).
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.19](CHANGELOG.md#0919--2026-10-27) 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.20](CHANGELOG.md#0920--2026-10-29) 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.19"><picture><source media="(prefers-color-scheme: light)" srcset="https://oar.sh/assets/shots/pagewright-cross-relay.light.png?v=0.9.19"><img src="https://oar.sh/assets/shots/pagewright-cross-relay.png?v=0.9.19" 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.20"><picture><source media="(prefers-color-scheme: light)" srcset="https://oar.sh/assets/shots/pagewright-cross-relay.light.png?v=0.9.20"><img src="https://oar.sh/assets/shots/pagewright-cross-relay.png?v=0.9.20" 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