@oar-sh/oar 0.9.17 → 0.9.18

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 (101) hide show
  1. package/.github/extensions/web-relay/skills/tool-activity.mjs +5 -4
  2. package/CHANGELOG.md +192 -0
  3. package/README.md +13 -12
  4. package/package.json +1 -1
  5. package/server/claude-api-worker/claude-api-backend.mjs +345 -0
  6. package/server/claude-api-worker/claude-api-session-worker.mjs +29 -0
  7. package/server/claude-worker/claude-cli-pinned-env.mjs +150 -0
  8. package/server/claude-worker/claude-remote-relay-tool.mjs +3 -3
  9. package/server/claude-worker/claude-sdk-adapter.mjs +35 -9
  10. package/server/claude-worker/claude-session-process.mjs +166 -21
  11. package/server/claude-worker/claude-session-worker-main.mjs +3 -0
  12. package/server/claude-worker/claude-subscription-key-check.mjs +64 -0
  13. package/server/claude-worker/claude-turn-publisher.mjs +117 -5
  14. package/server/claude-worker/claude-usage-totals.mjs +53 -0
  15. package/server/claude-worker/sdk-message-normalizer.mjs +1 -1
  16. package/server/copilot-worker/copilot-remote-relay-tool.mjs +2 -2
  17. package/server/copilot-worker/copilot-sdk-event-normalizer.mjs +1 -1
  18. package/server/copilot-worker/copilot-sdk-session-process.mjs +9 -9
  19. package/server/cursor-worker/cursor-remote-relay-tool.mjs +3 -3
  20. package/server/cursor-worker/cursor-turn-runner.mjs +4 -4
  21. package/server/cursor-worker/sdk-message-normalizer.mjs +2 -2
  22. package/server/db-schema.mjs +33 -1
  23. package/server/deepseek-worker/deepseek-claude-backend.mjs +14 -130
  24. package/server/grok-worker/acp-client.mjs +1 -1
  25. package/server/grok-worker/grok-turn-runner.mjs +4 -4
  26. package/server/grok-worker/sdk-message-normalizer.mjs +1 -1
  27. package/server/mcp/oar-mcp-server.mjs +11 -5
  28. package/server/migrations/0031-queue-provider.mjs +143 -0
  29. package/server/migrations/0032-runtime-session-billing-switch.mjs +94 -0
  30. package/server/migrations/0033-claude-api-usage-checkpoints.mjs +138 -0
  31. package/server/migrations/0034-claude-api-usage-totals.mjs +112 -0
  32. package/server/public/app/activity-replay-state.mjs +4 -3
  33. package/server/public/app/agent-sessions-settings-ui.js +1 -1
  34. package/server/public/app/agent-sessions-settings.mjs +1 -1
  35. package/server/public/app/api-client.js +52 -0
  36. package/server/public/app/bootstrap.js +153 -27
  37. package/server/public/app/claude-api-settings-ui.js +362 -0
  38. package/server/public/app/claude-billing-toggle-ui.js +224 -0
  39. package/server/public/app/claude-billing-toggle.mjs +112 -0
  40. package/server/public/app/context-tier-options.mjs +6 -2
  41. package/server/public/app/context-usage-view.mjs +1 -1
  42. package/server/public/app/conversation-provider-indicator.mjs +11 -0
  43. package/server/public/app/conversation-view.js +29 -3
  44. package/server/public/app/inbox-view.mjs +1 -1
  45. package/server/public/app/journal-view.js +127 -10
  46. package/server/public/app/model-selector-options.mjs +17 -1
  47. package/server/public/app/new-conversation-model-choice.mjs +21 -0
  48. package/server/public/app/provider-readiness.mjs +5 -2
  49. package/server/public/app/relay-error-ctas.mjs +50 -0
  50. package/server/public/app/relay-schedules-ui.js +1 -1
  51. package/server/public/app/settings-modal.js +11 -0
  52. package/server/public/app/settings-tabs.js +1 -1
  53. package/server/public/app/socket-handlers.js +18 -1
  54. package/server/public/app/tool-activity-emoji.mjs +4 -3
  55. package/server/public/app/usage-limit-ui.js +136 -5
  56. package/server/public/index.html +189 -3
  57. package/server/relay-tools.md +4 -4
  58. package/server/repositories/conversation-group-repository.mjs +1 -1
  59. package/server/repositories/message-repository.mjs +7 -0
  60. package/server/repositories/session-repository.mjs +8 -1
  61. package/server/routes/ask-user-routes.mjs +1 -1
  62. package/server/routes/conversation-group-routes.mjs +1 -1
  63. package/server/routes/messages-routes.mjs +645 -27
  64. package/server/routes/relay-ask-routes.mjs +3 -3
  65. package/server/routes/relay-event-routes.mjs +2 -2
  66. package/server/routes/relay-schedule-routes.mjs +1 -1
  67. package/server/routes/remote-relay-routes.mjs +3 -3
  68. package/server/routes/sessions-routes.mjs +327 -29
  69. package/server/server-runtime.mjs +363 -26
  70. package/server/services/about-service.mjs +1 -0
  71. package/server/services/background-work-view.mjs +2 -1
  72. package/server/services/claude-api-settings-service.mjs +575 -0
  73. package/server/services/claude-api-usage-service.mjs +361 -0
  74. package/server/services/claude-billing-swap-service.mjs +274 -0
  75. package/server/services/claude-billing-switch-service.mjs +343 -0
  76. package/server/services/plan-usage-claude-api.mjs +445 -0
  77. package/server/services/plan-usage-service.mjs +5 -0
  78. package/server/services/provider-model-selection.mjs +3 -2
  79. package/server/services/provider-reasoning-effort.mjs +9 -0
  80. package/server/services/relay-ask-service.mjs +1 -1
  81. package/server/services/relay-event-service.mjs +3 -2
  82. package/server/services/relay-schedule-service.mjs +1 -1
  83. package/server/services/remote-relay-caller-context.mjs +7 -0
  84. package/server/services/remote-relay-dispatcher.mjs +83 -14
  85. package/server/services/remote-relay-registry-service.mjs +1 -1
  86. package/server/services/session-worker-launch-service.mjs +61 -2
  87. package/server/services/session-worker-registry-service.mjs +28 -1
  88. package/server/services/session-worker-websocket-service.mjs +17 -0
  89. package/server/services/usage-limit-pause-service.mjs +55 -4
  90. package/server/services/worker-secret-redaction.mjs +2 -0
  91. package/shared/anthropic-api.mjs +202 -0
  92. package/shared/provider-routing.mjs +5 -3
  93. package/shared/relay-asks.mjs +11 -5
  94. package/shared/relay-events.mjs +2 -2
  95. package/shared/relay-schedules.mjs +2 -2
  96. package/shared/remote-relay-contract.mjs +29 -9
  97. package/shared/remote-relay-tool-core.mjs +23 -6
  98. package/shared/steer-settle-failure.mjs +1 -0
  99. package/shared/worker-runtime/api-client.mjs +2 -2
  100. package/shared/worker-runtime/heartbeat.mjs +5 -0
  101. package/shared/worker-runtime/worker-websocket-link.mjs +6 -0
