@oar-sh/oar 0.9.15 → 0.9.17

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 (109) hide show
  1. package/.github/extensions/web-relay/polling/polling-loop.mjs +14 -87
  2. package/CHANGELOG.md +305 -0
  3. package/README.md +12 -11
  4. package/package.json +2 -1
  5. package/server/claude-worker/claude-attachments.mjs +7 -4
  6. package/server/claude-worker/claude-remote-relay-tool.mjs +36 -1
  7. package/server/claude-worker/claude-sdk-adapter.mjs +167 -9
  8. package/server/claude-worker/claude-session-process.mjs +131 -33
  9. package/server/claude-worker/claude-session-worker-main.mjs +266 -0
  10. package/server/claude-worker/claude-session-worker.mjs +2 -253
  11. package/server/claude-worker/claude-turn-failure.mjs +8 -0
  12. package/server/claude-worker/claude-turn-publisher.mjs +282 -13
  13. package/server/claude-worker/claude-usage-totals.mjs +108 -0
  14. package/server/claude-worker/sdk-message-normalizer.mjs +55 -0
  15. package/server/copilot-worker/copilot-sdk-adapter.mjs +18 -4
  16. package/server/copilot-worker/copilot-sdk-session-process.mjs +73 -7
  17. package/server/db-schema.mjs +115 -0
  18. package/server/deepseek-worker/deepseek-claude-backend.mjs +433 -0
  19. package/server/deepseek-worker/deepseek-session-worker.mjs +37 -0
  20. package/server/migrations/0019-relay-event-subscription-options.mjs +106 -0
  21. package/server/migrations/0020-conversation-may-create-sessions.mjs +67 -0
  22. package/server/migrations/0021-conversation-group-agent-marks.mjs +77 -0
  23. package/server/migrations/0022-relay-schedules.mjs +142 -0
  24. package/server/migrations/0023-relay-asks.mjs +141 -0
  25. package/server/migrations/0024-deepseek-usage-totals.mjs +151 -0
  26. package/server/migrations/0025-deepseek-usage-requests-daily.mjs +179 -0
  27. package/server/migrations/0026-deepseek-usage-served-models.mjs +222 -0
  28. package/server/migrations/0027-deepseek-usage-checkpoint-runs.mjs +101 -0
  29. package/server/migrations/0028-deepseek-usage-checkpoint-request-runs.mjs +99 -0
  30. package/server/migrations/0029-deepseek-balance-rises.mjs +99 -0
  31. package/server/migrations/0030-deepseek-usage-actuals.mjs +123 -0
  32. package/server/public/app/api-client.js +53 -0
  33. package/server/public/app/ask-user-view.js +3 -0
  34. package/server/public/app/bootstrap.js +162 -16
  35. package/server/public/app/context-tier-options.mjs +4 -0
  36. package/server/public/app/context-usage-view.mjs +17 -0
  37. package/server/public/app/conversation-group-order.mjs +35 -0
  38. package/server/public/app/conversation-provider-indicator.mjs +7 -0
  39. package/server/public/app/conversation-view.js +2 -0
  40. package/server/public/app/deepseek-settings-ui.js +342 -0
  41. package/server/public/app/inbox-ui.js +367 -0
  42. package/server/public/app/inbox-view.mjs +370 -0
  43. package/server/public/app/journal-view.js +54 -5
  44. package/server/public/app/model-selector-options.mjs +11 -0
  45. package/server/public/app/model-vision-support.mjs +2 -0
  46. package/server/public/app/no-conversation-state.mjs +1 -1
  47. package/server/public/app/plan-usage-view.mjs +165 -13
  48. package/server/public/app/provider-readiness.mjs +4 -1
  49. package/server/public/app/relay-error-ctas.mjs +8 -0
  50. package/server/public/app/relay-event-subscriptions-ui.js +4 -2
  51. package/server/public/app/relay-event-view.mjs +33 -7
  52. package/server/public/app/relay-schedule-view.mjs +119 -0
  53. package/server/public/app/relay-schedules-ui.js +172 -0
  54. package/server/public/app/settings-modal.js +11 -0
  55. package/server/public/app/settings-tabs.js +1 -1
  56. package/server/public/app/socket-handlers.js +37 -3
  57. package/server/public/app/store.js +18 -5
  58. package/server/public/index.html +567 -35
  59. package/server/relay-tools.md +18 -3
  60. package/server/repositories/conversation-group-repository.mjs +110 -17
  61. package/server/repositories/remote-relay-repository.mjs +20 -2
  62. package/server/repositories/session-repository.mjs +7 -1
  63. package/server/routes/ask-user-routes.mjs +57 -3
  64. package/server/routes/cache-routes.mjs +6 -0
  65. package/server/routes/conversation-group-routes.mjs +19 -3
  66. package/server/routes/deepseek-usage-import-routes.mjs +72 -0
  67. package/server/routes/messages-routes.mjs +195 -18
  68. package/server/routes/relay-ask-routes.mjs +387 -0
  69. package/server/routes/relay-event-routes.mjs +3 -1
  70. package/server/routes/relay-schedule-routes.mjs +57 -0
  71. package/server/routes/sessions-routes.mjs +932 -97
  72. package/server/server-runtime.mjs +203 -11
  73. package/server/services/about-service.mjs +1 -0
  74. package/server/services/cache-rebuild-service.mjs +19 -2
  75. package/server/services/claude-session-leftovers.mjs +226 -0
  76. package/server/services/conversation-preferences-service.mjs +17 -0
  77. package/server/services/deepseek-balance-service.mjs +102 -0
  78. package/server/services/deepseek-settings-service.mjs +380 -0
  79. package/server/services/deepseek-usage-import.mjs +471 -0
  80. package/server/services/model-variant-catalog-service.mjs +1 -0
  81. package/server/services/plan-usage-contract.mjs +100 -5
  82. package/server/services/plan-usage-deepseek.mjs +931 -0
  83. package/server/services/plan-usage-service.mjs +431 -0
  84. package/server/services/provider-model-selection.mjs +1 -0
  85. package/server/services/provider-reasoning-effort.mjs +15 -2
  86. package/server/services/relay-ask-link-service.mjs +174 -0
  87. package/server/services/relay-ask-service.mjs +604 -0
  88. package/server/services/relay-event-service.mjs +589 -73
  89. package/server/services/relay-schedule-service.mjs +596 -0
  90. package/server/services/remote-relay-caller-context.mjs +89 -20
  91. package/server/services/remote-relay-dispatcher.mjs +958 -31
  92. package/server/services/session-worker-launch-service.mjs +233 -82
  93. package/server/services/worker-secret-redaction.mjs +318 -0
  94. package/shared/context-window-fallbacks.mjs +3 -0
  95. package/shared/deepseek-api.mjs +137 -0
  96. package/shared/deepseek-models.mjs +114 -0
  97. package/shared/deepseek-pricing.mjs +146 -0
  98. package/shared/host-claude-session-env.mjs +101 -0
  99. package/shared/model-id.mjs +7 -0
  100. package/shared/preview-tool-core.mjs +1 -1
  101. package/shared/provider-routing.mjs +2 -2
  102. package/shared/relay-asks.mjs +145 -0
  103. package/shared/relay-events.mjs +268 -18
  104. package/shared/relay-schedules.mjs +128 -0
  105. package/shared/remote-relay-contract.mjs +554 -26
  106. package/shared/remote-relay-tool-core.mjs +16 -5
  107. package/shared/steer-settle-failure.mjs +1 -0
  108. package/shared/worker-runtime/direct-openai-image.mjs +128 -0
  109. package/shared/worker-runtime/worker-log-file.mjs +60 -0