@@ -106,3 +106,56 @@ export function claudeToolRequests(serverToolUse, { modelKey = (value) => String
106
106
  }
107
107
  return [...requests.values()].filter((entry) => entry.searchRequests + entry.fetchRequests > 0);
108
108
  }
109
+
110
+ function costOf(value) {
111
+ const number = Number(value ?? 0);
112
+ return Number.isFinite(number) && number > 0 ? number : 0;
113
+ }
114
+
115
+ /**
116
+ * The CLI's totals per model with Claude Code's own cost estimate, for the
117
+ * Claude API booking (claude-api-usage-service.mjs): `[{ model, hitTokens,
118
+ * missTokens, outputTokens, costUsd, costUnknown }]`. From `modelUsage` (the
119
+ * session's running totals, sub-agents and helper calls included; `costUSD`
120
+ * per model, `costBasis: 'unknown'` for a model the CLI has no price for), or
121
+ * from `usage` alone under the turn's model (pass `modelUsage` as null): the
122
+ * turn's main agent loop, which carries no cost. Model ids keep Claude's
123
+ * `[1m]` tier; the relay books them under the base id.
124
+ */
125
+ export function claudeBillingTotals(modelUsage, { usage = null, model = '' } = {}) {
126
+ const totals = new Map();
127
+ const add = (name, hit, miss, output, costUsd, costUnknown) => {
128
+ const key = String(name ?? '').trim().toLowerCase();
129
+ if (!key) return;
130
+ const current = totals.get(key) || { model: key, hitTokens: 0, missTokens: 0, outputTokens: 0, costUsd: 0, costUnknown: false };
131
+ current.hitTokens += hit;
132
+ current.missTokens += miss;
133
+ current.outputTokens += output;
134
+ current.costUsd += costUsd;
135
+ current.costUnknown = current.costUnknown || costUnknown;
136
+ totals.set(key, current);
137
+ };
138
+ if (modelUsage && typeof modelUsage === 'object' && !Array.isArray(modelUsage)) {
139
+ for (const [name, entry] of Object.entries(modelUsage)) {
140
+ if (!entry || typeof entry !== 'object') continue;
141
+ add(
142
+ name,
143
+ count(entry.cacheReadInputTokens),
144
+ count(entry.inputTokens) + count(entry.cacheCreationInputTokens),
145
+ count(entry.outputTokens),
146
+ costOf(entry.costUSD),
147
+ entry.costBasis === 'unknown',
148
+ );
149
+ }
150
+ } else if (usage && typeof usage === 'object') {
151
+ add(
152
+ model,
153
+ count(usage.cache_read_input_tokens),
154
+ count(usage.input_tokens) + count(usage.cache_creation_input_tokens),
155
+ count(usage.output_tokens),
156
+ 0,
157
+ false,
158
+ );
159
+ }
160
+ return [...totals.values()].filter((entry) => entry.hitTokens + entry.missTokens + entry.outputTokens > 0 || entry.costUsd > 0);
161
+ }
@@ -135,7 +135,7 @@ export function isBackgroundLaunch(block, toolUseResult = null) {
135
135
 
136
136
  export function summarizeToolInput(toolName, input) {
137
137
  if (!input || typeof input !== 'object') return '';
138
- // `mcp__relay__remote_relay` reads as what it does on which relay
138
+ // `mcp__relay__relay_tool` reads as what it does on which relay
139
139
  // (`send → linux-test session 01234567: “…”`), not as its first argument.
140
140
  if (isRemoteRelayToolName(toolName)) {
141
141
  const summary = remoteRelayActivitySummary(input);
@@ -1,4 +1,4 @@
1
- // The relay's `remote_relay` tool for Copilot SDK sessions: a plain SDK
1
+ // The relay's `relay_tool` tool for Copilot SDK sessions: a plain SDK
2
2
  // `Tool` (`{ name, description, parameters, skipPermission, handler }`) built
3
3
  // from the shared contract and forwarded through the shared tool core. The
4
4
  // module never imports the SDK; the session process hands the definition to
@@ -50,7 +50,7 @@ export function buildCopilotRemoteRelayTool({
50
50
  }));
51
51
  } catch (error) {
52
52
  const message = error?.message || String(error);
53
- dbg('remote_relay tool failed', message);
53
+ dbg('relay_tool failed', message);
54
54
  result = { ok: false, code: REMOTE_RELAY_CALL_FAILED, error: message };
55
55
  }
56
56
  return formatRemoteRelayToolResult(result);
@@ -84,7 +84,7 @@ export function summarizeToolInput(input) {
84
84
 
85
85
  export function formatToolActivityText(toolName, input) {
86
86
  const rawName = String(toolName || '').trim() || 'tool';
87
- // `remote_relay` (bare, or namespaced by an MCP server) reads as what it
87
+ // `relay_tool` (bare, or namespaced by an MCP server) reads as what it
88
88
  // does on which relay (`send → linux-test session 01234567: “…”`); its
89
89
  // arguments may arrive as a JSON string.
90
90
  const isRemoteRelay = isRemoteRelayToolName(rawName);
@@ -456,7 +456,7 @@ export function createCopilotSdkSessionRunner({
456
456
  // runtime's built-in one. A runtime that refuses the override falls back
457
457
  // to the built-in for the rest of this worker's life.
458
458
  relayAskUserTool = true,
459
- // Whether sessions carry the relay's `remote_relay` tool
459
+ // Whether sessions carry the relay's `relay_tool` tool
460
460
  // (shared/remote-relay-tool-core.mjs). Default: ask the relay.
461
461
  remoteRelayToolGate = null,
462
462
  taskRefreshMs = DEFAULT_TASK_REFRESH_MS,
@@ -696,12 +696,12 @@ export function createCopilotSdkSessionRunner({
696
696
  // the human's eventual answer to a runtime whose row is already settled.
697
697
  // Same guard the Cursor worker's `hasPendingClientWork` provides.
698
698
  let pendingHumanRequests = 0;
699
- // Running `remote_relay` calls. The runtime is just as silent while one
699
+ // Running `relay_tool` calls. The runtime is just as silent while one
700
700
  // runs (the relay may wait up to 600 s for a remote turn, plus an approval
701
701
  // card), so they hold the stall watchdog and the idle shutdown too — but
702
702
  // not steering: nobody is answering a card here.
703
703
  let pendingRelayToolCalls = 0;
704
- // `remote_relay` is registered only while this relay has paired remotes.
704
+ // `relay_tool` is registered only while this relay has paired remotes.
705
705
  // Asked once when the worker starts; a session build waits for that first
706
706
  // answer (it is async anyway), and every runtime stop asks again so the
707
707
  // next session picks up a remote paired in the meantime.
@@ -2306,7 +2306,7 @@ export function createCopilotSdkSessionRunner({
2306
2306
  }
2307
2307
 
2308
2308
  /**
2309
- * The relay's `remote_relay` tool (copilot-remote-relay-tool.mjs). Every call
2309
+ * The relay's `relay_tool` tool (copilot-remote-relay-tool.mjs). Every call
2310
2310
  * is counted while it runs: the stall watchdog re-arms instead of failing the
2311
2311
  * row, and the lifecycle keeps the runtime up. The clock restarts from the
2312
2312
  * answer, like after a card. A call is cancelled by the runtime (its
@@ -2349,7 +2349,7 @@ export function createCopilotSdkSessionRunner({
2349
2349
  * runtime refuses the override (an older runtime). The refusal is
2350
2350
  * remembered until the runtime is stopped — the next runtime (possibly
2351
2351
  * auto-updated under the relay) gets another try. Only the override goes:
2352
- * the relay's other tools (`remote_relay`) override nothing and stay.
2352
+ * the relay's other tools (`relay_tool`) override nothing and stay.
2353
2353
  */
2354
2354
  async function openSession(open, config) {
2355
2355
  try {
@@ -3198,7 +3198,7 @@ export function createCopilotSdkSessionRunner({
3198
3198
  // Cancels any relay question card this turn is blocked on, so an aborted
3199
3199
  // turn does not leave a human answering into the void.
3200
3200
  abortController: new AbortController(),
3201
- // Cancels the `remote_relay` calls that started under this turn — on the
3201
+ // Cancels the `relay_tool` calls that started under this turn — on the
3202
3202
  // user's Stop only. Not `abortController`: that one also fires when the
3203
3203
  // turn settles normally, and a background agent's call must outlive that.
3204
3204
  remoteRelayStop: new AbortController(),
@@ -3369,7 +3369,7 @@ export function createCopilotSdkSessionRunner({
3369
3369
  if (turn.settled) return;
3370
3370
  // A human staring at a question card is not a stalled runtime: the card
3371
3371
  // has its own (much longer) timeout, and failing the row here would
3372
- // settle it while the answer is still coming. A remote_relay call waiting
3372
+ // settle it while the answer is still coming. A relay_tool call waiting
3373
3373
  // on another relay is the same kind of silence; the relay bounds that
3374
3374
  // wait itself. So is a compaction, bounded by its stale cap, and the wait
3375
3375
  // for the previous runtime to stop, bounded by the teardown's own limits.
@@ -3912,7 +3912,7 @@ export function createCopilotSdkSessionRunner({
3912
3912
  }
3913
3913
  turn.aborted = true;
3914
3914
  turn.abortedRowId = rowId;
3915
- // A pending `remote_relay` call ends now rather than when the runtime gets
3915
+ // A pending `relay_tool` call ends now rather than when the runtime gets
3916
3916
  // round to it: the relay drops the request, withdraws its approval card
3917
3917
  // and stops waiting on the remote, and the watchdog hold is released.
3918
3918
  turn.remoteRelayStop?.abort();
@@ -4564,7 +4564,7 @@ export function createCopilotSdkSessionRunner({
4564
4564
  * is not waiting for its tools, so a steer interrupts nothing. A
4565
4565
  * subagent the main agent runs in the foreground is covered by the main
4566
4566
  * agent's own tool call that started it.
4567
- * - `remote_relay`, which holds the stall watchdog but never held steering
4567
+ * - `relay_tool`, which holds the stall watchdog but never held steering
4568
4568
  * (see `pendingRelayToolCalls`).
4569
4569
  */
4570
4570
  function mainToolsInFlight(turn = activeTurn) {
@@ -1,7 +1,7 @@
1
1
  /**
2
- * The relay's `remote_relay` custom tool for the Cursor worker. SDK-free: the
2
+ * The relay's `relay_tool` custom tool for the Cursor worker. SDK-free: the
3
3
  * turn runner passes the returned `{ description, inputSchema, execute }` into
4
- * `customTools.remote_relay`, and every call goes through the shared tool core
4
+ * `customTools.relay_tool`, and every call goes through the shared tool core
5
5
  * to the local relay endpoint.
6
6
  *
7
7
  * A call can legitimately run for many minutes (a remote turn plus an approval
@@ -46,7 +46,7 @@ export function createRemoteRelayTool({
46
46
  } catch (error) {
47
47
  // A tool exception must never kill the run.
48
48
  const message = error?.message || String(error);
49
- dbg('remote_relay tool failed', message);
49
+ dbg('relay_tool failed', message);
50
50
  const result = { ok: false, code: REMOTE_RELAY_CALL_FAILED, error: message };
51
51
  return {
52
52
  structuredContent: result,
@@ -126,7 +126,7 @@ export function createCursorTurnRunner({
126
126
  // The error an agent started in `cwd` fails with up front, or null
127
127
  // (shared/worker-runtime/working-directory.mjs).
128
128
  checkWorkingDirectoryImpl = missingWorkingDirectoryError,
129
- // Whether agent handles carry the relay's `remote_relay` tool
129
+ // Whether agent handles carry the relay's `relay_tool` tool
130
130
  // (shared/remote-relay-tool-core.mjs). Default: ask the relay.
131
131
  remoteRelayToolGate = null,
132
132
  dbg = () => {},
@@ -149,7 +149,7 @@ export function createCursorTurnRunner({
149
149
  // Open ask_user round-trips: the merged-stream stall watchdog defers while
150
150
  // a human is thinking about a question card.
151
151
  let pendingAskUserCalls = 0;
152
- // Running remote_relay calls, same family: the relay may be waiting up to
152
+ // Running relay_tool calls, same family: the relay may be waiting up to
153
153
  // 600 s for a remote turn (plus an approval card) with no SDK traffic.
154
154
  let pendingRemoteRelayCalls = 0;
155
155
  // Mirrors the Copilot extension's lastPromptedRelayMode: full mode
@@ -178,7 +178,7 @@ export function createCursorTurnRunner({
178
178
  // plain refusal the model can relay instead of a failed turn.
179
179
  const previewTool = createPreviewTool({
180
180
  api,
181
- // Between turns, the worker's own session (as remote_relay below): list
181
+ // Between turns, the worker's own session (as relay_tool below): list
182
182
  // and close are scoped to a conversation and refused without one.
183
183
  getConversationId: () => activeMessage?.conversationId || sdkSessionId || '',
184
184
  dbg,
@@ -202,7 +202,7 @@ export function createCursorTurnRunner({
202
202
  execute: previewTool.execute,
203
203
  },
204
204
  };
205
- // `remote_relay` joins the set only while this relay has paired remotes.
205
+ // `relay_tool` joins the set only while this relay has paired remotes.
206
206
  // Asked once when the worker starts; the first handle waits for the answer
207
207
  // (it is created asynchronously anyway), and every handle creation refreshes
208
208
  // it so a later rebuild picks up a remote paired in the meantime.
@@ -47,8 +47,8 @@ export function summarizeToolInput(toolName, input) {
47
47
  /**
48
48
  * The relay's custom tools reach the stream as the SDK's generic MCP call
49
49
  * (`name: 'mcp'`, args `{ providerIdentifier: 'custom-user-tools', toolName,
50
- * args }`). `remote_relay` is shown as itself with the contract's one-line
51
- * summary, so the transcript reads `Tool (remote_relay): send → …` like every
50
+ * args }`). `relay_tool` is shown as itself with the contract's one-line
51
+ * summary, so the transcript reads `Tool (relay_tool): send → …` like every
52
52
  * other provider's; null for any other tool.
53
53
  */
54
54
  function remoteRelayCallArgs(toolName, input) {
@@ -36,6 +36,10 @@ import { migrateDeepSeekUsageCheckpointRuns } from './migrations/0027-deepseek-u
36
36
  import { migrateDeepSeekUsageCheckpointRequestRuns } from './migrations/0028-deepseek-usage-checkpoint-request-runs.mjs';
37
37
  import { migrateDeepSeekBalanceRises } from './migrations/0029-deepseek-balance-rises.mjs';
38
38
  import { migrateDeepSeekUsageActuals } from './migrations/0030-deepseek-usage-actuals.mjs';
39
+ import { migrateQueueProvider } from './migrations/0031-queue-provider.mjs';
40
+ import { migrateRuntimeSessionBillingSwitch } from './migrations/0032-runtime-session-billing-switch.mjs';
41
+ import { migrateClaudeApiUsageCheckpoints } from './migrations/0033-claude-api-usage-checkpoints.mjs';
42
+ import { migrateClaudeApiUsageTotals } from './migrations/0034-claude-api-usage-totals.mjs';
39
43
  import { migrateImageConversationSchema } from './repositories/image-conversation-repository.mjs';
40
44
 
41
45
  // Mirrors DEFAULT_RELAY_MODE in server-runtime.mjs; used only by the one-time
@@ -905,7 +909,7 @@ try {
905
909
  console.warn(`[relay-schedules] migration 0022 failed; retrying on next boot: ${error?.message || error}`);
906
910
  }
907
911
 
908
- // Questions that don't block (remote_relay ask): one new table. The ask
912
+ // Questions that don't block (relay_tool ask): one new table. The ask
909
913
  // service checks for it and refuses the ask actions without it, so a failure
910
914
  // is logged and retried next boot like 0022.
911
915
  try {
@@ -1027,6 +1031,34 @@ if (runtimeSessionColumns.length) {
1027
1031
  }
1028
1032
  }
1029
1033
 
1034
+ // The Claude billing switch: the billing a queue row runs on, and the chat's
1035
+ // last switch. After runtime_sessions has provider_type: 0031's back-fill
1036
+ // reads it. Every reader and writer of these columns checks for them, so a
1037
+ // failure is logged and retried next boot like 0004.
1038
+ try {
1039
+ migrateQueueProvider(db);
1040
+ } catch (error) {
1041
+ console.warn(`[claude-billing] migration 0031 failed; retrying on next boot: ${error?.message || error}`);
1042
+ }
1043
+ try {
1044
+ migrateRuntimeSessionBillingSwitch(db);
1045
+ } catch (error) {
1046
+ console.warn(`[claude-billing] migration 0032 failed; retrying on next boot: ${error?.message || error}`);
1047
+ }
1048
+
1049
+ // Claude API usage: the checkpoints across both Claude billings and the
1050
+ // booked totals per day and model, handled like 0024.
1051
+ try {
1052
+ migrateClaudeApiUsageCheckpoints(db);
1053
+ } catch (error) {
1054
+ console.warn(`[claude-api-usage] migration 0033 failed; retrying on next boot: ${error?.message || error}`);
1055
+ }
1056
+ try {
1057
+ migrateClaudeApiUsageTotals(db);
1058
+ } catch (error) {
1059
+ console.warn(`[claude-api-usage] migration 0034 failed; retrying on next boot: ${error?.message || error}`);
1060
+ }
1061
+
1030
1062
  const conversationColumns = db.prepare(`PRAGMA table_info(conversations)`).all().map((c) => c.name);
1031
1063
  if (!conversationColumns.includes('archived')) {
1032
1064
  db.exec(`ALTER TABLE conversations ADD COLUMN archived INTEGER NOT NULL DEFAULT 0`);
@@ -7,9 +7,11 @@
7
7
  // directory of its own, no nonessential traffic, and nothing of the host's
8
8
  // Claude login or of another Anthropic configuration. The endpoint, the
9
9
  // key and everything else that routes the CLI's requests or decides whom
10
- // it trusts (DEEPSEEK_CLI_PINNED_ENV: provider switches, credentials,
10
+ // it trusts (CLAUDE_CLI_PINNED_ENV: provider switches, credentials,
11
11
  // proxies, TLS) are pinned again in the CLI's flag settings
12
12
  // (liveFlagSettings), over the settings of the chat's folder.
13
+ // - Hooks: none run, neither the user's nor the chat folder's, and no
14
+ // statusLine command (CLAUDE_CLI_KEY_CHAT_SETTINGS, on the command line).
13
15
  // - The model: DeepSeek's ids, with Claude Code's `[1m]` suffix so the CLI
14
16
  // plans with the 1M window; helper calls (titles, summaries) go to flash.
15
17
  // Claude Code's model aliases are pinned to the session's model
@@ -34,6 +36,7 @@
34
36
 
35
37
  import { deepseekAnthropicBaseUrl } from '../../shared/deepseek-api.mjs';
36
38
  import { isHostClaudeSessionVariable } from '../../shared/host-claude-session-env.mjs';
39
+ import { CLAUDE_CLI_KEY_CHAT_SETTINGS, claudeCliPinnedEnv } from '../claude-worker/claude-cli-pinned-env.mjs';
37
40
  import {
38
41
  DEEPSEEK_PROVIDER_TYPE,
39
42
  DEFAULT_DEEPSEEK_MODEL,
@@ -120,133 +123,6 @@ const FOREIGN_CLI_VARIABLES = Object.freeze([
120
123
  // ANTHROPIC_AUTH_TOKEN) and the relay's own OAR_* switches.
121
124
  const FOREIGN_CLI_PREFIXES = Object.freeze(['ANTHROPIC_', 'COPILOT_WEB_RELAY_', 'COPILOT_PROVIDER_', 'DEEPSEEK_', 'OAR_']);
122
125
 
123
- /**
124
- * Every variable through which the env of the chat folder's settings
125
- * (.claude/settings.json, settings.local.json) could send the CLI's requests,
126
- * and with them the key, somewhere else, or change whom the CLI trusts on the
127
- * way: the endpoints, the provider switches, the credentials, the proxies and
128
- * the TLS trust. liveFlagSettings pins each one in the flag settings layer,
129
- * which the CLI applies over the folder's, to the value the CLI was spawned
130
- * with: the worker's own, else the other case's for a proxy variable, else ''.
131
- * '' is unset to the CLI for each of these (truthy or trimmed reads), except
132
- * SSL_CERT_FILE and SSL_CERT_DIR: '' empties OpenSSL's and Node's system
133
- * store, so those two are pinned only when the worker sets them.
134
- *
135
- * Source: the names Claude Code 2.1.304 (the bundled CLI) reads, by a grep
136
- * of the binary; the CLI's own lists of endpoints (with their selection
137
- * switches and companions) and credentials are in it as they are there. A
138
- * deny-list, because a settings layer can only pin names known in advance.
139
- * Left out, each for its reason: the model variables (same endpoint, same
140
- * key), what the CLI already drops from a folder's settings or from all
141
- * settings (CLAUDE_CONFIG_DIR, CLAUDE_CODE_CUSTOM_OAUTH_URL, the OTEL
142
- * exporter endpoints, ANTHROPIC_UNIX_SOCKET, the host-managed provider
143
- * switches, the file-descriptor credentials), a provider's own SDK settings
144
- * (AWS_*, GOOGLE_*, AZURE_*, read only for the provider the switches here
145
- * turn off), NODE_OPTIONS (at runtime the CLI reads only its system-CA
146
- * flags, which add no CA), and what routes only other programs' traffic
147
- * (NPM_CONFIG_*, GIT_*, the resolver's variables).
148
- */
149
- export const DEEPSEEK_CLI_PINNED_ENV = Object.freeze([
150
- // Endpoints: the CLI's list, its fallback and the headers sent with them.
151
- 'ANTHROPIC_BASE_URL',
152
- '_CLAUDE_CODE_ASSUME_FIRST_PARTY_BASE_URL',
153
- 'ANTHROPIC_CUSTOM_HEADERS',
154
- 'ANTHROPIC_BEDROCK_BASE_URL',
155
- 'ANTHROPIC_VERTEX_BASE_URL',
156
- 'ANTHROPIC_FOUNDRY_BASE_URL',
157
- 'ANTHROPIC_AWS_BASE_URL',
158
- 'ANTHROPIC_GOOGLE_CLOUD_BASE_URL',
159
- 'ANTHROPIC_BEDROCK_MANTLE_BASE_URL',
160
- 'CLAUDE_CODE_API_BASE_URL',
161
- 'CLAUDE_CODE_ARTIFACTS_API_BASE_URL',
162
- 'CLAUDE_CODE_ARTIFACTS_API_TOKEN',
163
- 'CLAUDE_CODE_ARTIFACT_ASSET_BASE_URL',
164
- 'CLAUDE_CODE_ARTIFACT_LIVE_BASE_URL',
165
- 'CLAUDE_CODE_ARTIFACT_SYNC_BASE_URL',
166
- 'CLAUDE_CODE_ARTIFACT_VIEWER_BASE_URL',
167
- 'CLAUDE_CODE_MEMORY_API_BASE_URL',
168
- 'CLAUDE_CODE_MEMORY_API_TOKEN',
169
- // Provider switches.
170
- 'CLAUDE_CODE_USE_BEDROCK',
171
- 'CLAUDE_CODE_USE_VERTEX',
172
- 'CLAUDE_CODE_USE_FOUNDRY',
173
- 'CLAUDE_CODE_USE_ANTHROPIC_AWS',
174
- 'CLAUDE_CODE_USE_ANTHROPIC_GOOGLE_CLOUD',
175
- 'CLAUDE_CODE_USE_MANTLE',
176
- 'CLAUDE_CODE_USE_GATEWAY',
177
- 'CLAUDE_CODE_SKIP_BEDROCK_AUTH',
178
- 'CLAUDE_CODE_SKIP_VERTEX_AUTH',
179
- 'CLAUDE_CODE_SKIP_FOUNDRY_AUTH',
180
- 'CLAUDE_CODE_SKIP_ANTHROPIC_AWS_AUTH',
181
- 'CLAUDE_CODE_SKIP_ANTHROPIC_GOOGLE_CLOUD_AUTH',
182
- 'CLAUDE_CODE_SKIP_MANTLE_AUTH',
183
- // Credentials: the CLI's list, the OAuth login, the federation profile and
184
- // the proxy credential helper.
185
- 'ANTHROPIC_API_KEY',
186
- 'ANTHROPIC_AUTH_TOKEN',
187
- 'CLAUDE_CODE_OAUTH_TOKEN',
188
- 'AWS_BEARER_TOKEN_BEDROCK',
189
- 'ANTHROPIC_FOUNDRY_API_KEY',
190
- 'ANTHROPIC_FOUNDRY_AUTH_TOKEN',
191
- 'ANTHROPIC_AWS_API_KEY',
192
- 'CLAUDE_CODE_OAUTH_REFRESH_TOKEN',
193
- 'CLAUDE_CODE_OAUTH_SCOPES',
194
- 'CLAUDE_CODE_OAUTH_CLIENT_ID',
195
- 'ANTHROPIC_PROFILE',
196
- 'ANTHROPIC_IDENTITY_TOKEN',
197
- 'ANTHROPIC_IDENTITY_TOKEN_FILE',
198
- 'ANTHROPIC_FEDERATION_RULE_ID',
199
- 'ANTHROPIC_ORGANIZATION_ID',
200
- 'ANTHROPIC_SERVICE_ACCOUNT_ID',
201
- 'ANTHROPIC_SCOPE',
202
- 'ANTHROPIC_WORKSPACE_ID',
203
- 'CLAUDE_CODE_ENABLE_PROXY_AUTH_HELPER',
204
- // Proxies: the CLI reads both cases, the lower one first.
205
- 'HTTPS_PROXY',
206
- 'https_proxy',
207
- 'HTTP_PROXY',
208
- 'http_proxy',
209
- 'ALL_PROXY',
210
- 'all_proxy',
211
- 'NO_PROXY',
212
- 'no_proxy',
213
- 'CLAUDE_CODE_HTTPS_PROXY',
214
- 'CLAUDE_CODE_HTTP_PROXY',
215
- // TLS trust and the client certificate.
216
- 'NODE_TLS_REJECT_UNAUTHORIZED',
217
- 'NODE_EXTRA_CA_CERTS',
218
- 'CLAUDE_CODE_CERT_STORE',
219
- 'CLAUDE_CODE_CLIENT_CERT',
220
- 'CLAUDE_CODE_CLIENT_KEY',
221
- 'CLAUDE_CODE_CLIENT_KEY_PASSPHRASE',
222
- 'SSL_CERT_FILE',
223
- 'SSL_CERT_DIR',
224
- ]);
225
- // The two of DEEPSEEK_CLI_PINNED_ENV for which '' is not unset (see there).
226
- const PINNED_ONLY_WHEN_SET = new Set(['SSL_CERT_FILE', 'SSL_CERT_DIR']);
227
- // The proxy variables the CLI and the tools it runs read in either case.
228
- const OTHER_CASE = new Map(
229
- ['HTTPS_PROXY', 'HTTP_PROXY', 'ALL_PROXY', 'NO_PROXY'].flatMap((name) => [
230
- [name, name.toLowerCase()],
231
- [name.toLowerCase(), name],
232
- ]),
233
- );
234
-
235
- /**
236
- * The pins of DEEPSEEK_CLI_PINNED_ENV for a CLI spawned with `cliEnv`: the
237
- * value it was spawned with, else the other case's (a proxy the worker sets
238
- * as https_proxy holds for HTTPS_PROXY as well), else ''.
239
- */
240
- export function deepseekPinnedEnv(cliEnv = {}) {
241
- const pins = {};
242
- for (const name of DEEPSEEK_CLI_PINNED_ENV) {
243
- const own = cliEnv[name] ?? cliEnv[OTHER_CASE.get(name)];
244
- if (own !== undefined) pins[name] = String(own);
245
- else if (!PINNED_ONLY_WHEN_SET.has(name)) pins[name] = '';
246
- }
247
- return pins;
248
- }
249
-
250
126
  /**
251
127
  * Whether a variable of the worker's stays out of the CLI's environment:
252
128
  * another Anthropic configuration or credential, the relay's settings for
@@ -410,13 +286,21 @@ export function createDeepSeekClaudeBackend({ env = process.env } = {}) {
410
286
 
411
287
  spawnEnv,
412
288
 
289
+ /**
290
+ * The settings layer every CLI of this backend is spawned with, on its
291
+ * command line (no key in it): no hooks and no statusLine command, from
292
+ * the user's settings or the chat folder's. They would run with the key
293
+ * in their environment, a folder's SessionStart hook before the pins.
294
+ */
295
+ flagSettings: () => ({ ...CLAUDE_CLI_KEY_CHAT_SETTINGS }),
296
+
413
297
  /**
414
298
  * The CLI's routing, trust and credentials once more, in its flag
415
299
  * settings layer, which outranks the settings of the chat's folder: a
416
300
  * .claude/settings.json or settings.local.json there whose
417
301
  * env.ANTHROPIC_BASE_URL, proxy or CA points elsewhere would otherwise
418
302
  * get the DeepSeek key as its bearer token (scripts/deepseek-cli-proof.mjs).
419
- * Every variable of DEEPSEEK_CLI_PINNED_ENV holds the value of `cliEnv`,
303
+ * Every variable of CLAUDE_CLI_PINNED_ENV holds the value of `cliEnv`,
420
304
  * the environment the CLI was spawned with (spawnEnv), or is blanked, and
421
305
  * so is an apiKeyHelper of the folder's. The key may not go on the
422
306
  * command line, so the adapter sends these over the control channel
@@ -425,7 +309,7 @@ export function createDeepSeekClaudeBackend({ env = process.env } = {}) {
425
309
  liveFlagSettings(cliEnv = spawnEnv(env)) {
426
310
  if (!apiKey) throw new Error('deepseek-api-key-not-configured');
427
311
  return {
428
- env: { ...deepseekPinnedEnv(cliEnv), ANTHROPIC_BASE_URL: anthropicBaseUrl, ANTHROPIC_AUTH_TOKEN: apiKey },
312
+ env: { ...claudeCliPinnedEnv(cliEnv), ANTHROPIC_BASE_URL: anthropicBaseUrl, ANTHROPIC_AUTH_TOKEN: apiKey },
429
313
  apiKeyHelper: '',
430
314
  };
431
315
  },
@@ -358,7 +358,7 @@ export class AcpClient extends EventEmitter {
358
358
  * `holdStall` (async, optional) is asked once the inactivity window has run
359
359
  * out: `true` re-arms the full window instead of failing the prompt. It is
360
360
  * for work the agent is blocked on that this client cannot see — a
361
- * remote_relay call the agent runs through its MCP server, which the relay
361
+ * relay_tool call the agent runs through its MCP server, which the relay
362
362
  * reports as in flight. A question that fails or takes longer than
363
363
  * `holdCheckTimeoutMs` counts as `false`.
364
364
  *
@@ -134,7 +134,7 @@ export function createGrokTurnRunner({
134
134
  } = {}) {
135
135
  const terminalTasks = taskPublisher;
136
136
  // The OAR MCP server gives Grok the relay's real tools — `preview`, and
137
- // `remote_relay` when remotes are paired — bound to this conversation.
137
+ // `relay_tool` when remotes are paired — bound to this conversation.
138
138
  // `OAR_MCP_SERVER=0` is the operator's way back to the preview instruction
139
139
  // block, should an agent build turn out to ignore ACP `mcpServers`.
140
140
  const oarMcpLaunch = oarMcpServer !== undefined
@@ -238,7 +238,7 @@ export function createGrokTurnRunner({
238
238
  }
239
239
 
240
240
  /**
241
- * The inactivity hold. A remote_relay call runs inside the MCP server, which
241
+ * The inactivity hold. A relay_tool call runs inside the MCP server, which
242
242
  * this worker cannot see: Grok just goes quiet for as long as the relay
243
243
  * waits on the remote turn. Before the ACP watchdog calls that a stall, the
244
244
  * relay is asked whether a call of this conversation is still in flight.
@@ -247,7 +247,7 @@ export function createGrokTurnRunner({
247
247
  */
248
248
  async function remoteRelayCallInFlight() {
249
249
  const inflight = await fetchRemoteRelayInflight({ api, conversationId: sdkSessionId });
250
- if (inflight > 0) dbg('remote_relay call in flight; holding the inactivity watchdog', String(inflight));
250
+ if (inflight > 0) dbg('relay_tool call in flight; holding the inactivity watchdog', String(inflight));
251
251
  return inflight > 0;
252
252
  }
253
253
 
@@ -447,7 +447,7 @@ export function createGrokTurnRunner({
447
447
  // absolute cap: "No limit" (0) must not be silently trimmed to the
448
448
  // 30-minute default. The inactivity watchdog stays — it detects a dead
449
449
  // transport, not a long turn — but asks the relay before it fires while
450
- // Grok may be blocked on a remote_relay call.
450
+ // Grok may be blocked on a relay_tool call.
451
451
  const watchdog = {
452
452
  ...(turnCeilingMs !== null ? { maxTurnMs: turnCeilingMs > 0 ? turnCeilingMs : 0 } : {}),
453
453
  ...(acpMcpServers.length ? { holdStall: remoteRelayCallInFlight } : {}),
@@ -64,7 +64,7 @@ export function summarizeToolInput(input) {
64
64
  }
65
65
 
66
66
  export function formatToolActivityText(toolName, input, title = '') {
67
- // The OAR MCP server's `remote_relay`, however the agent labels it (bare,
67
+ // The OAR MCP server's `relay_tool`, however the agent labels it (bare,
68
68
  // or namespaced by the server), reads as what it does on which relay.
69
69
  if (isRemoteRelayToolName(toolName) || isRemoteRelayToolName(title)) {
70
70
  const remoteSummary = remoteRelayActivitySummary(input);
@@ -2,7 +2,7 @@
2
2
  // OAR's own stdio MCP server. It gives the agents OAR launches without an
3
3
  // in-process tool surface — Grok (ACP `mcpServers`) and the Copilot CLI
4
4
  // extension engine (`--additional-mcp-config`) — the relay's real tools:
5
- // `remote_relay` and `preview`, bound to the one conversation it was started
5
+ // `relay_tool` and `preview`, bound to the one conversation it was started
6
6
  // for, so the relay's mention gate and approval apply exactly as they do for
7
7
  // the Claude, Cursor and Copilot SDK workers.
8
8
  //
@@ -34,6 +34,7 @@ import {
34
34
  REMOTE_RELAY_TOOL_CALL_TIMEOUT_MS,
35
35
  REMOTE_RELAY_TOOL_DESCRIPTION,
36
36
  REMOTE_RELAY_TOOL_NAME,
37
+ canonicalRemoteRelayToolCallName,
37
38
  cloneRemoteRelayToolInputSchema,
38
39
  executeRemoteRelayTool,
39
40
  formatRemoteRelayToolResult,
@@ -216,10 +217,15 @@ export function createOarMcpServer({
216
217
  }
217
218
 
218
219
  async function callTool(id, params) {
219
- const name = toText(params?.name);
220
+ // `remote_relay`, the tool's name up to 0.9.17, still dispatches while
221
+ // the tool is on, but is never listed: a second definition would only
222
+ // cost context. The hosts in use (Grok, the Copilot CLI) call only what
223
+ // tools/list gave them, so for them the old name is gone; a client that
224
+ // kept it still works.
225
+ const name = canonicalRemoteRelayToolCallName(params?.name);
220
226
  const tools = await listTools();
221
227
  if (!tools.some((tool) => tool.name === name)) {
222
- return fail(id, JSON_RPC.invalidParams, `Unknown tool: ${name || '(none)'}`);
228
+ return fail(id, JSON_RPC.invalidParams, `Unknown tool: ${toText(params?.name) || '(none)'}`);
223
229
  }
224
230
  const args = params?.arguments && typeof params.arguments === 'object' && !Array.isArray(params.arguments)
225
231
  ? params.arguments
@@ -348,7 +354,7 @@ export function createOarMcpServer({
348
354
 
349
355
  /**
350
356
  * The stdio process: parse the arguments, reach the relay like a worker does,
351
- * decide whether `remote_relay` is listed (asked once, at startup), then serve
357
+ * decide whether `relay_tool` is listed (asked once, at startup), then serve
352
358
  * until stdin closes. Returns the server, or null after a startup failure.
353
359
  */
354
360
  export async function runOarMcpServer({
@@ -413,7 +419,7 @@ export async function runOarMcpServer({
413
419
  remoteRelay: Promise.resolve()
414
420
  .then(() => shouldRegisterRemoteRelayToolImpl({ api }))
415
421
  .then((enabled) => {
416
- log(`serving conversation ${conversationId.slice(0, 8)}; remote_relay ${enabled ? 'on' : 'off'}`);
422
+ log(`serving conversation ${conversationId.slice(0, 8)}; relay_tool ${enabled ? 'on' : 'off'}`);
417
423
  return enabled;
418
424
  }),
419
425
  version: readPackageVersion(),