@@ -9,6 +9,14 @@ import { DEFAULT_QUESTION_TIMEOUT_MS } from "../../../../shared/question-timeout
9
9
  import { waitForRelayQuestion } from "../../../../shared/question-wait.mjs";
10
10
  import { EMPTY_TURN_COMPLETION_NOTE } from "../../../../shared/empty-turn-completion.mjs";
11
11
  import { stripPromptContextPrefix } from "../skills/prompt-context.mjs";
12
+ import {
13
+ isOpenAIImageTerminalError,
14
+ runDirectOpenAIImageTurn,
15
+ shouldUseDirectOpenAIImageApi,
16
+ } from "../../../../shared/worker-runtime/direct-openai-image.mjs";
17
+
18
+ // Re-exported for the extension's tests, which import it from here.
19
+ export { shouldUseDirectOpenAIImageApi };
12
20
 
13
21
  function isImageAttachment(att) {
14
22
  const type = String(att?.type || "").toLowerCase();
@@ -257,28 +265,6 @@ export function resolveEmptyFinalTextHandling({ lastStreamedSent = "", lastActiv
257
265
  };
258
266
  }
259
267
 
260
- export function shouldUseDirectOpenAIImageApi(message = {}) {
261
- const providerType = String(message?.providerType || "").trim().toLowerCase();
262
- if (providerType !== "openai") return false;
263
- const model = String(message?.providerModel || message?.model || "").trim().toLowerCase();
264
- return model.startsWith("gpt-image-") || model.startsWith("dall-e-");
265
- }
266
-
267
- function isDirectOpenAIImageTerminalStatus(status) {
268
- const numeric = Number(status);
269
- if (!Number.isFinite(numeric)) return false;
270
- return (
271
- numeric === 400
272
- || numeric === 401
273
- || numeric === 403
274
- || numeric === 404
275
- || numeric === 409
276
- || numeric === 422
277
- || numeric === 429
278
- || numeric === 503
279
- );
280
- }
281
-
282
268
  export async function publishRelayStreamEvent({
283
269
  api,
284
270
  message,
@@ -681,71 +667,12 @@ export function createPollingLoop({
681
667
  let sendAndWaitStartedAtMs = 0;
682
668
  const processDirectOpenAIImageRequest = async () => {
683
669
  if (!shouldUseDirectOpenAIImageApi(message)) return false;
684
- if (message?.imageOperationId) {
685
- const result = await api(
686
- "POST",
687
- `/api/image-operations/${encodeURIComponent(message.imageOperationId)}/execute`,
688
- { messageId: message.id },
689
- ).catch((error) => {
690
- if (error?.status === 409 || error?.status === 503) {
691
- return error?.payload || { outcome: error?.status === 503 ? "retryable" : "terminal" };
692
- }
693
- throw error;
694
- });
695
- const outcome = String(result?.outcome || "").trim().toLowerCase();
696
- if (!["completed", "executing", "terminal", "uncertain", "retryable"].includes(outcome)) {
697
- throw new Error("Image operation returned an invalid outcome");
698
- }
699
- await session.log(`Image operation ${outcome}`, {
700
- ephemeral: true,
701
- });
702
- return true;
703
- }
704
- const directModel = String(message?.providerModel || requestedManualModelOrNull(message) || message?.model || "").trim();
705
- const prompt = String(message?.text || "").trim();
706
- if (!directModel) throw new Error("OpenAI image request is missing model");
707
- if (!prompt) throw new Error("OpenAI image request is missing prompt");
708
- await api("POST", "/api/activity", {
709
- messageId: message.id,
710
- conversationId: message.conversationId,
711
- mode: message.relayMode || "agent",
712
- text: `Calling OpenAI image API directly (${directModel})`,
713
- }).catch(() => {});
714
- let directResult;
715
- try {
716
- directResult = await api("POST", "/api/openai/images/generate", {
717
- messageId: message.id,
718
- conversationId: message.conversationId,
719
- model: directModel,
720
- prompt,
721
- n: message?.n,
722
- size: message?.size || message?.contextTier,
723
- quality: message?.quality || message?.reasoningEffort,
724
- attachments: Array.isArray(message?.attachments) ? message.attachments : [],
725
- });
726
- } catch (error) {
727
- if (isDirectOpenAIImageTerminalStatus(error?.status)) {
728
- const terminalError = new Error(
729
- `OpenAI image request failed: ${error?.detail || `HTTP ${error?.status || "error"}`}`,
730
- );
731
- terminalError.code = "RELAY_OPENAI_IMAGE_TERMINAL";
732
- throw terminalError;
733
- }
734
- throw error;
735
- }
736
- const generatedImages = Array.isArray(directResult?.generatedImages) ? directResult.generatedImages : [];
737
- if (!generatedImages.length) {
738
- throw new Error("OpenAI image API returned no images");
670
+ const result = await runDirectOpenAIImageTurn({ api, message });
671
+ if (result.via === "operation") {
672
+ await session.log(`Image operation ${result.outcome}`, { ephemeral: true });
673
+ } else {
674
+ await session.log(`✅ Sent generated image response (${result.imageCount} image${result.imageCount === 1 ? "" : "s"})`, { ephemeral: true });
739
675
  }
740
- await api("POST", "/api/response", {
741
- messageId: message.id,
742
- conversationId: message.conversationId,
743
- text: "",
744
- generatedImages,
745
- model: String(directResult?.model || directModel).trim() || directModel,
746
- modelOrigin: isAutoRequestedModel(message?.model) ? "auto" : "manual",
747
- });
748
- await session.log(`✅ Sent generated image response (${generatedImages.length} image${generatedImages.length === 1 ? "" : "s"})`, { ephemeral: true });
749
676
  return true;
750
677
  };
751
678
 
@@ -988,7 +915,7 @@ export function createPollingLoop({
988
915
  }).catch(async () => {
989
916
  await api("POST", "/api/requeue", { messageId: message.id }).catch(() => {});
990
917
  });
991
- } else if (String(e?.code || "").trim() === "RELAY_OPENAI_IMAGE_TERMINAL") {
918
+ } else if (isOpenAIImageTerminalError(e)) {
992
919
  await pushRelayStream(lastStreamedSent, true);
993
920
  await api("POST", "/api/response", {
994
921
  messageId: message.id,
package/CHANGELOG.md CHANGED
@@ -3,6 +3,311 @@
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.17] — 2026-10-25
7
+
8
+ Highlights: DeepSeek is a provider of its own. Paste a DeepSeek API key and
9
+ New Chat offers DeepSeek's models, with all four modes, tools, question
10
+ cards, sub-agents and reasoning effort, the 1M context window, a usage card
11
+ with the balance and the estimated spend, and an import of earlier usage
12
+ from DeepSeek's usage export. A new session in a group now shows
13
+ at the top of the group. And the relay's own Anthropic credentials
14
+ (`ANTHROPIC_API_KEY`, `ANTHROPIC_AUTH_TOKEN`) no longer reach the agents it
15
+ starts.
16
+
17
+ ### Added
18
+
19
+ - **DeepSeek is a provider of its own.** Paste a DeepSeek API key in
20
+ Settings → Providers → DeepSeek, and New Chat offers DeepSeek with the
21
+ models DeepSeek lists (`deepseek-flash` by default, `deepseek-v4-pro`).
22
+ - A DeepSeek chat runs Claude Code on the relay host against DeepSeek's
23
+ Anthropic-compatible API, so it has all four modes, tools, question
24
+ cards, sub-agents, steering, Stop and background tasks.
25
+ - Reasoning effort is low, high or max (high by default). The model can
26
+ change between turns; a switch to a model without image input is
27
+ refused once the chat holds images.
28
+ - The context panel shows the 1M window, and the chat compacts at 400K
29
+ unless you set another window there.
30
+ - Commits carry `Open Agent Relay (DeepSeek Flash)` or no attribution, as
31
+ set on the DeepSeek tab.
32
+ - The key is stored on the relay host, is never sent to browsers, and
33
+ reaches only DeepSeek's workers. Their Claude Code keeps its state in a
34
+ directory of its own, without your Claude login. The tab's Advanced
35
+ section takes another base URL (https, or http on the same machine).
36
+ - The chat folder's settings can't route the key elsewhere. A
37
+ `.claude/settings.json` or `.claude/settings.local.json` there that sets
38
+ `ANTHROPIC_BASE_URL`, a key of its own or an `apiKeyHelper` took the
39
+ DeepSeek key to that endpoint, or replaced it; the relay now pins
40
+ DeepSeek's endpoint and key over the folder's settings, and with them
41
+ the proxy, TLS trust, provider and credential settings, without putting
42
+ the key on the command line, and sends nothing when Claude Code doesn't
43
+ take them within a minute (`scripts/deepseek-cli-proof.mjs` shows it on
44
+ the bundled Claude Code). This covers settings, not commands: hooks and
45
+ a status line in those files still run as the relay's user, as in any
46
+ Claude Code chat, so use DeepSeek chats only in folders you trust.
47
+ - The agent's own commands can read the key from their environment, as
48
+ with any key a CLI holds. The relay replaces it with `[redacted]` in
49
+ everything a worker posts (replies, thoughts, tool activity, sub-agent
50
+ text, questions, task output), including the first part of a key cut
51
+ short at a text's end or before an ellipsis, the last part of one a
52
+ text starts with, and keys changed since the relay started, before it
53
+ stores or shows it; the DeepSeek worker does the same in its log. Task
54
+ output the relay reads from a task's file is scrubbed by shape only
55
+ (`sk-` keys and other long tokens), not by the saved key's exact text.
56
+ This keeps the key out of what the relay stores and shows; it is no
57
+ barrier against the agent, which can change the text, call the relay's
58
+ local API without the worker header or read the relay's database.
59
+ - The usage view has a DeepSeek card: the account balance, read with the
60
+ saved key, and the estimated spend of DeepSeek turns on this relay, by
61
+ model, from DeepSeek's published prices with peak and off-peak hours
62
+ (the card names the peak hours in your browser's time zone). On Chinese
63
+ public holidays DeepSeek charges off-peak prices all day; the estimate
64
+ doesn't know those dates, so turns at peak hours on a holiday are
65
+ counted at double the real price. Only WebSearch runs on DeepSeek's
66
+ servers: its searches are counted at $0.75 / ¥5 per 1,000 requests from
67
+ 2026-10-31 16:00 UTC, on a "Search" row of their own, taken from Claude
68
+ Code's own totals, which count the request its WebSearch tool makes.
69
+ WebFetch reads the page on the relay host and costs only tokens, and
70
+ DeepSeek does not bill count_tokens requests.
71
+ - The card draws the money: a balance bar per currency, measured against
72
+ the balance after its last top-up (kept on the relay; migration 0029
73
+ notes when it rose), or against a starting balance set on the DeepSeek
74
+ tab, such as the first top-up, until the next top-up replaces it (typed
75
+ in again, the same amount too, it counts from then on); the
76
+ daily spend of the last 30 days as columns stacked by model, with the
77
+ day's total on hover or tap; where the money goes per model (cache hits, misses,
78
+ output, searches) with what the cache saved; and a monthly
79
+ budget bar when a budget is set on the DeepSeek tab, in the currency
80
+ chosen with it (USD or CNY, named on the bar; a budget saved without one
81
+ follows the balance's currency), amber near it and red over it, starting
82
+ over on the 1st (UTC). No chart library; every bar has a text equivalent
83
+ and every chart a table, in both themes. The card shows everything at
84
+ once, figures first (the balance and the spend as label-and-value rows,
85
+ the tables by model open, the note on the estimate last), and fits a
86
+ phone's width.
87
+ - A DeepSeek chat's cold start reads "Starting Claude Code for DeepSeek",
88
+ and the `preview` tool tells the agent to bind its dev server to
89
+ 127.0.0.1 (every provider).
90
+ - Sub-agents stay on the chat's model. Claude Code's model aliases are
91
+ pinned to it, so a sub-agent the agent asks for as "opus" or "sonnet",
92
+ or one without a model, runs on the chat's model, and one asked for as
93
+ "haiku" on DeepSeek Flash; before, an alias went to DeepSeek as a Claude
94
+ model id, which DeepSeek serves by its own mapping (a Flash chat got a
95
+ V4 Pro sub-agent). A model switch between turns now restarts the chat's
96
+ Claude Code, like an effort change. A sub-agent's badge names the
97
+ DeepSeek model, and usage a Claude Code still reports under a Claude
98
+ name is booked and priced under the DeepSeek model that served it;
99
+ rows booked under a Claude name earlier are folded in at start-up
100
+ (migration 0026). The folded rows are priced at one time each, the
101
+ row's last turn, so a row that spans peak and off-peak turns is
102
+ approximated (the token part by up to 2×).
103
+ - **Import usage export** on the DeepSeek tab takes the two CSV files
104
+ DeepSeek's usage page exports (amount and cost) and shows what was
105
+ imported. The usage card then shows DeepSeek's own figures for the saved
106
+ key on the days the export covers, by the export's days, and the
107
+ estimate on the other days; the card says through when. Only rows in
108
+ the card's currency are used, and the import says when they won't be.
109
+ A re-import replaces the key's days it covers, a day with a period that
110
+ has not ended is left out, and so are export days up to and including
111
+ the first day of the relay's daily history.
112
+ `npm run deepseek:import -- amount.csv cost.csv [db-path]` does the same
113
+ from the shell. The relay's own usage logging is unchanged (migration
114
+ 0030 adds a table of its own).
115
+
116
+ ### Changed
117
+
118
+ - **A new session in a group shows at the top of the group.** In a group
119
+ you ordered by hand, a session an agent creates there, one started with
120
+ the group chosen, or one moved in by its menu used to join at the end;
121
+ it now comes first, right under the group's first session. The order you
122
+ set stays, and the next drag numbers the members again from there.
123
+ - **A host's `ANTHROPIC_API_KEY` and `ANTHROPIC_AUTH_TOKEN` no longer reach
124
+ any worker.** A relay started with either in its environment handed it to
125
+ every worker it started (Copilot, OpenAI, Cursor, Grok, Claude, Claude
126
+ Cloud, DeepSeek), on every platform, and Claude Code prefers either to the
127
+ Claude login. Now no worker gets them, and under tmux the worker's shell
128
+ also unsets them, so they cannot come back from a tmux server that was
129
+ already running or from your login profile. Claude chats that ran on such
130
+ a key now run on the host's Claude login.
131
+
132
+ ### Fixed
133
+
134
+ - **The Inbox opens at the top, or where you left it.** It used to open at
135
+ the scroll height of the transcript it replaced, somewhere in the middle of
136
+ the list. It now opens at the top the first time and afterwards where it
137
+ was scrolled when you last left it.
138
+ - **The session header on a phone comes in two rows.** ☰, the title and the
139
+ buttons share the first row, so the title gets the width left after the
140
+ buttons. The "via agent" line, the watching, watched and scheduled chips
141
+ and the folder sit side by side on a second row across the header, and a
142
+ chip no longer breaks inside its text. The desktop header is unchanged.
143
+ - **A DeepSeek chat's failures carry DeepSeek's codes, and their buttons
144
+ show.** A refused DeepSeek request printed as `relay.claude-api-error`,
145
+ and a used-up balance or a refused key as `relay.claude-deepseek-balance`
146
+ or `relay.claude-deepseek-api-key`, which the **Top up DeepSeek** and
147
+ **DeepSeek settings** buttons never matched. They now print as
148
+ `relay.deepseek-balance`, `relay.deepseek-api-key`,
149
+ `relay.deepseek-api-error` and so on, without the Claude account advice.
150
+ - **A relay's Claude login token no longer reaches workers that are not
151
+ Claude's.** On a relay started with `CLAUDE_CODE_OAUTH_TOKEN` in its
152
+ environment, the Copilot, OpenAI, Cursor and Grok workers got the token too:
153
+ on Windows and without tmux through the environment they inherit, and under
154
+ tmux whenever the launch started the tmux server, which then kept it for
155
+ every later session. Now only Claude Cloud and Claude workers get it, under
156
+ tmux through the owner-only file. Local Claude chats on a host that runs on
157
+ the token alone now get it under tmux as well.
158
+ - **A relay started from inside a Claude Code session no longer hands that
159
+ session to its workers.** Its variables (`CLAUDECODE`, the session id, the
160
+ messaging socket and token, entrypoint, `CLAUDE_PID`, `CLAUDE_EFFORT`)
161
+ reached every worker, with and without tmux, and a DeepSeek chat's Claude
162
+ Code took itself for a child of the host's session. The launcher now drops
163
+ them for every provider, and the DeepSeek backend drops them again. The
164
+ values of one launch (worker kind, model, folder) go into that session's
165
+ command only, no longer into the environment a tmux server started by the
166
+ relay kept for every later session. Under tmux that was not enough: a new
167
+ tmux session starts from the tmux server's own environment, which a
168
+ server started from inside a Claude Code session, or by an older relay,
169
+ carried on to every worker. Every worker's command now begins by unsetting
170
+ the host session's variables and the launch variables it sets no value
171
+ for, so a stale model or worker kind from an earlier launch is gone too.
172
+ The host session's variables are matched by family as well as by name
173
+ (every `CLAUDE_CODE_SESSION_*`, `CLAUDE_CODE_*_SESSION_ID` and
174
+ `CLAUDE_CODE_MESSAGING_*`, with the SDK host's version and app markers
175
+ and the Bash cap a relay's Claude worker gives its CLI), so a name a
176
+ newer Claude Code adds is dropped too; your own `CLAUDE_CODE_*` and
177
+ `CLAUDE_AGENT_SDK_*` settings stay. A DeepSeek chat's Claude Code also no
178
+ longer gets the relay's settings for its worker (worker kind, the relay's
179
+ folders and tools file, `COPILOT_ALLOW_ALL`, `SESSION_ID`);
180
+ with them the worker log file a relay started from inside a worker
181
+ session inherited, and, by name, every secret the launch carries no value
182
+ for (other kinds' and its own alike) that a shell may have exported into
183
+ the tmux server's environment; the worker's own keys come from its secret
184
+ file, read after that.
185
+ - **The DeepSeek spend no longer undercounts a Claude Code that started
186
+ counting over, and no longer double-counts a report that fell behind.**
187
+ The relay books what a worker's report adds to the session's last totals;
188
+ totals below them were booked as nothing. Now the worker names its Claude
189
+ Code process in each report (`runId`; migration 0027 keeps it with the
190
+ checkpoint): totals below the checkpoint from the same process are a
191
+ stale report (a retried post, or one delivered after a later one) and
192
+ book nothing, while from another process they are a CLI counting from
193
+ zero again on the same session id, booked in full. The web searches and
194
+ fetches are reported as the process's running counts and booked the same
195
+ way, so a retried post no longer counts them twice.
196
+ - **The DeepSeek spend no longer books a session's history again after a
197
+ Claude Code died hard.** Claude Code writes its cost state at exit, so a
198
+ resumed CLI that was killed left an older snapshot, and the next resume
199
+ restored totals below what the relay had booked: read as a process
200
+ counting from zero, the whole restored history was booked once more. The
201
+ worker no longer reports what the CLI restored: it reports each process's
202
+ own totals, counted from zero (what the session totals grow by per
203
+ result; for the first result of a resumed process the turn's own usage,
204
+ which leaves out what sub-agents spent in that one turn), and the relay
205
+ books a new process in full and the increase within one. The search and
206
+ fetch counts keep a run id of their own in the checkpoint (migration
207
+ 0028), so a report with counts only no longer moves the token totals' run
208
+ id, and a `/clear` in the CLI, which starts a new session id, starts the
209
+ worker's counts over with it instead of booking the old session's under
210
+ the new one. A turn that finishes after its Claude Code died (a background
211
+ task's continuation whose row was still registering) reports under that
212
+ process, not under the one running by then or as a report from no
213
+ process, and a result that arrives behind a newer one never lowers the
214
+ worker's baseline. A late report of an earlier process after a later one
215
+ started is booked in full, and the later process's next report is booked
216
+ in full too: the relay cannot tell either from a new process.
217
+ - **OpenAI image models work on the SDK engine again.** A turn on
218
+ `gpt-image-*` or `dall-e-*` went to the Copilot runtime as a chat turn
219
+ and failed ("No available capacity was found for the model", or a request
220
+ rejected as invalid). It now goes to the OpenAI Images API, as on the
221
+ Extension engine, and an attached image is edited. A refused image request
222
+ now fails its turn with `relay.image-provider-rejected` instead of staying
223
+ in progress until the relay ran it again, and an OpenAI chat turn that
224
+ fails on the SDK engine says OpenAI rather than Copilot.
225
+
226
+ ## [0.9.16] — 2026-10-24
227
+
228
+ Highlights: agents can file the sessions they start into your session
229
+ groups, change another session's model, mode or other settings, watch other
230
+ sessions with fewer wake-ups (digests, quiet hours, exclusions), and
231
+ schedule prompts that survive a relay restart. Agents can ask you a question
232
+ without stopping their work, and the new Inbox lists every open question
233
+ from this relay and its paired relays in one place. A new switch lets the
234
+ agents of a conversation start sessions themselves.
235
+
236
+ ### Added
237
+
238
+ - **Agents can put a new session into an existing session group.**
239
+ `remote_relay` create_session takes `group` (a group id, or its name in
240
+ any case) on this relay or a paired one, and `relay_info` lists the groups
241
+ there. An unknown group is refused with the groups listed, and nothing is
242
+ created. The approval card names the group, and the result carries it. A
243
+ relay too old for this is refused before anything is created. Renaming
244
+ and rearranging groups stays the owner's. An argument an action does not
245
+ take is no longer dropped without a word: create_session refuses it, and
246
+ the other actions list it in `ignored`.
247
+ - **Agents can change another session's settings.** `remote_relay`
248
+ update_session sets a session's model, reasoning effort, relay mode,
249
+ auto-compact window, title, group, thinking or working folder for its next
250
+ turns, on this relay or a paired one, and answers the settings and what
251
+ changed. An unknown model is refused with the close matches; a group name
252
+ that does not exist makes a new group, and `none` takes the session out (a
253
+ group you made is never removed this way: it stays, shown empty in the
254
+ sidebar for you to rename or dissolve); a new folder
255
+ restarts the session's CLI there and is refused while it works. Only a
256
+ session the agent's own conversation started may be changed freely; any
257
+ other, yours or another agent's, needs your Allow once per conversation and
258
+ relay. A failed change still lists what it wrote. An open page shows the
259
+ change at once.
260
+ - **"Agents in this conversation may start sessions."** A switch in the 🧠
261
+ settings of a session an agent started lets its agents start sessions
262
+ themselves, past the one-level rule; the limit of four at work and your
263
+ first Allow still apply. Only you can set it: agents and paired relays are
264
+ refused.
265
+ - **Fewer wake-ups for watching agents.** A `remote_relay` subscription takes
266
+ `digest_minutes` (its events come as one message every N minutes),
267
+ `quiet_hours` (a daily window in a time zone, crossing midnight and the
268
+ clock changes included, whose events come as one message when it ends) and
269
+ `exclude` (sessions a subscription to all leaves out). Questions and errors
270
+ still come at once, held events survive a restart, and the hourly limit
271
+ stays on top. A turn the watcher prompted itself and whose finished reply it
272
+ waited for no longer wakes it a second time with turn_end.
273
+ `list_subscriptions` shows how many events each subscription holds and when
274
+ they are due, and the watching chip shows a small digest or quiet-hours hint.
275
+ - **Scheduled prompts that survive a restart.** `remote_relay` schedule
276
+ queues a prompt at a time (`at`) or every N minutes (`every_minutes`, 5 to
277
+ 10080), into the agent's own conversation or, with the user's approval in
278
+ ask and plan mode, another session on this relay. The relay keeps it in its
279
+ database, so a nightly review or a sweep no longer has to be set up again
280
+ after a deploy. A time missed while the relay was down fires once when it is
281
+ back, and a repeat waits while the one before is still queued. The message
282
+ says it is automated and approves nothing. `list_schedules` and `unschedule`
283
+ manage them, archiving a session ends its schedules, and a conversation
284
+ holds at most 20. The sidebar marks a scheduled session, and its header
285
+ lists the schedules with a Cancel button each.
286
+ - **Questions that don't block, and the Inbox.** `remote_relay` ask puts a
287
+ question for you into the agent's session (up to eight options, a
288
+ recommendation, a folded detail, and urgency `now` for a push) and returns
289
+ at once, so the agent goes on with what does not depend on it. Your answer
290
+ or dismissal reaches it as a relay event, question_answered, without a
291
+ subscription, and does not count as your message for the agents' loop
292
+ guard. A lead can ask on behalf of another session, on this relay or a
293
+ paired one (kept where that session lives); `list_questions` and `withdraw`
294
+ manage them, and `read_session` lists them with their kind. The new
295
+ **Inbox** in the sidebar, with its count, lists every pending question
296
+ across this relay and the paired relays that answer, grouped by relay,
297
+ with a filter for your sessions or the agents', and what you answered in
298
+ the last day under a divider. An ask is answered there or in its session,
299
+ also one on a paired relay; a paired relay's cards open there, and the
300
+ pairing never carries an answer to a card. Agent text shows as plain text,
301
+ and on a phone the Inbox is one column with full-width buttons.
302
+
303
+ ### Fixed
304
+
305
+ - **The agents' loop guard no longer counts the relay's own messages as the
306
+ user's.** A relay event, the usage-limit follow-up or the note after a
307
+ restart reset the count of prompts an agent may send to other agents
308
+ before the user is asked, so two agents that watched each other could go
309
+ on without the user. Only a message the user wrote resets it now.
310
+
6
311
  ## [0.9.15] — 2026-10-22
7
312
 
8
313
  Highlights: `oar setup` asks how the relay starts on Windows, at sign-in or
package/README.md CHANGED
@@ -17,7 +17,7 @@ On Windows, run `irm oar.sh/install.ps1 | iex` in PowerShell. Other ways to inst
17
17
 
18
18
  ## What OAR does
19
19
 
20
- Coding agents such as GitHub Copilot, Claude Code, Cursor and Grok usually live in a terminal. OAR puts them into a chat you can open anywhere: start a task at your desk, follow it from the sofa, answer the agent's question from your phone. The agents keep running on your own computer, with your files and your logins; OAR passes the conversation back and forth.
20
+ Coding agents such as GitHub Copilot, Claude Code, Cursor, Grok and DeepSeek usually live in a terminal. OAR puts them into a chat you can open anywhere: start a task at your desk, follow it from the sofa, answer the agent's question from your phone. The agents keep running on your own computer, with your files and your logins; OAR passes the conversation back and forth.
21
21
 
22
22
  ```mermaid
23
23
  flowchart LR
@@ -27,7 +27,7 @@ flowchart LR
27
27
  end
28
28
  subgraph host["Your computer"]
29
29
  relay["OAR relay<br/>chats, queue, history, files"]
30
- agents["Copilot · OpenAI · Claude<br/>Cursor · Grok"]
30
+ agents["Copilot · OpenAI · Claude<br/>Cursor · Grok · DeepSeek"]
31
31
  end
32
32
  cloud["☁️ Claude Cloud<br/>sandbox at Anthropic"]
33
33
  phone <--> relay
@@ -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.15"><picture><source media="(prefers-color-scheme: light)" srcset="https://oar.sh/assets/shots/cfmail-mobile.light.png?v=0.9.15"><img src="https://oar.sh/assets/shots/cfmail-mobile.png?v=0.9.15" 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.17"><picture><source media="(prefers-color-scheme: light)" srcset="https://oar.sh/assets/shots/cfmail-mobile.light.png?v=0.9.17"><img src="https://oar.sh/assets/shots/cfmail-mobile.png?v=0.9.17" 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.15"><picture><source media="(prefers-color-scheme: light)" srcset="https://oar.sh/assets/shots/cfmail-agents.light.png?v=0.9.15"><img src="https://oar.sh/assets/shots/cfmail-agents.png?v=0.9.15" 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.17"><picture><source media="(prefers-color-scheme: light)" srcset="https://oar.sh/assets/shots/cfmail-agents.light.png?v=0.9.17"><img src="https://oar.sh/assets/shots/cfmail-agents.png?v=0.9.17" 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.15"><picture><source media="(prefers-color-scheme: light)" srcset="https://oar.sh/assets/shots/cfmail-question.light.png?v=0.9.15"><img src="https://oar.sh/assets/shots/cfmail-question.png?v=0.9.15" 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.17"><picture><source media="(prefers-color-scheme: light)" srcset="https://oar.sh/assets/shots/cfmail-question.light.png?v=0.9.17"><img src="https://oar.sh/assets/shots/cfmail-question.png?v=0.9.17" 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.15"><picture><source media="(prefers-color-scheme: light)" srcset="https://oar.sh/assets/shots/pingcf-thoughts.light.png?v=0.9.15"><img src="https://oar.sh/assets/shots/pingcf-thoughts.png?v=0.9.15" 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.17"><picture><source media="(prefers-color-scheme: light)" srcset="https://oar.sh/assets/shots/pingcf-thoughts.light.png?v=0.9.17"><img src="https://oar.sh/assets/shots/pingcf-thoughts.png?v=0.9.17" 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.15"><picture><source media="(prefers-color-scheme: light)" srcset="https://oar.sh/assets/shots/pagewright-watching.light.png?v=0.9.15"><img src="https://oar.sh/assets/shots/pagewright-watching.png?v=0.9.15" 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.17"><picture><source media="(prefers-color-scheme: light)" srcset="https://oar.sh/assets/shots/pagewright-watching.light.png?v=0.9.17"><img src="https://oar.sh/assets/shots/pagewright-watching.png?v=0.9.17" 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>
@@ -105,6 +105,7 @@ The runtime (the *provider* in the UI) is chosen in **New Chat** and stays fixed
105
105
  | [**Claude Cloud**](docs/providers.md#claude-cloud) (experimental) | Claude Code in a sandbox at Anthropic, on a GitHub repository | A claude.ai account with GitHub connected |
106
106
  | [**Cursor**](docs/providers.md#cursor-agent-sdk) | Cursor's agent | A Cursor API key |
107
107
  | [**Grok**](docs/providers.md#grok-cli-acp) | xAI's Grok CLI | A Grok sign-in, made from **⚙️ Settings** |
108
+ | [**DeepSeek**](docs/providers.md#deepseek) | Claude Code on your machine, thinking with DeepSeek's models | A DeepSeek API key |
108
109
 
109
110
  API keys are entered in **⚙️ Settings** and stay on the relay host. The Claude and Grok CLIs can be [installed and signed in to from the browser](docs/providers.md#installing-a-provider-cli-from-the-relay), so no shell on the host is needed. Each runtime discovers its own [models](docs/providers.md#models). The full picture of each runtime is in [Providers](docs/providers.md).
110
111
 
@@ -116,11 +117,11 @@ API keys are entered in **⚙️ Settings** and stay on the relay host. The Clau
116
117
  <table>
117
118
  <tr>
118
119
  <td width="62%" valign="top">
119
- <a href="https://oar.sh/assets/shots/cfmail-steering.png?v=0.9.15"><picture><source media="(prefers-color-scheme: light)" srcset="https://oar.sh/assets/shots/cfmail-steering.light.png?v=0.9.15"><img src="https://oar.sh/assets/shots/cfmail-steering.png?v=0.9.15" 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>
120
+ <a href="https://oar.sh/assets/shots/cfmail-steering.png?v=0.9.17"><picture><source media="(prefers-color-scheme: light)" srcset="https://oar.sh/assets/shots/cfmail-steering.light.png?v=0.9.17"><img src="https://oar.sh/assets/shots/cfmail-steering.png?v=0.9.17" 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>
120
121
  <br><sub>Send a message while the agent works, and it steers the running turn.</sub>
121
122
  </td>
122
123
  <td width="38%" valign="top">
123
- <a href="https://oar.sh/assets/shots/usage-copilot-compact.png?v=0.9.15"><picture><source media="(prefers-color-scheme: light)" srcset="https://oar.sh/assets/shots/usage-copilot-compact.light.png?v=0.9.15"><img src="https://oar.sh/assets/shots/usage-copilot-compact.png?v=0.9.15" 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>
124
+ <a href="https://oar.sh/assets/shots/usage-copilot-compact.png?v=0.9.17"><picture><source media="(prefers-color-scheme: light)" srcset="https://oar.sh/assets/shots/usage-copilot-compact.light.png?v=0.9.17"><img src="https://oar.sh/assets/shots/usage-copilot-compact.png?v=0.9.17" 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>
124
125
  <br><sub>What each plan has left, in one place.</sub>
125
126
  </td>
126
127
  </tr>
@@ -137,7 +138,7 @@ API keys are entered in **⚙️ Settings** and stay on the relay host. The Clau
137
138
  - **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)).
138
139
  - **On your phone**: install OAR as an app and get push notifications ([Notifications and the app](docs/web-ui.md#notifications-and-the-app)).
139
140
 
140
- 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.15](CHANGELOG.md#0915--2026-10-22) as the latest.
141
+ 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.17](CHANGELOG.md#0917--2026-10-25) as the latest.
141
142
 
142
143
  ## Remote access
143
144
 
@@ -188,7 +189,7 @@ flowchart LR
188
189
 
189
190
  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.
190
191
 
191
- <p><a href="https://oar.sh/assets/shots/pagewright-cross-relay.png?v=0.9.15"><picture><source media="(prefers-color-scheme: light)" srcset="https://oar.sh/assets/shots/pagewright-cross-relay.light.png?v=0.9.15"><img src="https://oar.sh/assets/shots/pagewright-cross-relay.png?v=0.9.15" 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>
192
+ <p><a href="https://oar.sh/assets/shots/pagewright-cross-relay.png?v=0.9.17"><picture><source media="(prefers-color-scheme: light)" srcset="https://oar.sh/assets/shots/pagewright-cross-relay.light.png?v=0.9.17"><img src="https://oar.sh/assets/shots/pagewright-cross-relay.png?v=0.9.17" 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>
192
193
 
193
194
  How pairing, permissions and limits work is in [Agents working together](docs/agents-together.md).
194
195
 
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@oar-sh/oar",
3
- "version": "0.9.15",
3
+ "version": "0.9.17",
4
4
  "type": "module",
5
5
  "description": "OAR — Open Agent Relay: drive your local coding agents from any browser",
6
6
  "license": "MIT",
@@ -49,6 +49,7 @@
49
49
  "setup:git": "node scripts/setup-git.mjs",
50
50
  "land": "node scripts/land.mjs",
51
51
  "prune:local": "node scripts/prune-local.mjs",
52
+ "deepseek:import": "node server/services/deepseek-usage-import.mjs",
52
53
  "test:e2e:raw": "node ./node_modules/@playwright/test/cli.js test --config tests/playwright.config.mjs"
53
54
  },
54
55
  "dependencies": {
@@ -52,6 +52,9 @@ function imageBlockFromDataUrl(att, maxInlineImageBytes) {
52
52
  export function buildClaudeAttachmentContent(rawAttachments, {
53
53
  fsImpl = fs,
54
54
  maxInlineImageBytes = MAX_INLINE_IMAGE_BYTES,
55
+ // False for a model that takes no image input: an image block would fail
56
+ // the whole request, so every image goes by its path instead.
57
+ embedImages = true,
55
58
  } = {}) {
56
59
  const input = Array.isArray(rawAttachments) ? rawAttachments : [];
57
60
  const imageBlocks = [];
@@ -67,7 +70,7 @@ export function buildClaudeAttachmentContent(rawAttachments, {
67
70
  // Only media types the Anthropic image block accepts are embedded;
68
71
  // everything else (svg, bmp, heic, …) would 400 the whole turn and
69
72
  // falls back to a path reference instead.
70
- if (filePath && isEmbeddableImageMediaType(mime) && fsImpl.existsSync(filePath)) {
73
+ if (embedImages && filePath && isEmbeddableImageMediaType(mime) && fsImpl.existsSync(filePath)) {
71
74
  try {
72
75
  const bytes = fsImpl.readFileSync(filePath);
73
76
  if (Buffer.isBuffer(bytes) && bytes.length && bytes.length <= maxInlineImageBytes) {
@@ -85,7 +88,7 @@ export function buildClaudeAttachmentContent(rawAttachments, {
85
88
  embedded = false;
86
89
  }
87
90
  }
88
- if (!embedded) {
91
+ if (!embedded && embedImages) {
89
92
  const blockFromDataUrl = imageBlockFromDataUrl(att, maxInlineImageBytes);
90
93
  if (blockFromDataUrl) {
91
94
  imageBlocks.push(blockFromDataUrl);
@@ -118,9 +121,9 @@ export function buildClaudeAttachmentContent(rawAttachments, {
118
121
  * Text first (user text + attachment notes + server-provided attachment
119
122
  * context), then inline image blocks.
120
123
  */
121
- export function buildClaudeUserContent(message, { fsImpl = fs, maxInlineImageBytes = MAX_INLINE_IMAGE_BYTES } = {}) {
124
+ export function buildClaudeUserContent(message, { fsImpl = fs, maxInlineImageBytes = MAX_INLINE_IMAGE_BYTES, embedImages = true } = {}) {
122
125
  const text = String(message?.text || '').trim();
123
- const { imageBlocks, noteLines } = buildClaudeAttachmentContent(message?.attachments, { fsImpl, maxInlineImageBytes });
126
+ const { imageBlocks, noteLines } = buildClaudeAttachmentContent(message?.attachments, { fsImpl, maxInlineImageBytes, embedImages });
124
127
  const attachmentPromptContext = String(message?.attachmentPromptContext || '').trim();
125
128
  const textParts = [text];
126
129
  if (noteLines.length) textParts.push(noteLines.join('\n'));