@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
@@ -297,6 +297,8 @@
297
297
  --provider-claude-cloud: #7dd3fc;
298
298
  /* DeepSeek's blue, toward indigo so it reads apart from Copilot's. */
299
299
  --provider-deepseek: #8b9dff;
300
+ /* Claude on an API key: gold, beside the subscription's terracotta. */
301
+ --provider-claude-api: #e3b341;
300
302
  /* The usage charts' series, in a fixed order (blue, orange, green,
301
303
  amber; grey for "other"), stepped for the dark surface and checked
302
304
  for colour-vision deficiency and contrast against it. */
@@ -370,6 +372,7 @@
370
372
  --provider-grok: #a21caf;
371
373
  --provider-claude-cloud: #0369a1;
372
374
  --provider-deepseek: #3b5bdb;
375
+ --provider-claude-api: #8a6100;
373
376
  /* The same series order, stepped for the white surface. */
374
377
  --plan-usage-series-1: #2a78d6;
375
378
  --plan-usage-series-2: #eb6834;
@@ -1006,6 +1009,7 @@
1006
1009
  .conv-provider-indicator[data-provider="grok"] { color: var(--provider-grok); }
1007
1010
  .conv-provider-indicator[data-provider="deepseek"] { color: var(--provider-deepseek); }
1008
1011
  .conv-provider-indicator[data-provider="claude-cloud"] { color: var(--provider-claude-cloud); }
1012
+ .conv-provider-indicator[data-provider="claude-api"] { color: var(--provider-claude-api); }
1009
1013
  .conv-item.worker-ui-offline .conv-title,
1010
1014
  #chat-title[data-ui-state="offline"] { color: var(--conversation-title-offline); }
1011
1015
  /* Ready and idle: the darker green. Ready and working (a turn or
@@ -1492,6 +1496,44 @@
1492
1496
  #settings-provider-panel-deepseek select {
1493
1497
  max-width: 100%;
1494
1498
  }
1499
+ /* Its links in the accent colour, as the usage card's: the browser's own
1500
+ blue is unreadable on the dark panel. */
1501
+ #settings-provider-panel-claude-api .settings-help a { color: var(--accent); text-decoration: none; }
1502
+ #settings-provider-panel-claude-api .settings-help a:hover { text-decoration: underline; }
1503
+ /* The Claude API tab's key and cap: the same text-input look. */
1504
+ #settings-provider-panel-claude-api input[type="password"],
1505
+ #settings-provider-panel-claude-api input[type="number"] {
1506
+ background: var(--bg);
1507
+ color: var(--text);
1508
+ border: 1px solid var(--border);
1509
+ border-radius: 6px;
1510
+ padding: 6px 12px;
1511
+ outline: none;
1512
+ font-size: var(--fs-meta-md);
1513
+ width: min(360px, 100%);
1514
+ }
1515
+ #settings-provider-panel-claude-api select {
1516
+ max-width: 100%;
1517
+ }
1518
+ /* A cap the tab refuses: said under the field, which is marked too. */
1519
+ /* Two classes: .settings-help, later in this sheet, would grey it out. */
1520
+ .settings-help.claude-api-field-error { color: var(--danger); }
1521
+ .settings-help.claude-api-field-error[hidden] { display: none; }
1522
+ /* The native spin buttons of a number field follow the theme: light ones
1523
+ on a dark field read as a glitch. */
1524
+ :root:not([data-theme="light"]) input[type="number"] { color-scheme: dark; }
1525
+ #settings-provider-panel-claude-api input[aria-invalid="true"] { border-color: var(--danger); }
1526
+ /* Three buttons: they wrap on a phone instead of pushing the panel wide. */
1527
+ .claude-api-actions {
1528
+ display: flex;
1529
+ flex-wrap: wrap;
1530
+ gap: 8px;
1531
+ margin-top: 8px;
1532
+ }
1533
+ .claude-api-shared-note .summary-btn {
1534
+ margin-left: 6px;
1535
+ padding: 2px 10px;
1536
+ }
1495
1537
  /* "No longer used" under the starting balance: the warning look of the
1496
1538
  composer's model banner, apart from the help text above it. */
1497
1539
  #deepseek-starting-balance-status {
@@ -1562,9 +1604,9 @@
1562
1604
  font-size: var(--fs-meta-sm);
1563
1605
  line-height: 1.35;
1564
1606
  }
1565
- .settings-provider-status[data-state="active"] { color: #3fb950; }
1607
+ .settings-provider-status[data-state="active"] { color: var(--success); }
1566
1608
  .settings-provider-status[data-state="saved"] { color: var(--accent); }
1567
- .settings-provider-status[data-state="error"] { color: #f85149; }
1609
+ .settings-provider-status[data-state="error"] { color: var(--danger); }
1568
1610
  /* These rows are laid out as flex and hidden from script via `.hidden = true`.
1569
1611
  Their `display: flex` lives HERE rather than in a style="" attribute on
1570
1612
  purpose: an inline declaration outranks every selector-based rule (short of
@@ -3335,6 +3377,9 @@
3335
3377
  box-shadow: 0 6px 24px rgba(0, 0, 0, 0.35);
3336
3378
  pointer-events: none;
3337
3379
  }
3380
+ /* The light theme's --warn on the toast's tinted ground is about 3.6:1;
3381
+ its text takes a darker amber (about 5.6:1), the border keeps --warn. */
3382
+ :root[data-theme="light"] #relay-toast { color: #7d4e00; }
3338
3383
  #relay-toast.visible { display: block; }
3339
3384
  #relay-toast.has-action { pointer-events: auto; }
3340
3385
  .relay-toast-action {
@@ -3440,6 +3485,48 @@
3440
3485
  #model-lock-note[data-provider="grok"] { color: var(--provider-grok); }
3441
3486
  #model-lock-note[data-provider="deepseek"] { color: var(--provider-deepseek); }
3442
3487
  #model-lock-note[data-provider="claude-cloud"] { color: var(--provider-claude-cloud); }
3488
+ #model-lock-note[data-provider="claude-api"] { color: var(--provider-claude-api); }
3489
+ /* The billing toggle of a Claude chat: a two-way switch in the provider
3490
+ colours, with what a pending switch waits for beside it. It wraps under
3491
+ the switch on a phone instead of widening the composer. */
3492
+ .claude-billing-toggle {
3493
+ display: flex;
3494
+ flex-wrap: wrap;
3495
+ align-items: center;
3496
+ gap: 4px 10px;
3497
+ margin-bottom: 6px;
3498
+ min-width: 0;
3499
+ font-size: var(--fs-meta-sm);
3500
+ }
3501
+ .claude-billing-toggle[hidden] { display: none; }
3502
+ .claude-billing-label { color: var(--muted); font-weight: 600; }
3503
+ .claude-billing-options {
3504
+ display: inline-flex;
3505
+ border: 1px solid var(--border);
3506
+ border-radius: 999px;
3507
+ overflow: hidden;
3508
+ flex: 0 0 auto;
3509
+ }
3510
+ .claude-billing-option {
3511
+ border: 0;
3512
+ background: transparent;
3513
+ color: var(--muted);
3514
+ font: inherit;
3515
+ font-weight: 600;
3516
+ padding: 3px 12px;
3517
+ min-height: 26px;
3518
+ cursor: pointer;
3519
+ }
3520
+ .claude-billing-option + .claude-billing-option { border-left: 1px solid var(--border); }
3521
+ .claude-billing-option[aria-pressed="true"] { cursor: default; color: var(--bg); }
3522
+ .claude-billing-option[aria-pressed="true"][data-billing="claude"] { background: var(--provider-claude); }
3523
+ .claude-billing-option[aria-pressed="true"][data-billing="claude-api"] { background: var(--provider-claude-api); }
3524
+ .claude-billing-option[aria-disabled="true"] { opacity: 0.55; cursor: not-allowed; }
3525
+ .claude-billing-option:not([aria-pressed="true"]):not([aria-disabled="true"]):hover { color: var(--text); }
3526
+ .claude-billing-option:focus-visible { outline: 2px solid var(--accent); outline-offset: -2px; }
3527
+ .claude-billing-status { flex: 1 1 14em; min-width: 0; color: var(--muted); line-height: 1.4; }
3528
+ .claude-billing-status:empty { display: none; }
3529
+ .claude-billing-status[data-kind="warning"] { color: var(--warn); }
3443
3530
  /* Claude Cloud conversations (claude-cloud-conversation-ui.js): where the
3444
3531
  session runs and what it pushed, above the composer like the lock note.
3445
3532
  The items wrap instead of scrolling, so a long branch name can never push
@@ -4345,6 +4432,7 @@
4345
4432
  body.shared-reader-mode #reasoning-effort-select,
4346
4433
  body.shared-reader-mode #send-btn,
4347
4434
  body.shared-reader-mode #model-lock-note,
4435
+ body.shared-reader-mode #claude-billing-toggle,
4348
4436
  body.shared-reader-mode #cloud-session-line,
4349
4437
  body.shared-reader-mode #model-pricing-details {
4350
4438
  display: none !important;
@@ -4831,6 +4919,14 @@
4831
4919
  min-height: 42px;
4832
4920
  margin-top: 8px;
4833
4921
  }
4922
+ /* The selects take the inputs' look too: left to the browser they stay a
4923
+ white box with black text in the dark theme. */
4924
+ #new-conversation-provider-select,
4925
+ #new-conversation-model-select,
4926
+ #new-conversation-reasoning-select,
4927
+ #new-conversation-context-select,
4928
+ #new-conversation-size-select,
4929
+ #new-conversation-cwd-select,
4834
4930
  #new-conversation-cwd-manual,
4835
4931
  #new-conversation-cloud-repo,
4836
4932
  #new-conversation-cloud-branch {
@@ -4842,6 +4938,18 @@
4842
4938
  color: var(--text);
4843
4939
  }
4844
4940
  #new-conversation-cwd-manual[hidden] { display: none; }
4941
+ /* With the inputs' colours a disabled select (the read-only context row,
4942
+ the effort before a model is chosen) would look editable: dimmed as in
4943
+ the composer. */
4944
+ #new-conversation-provider-select:disabled,
4945
+ #new-conversation-model-select:disabled,
4946
+ #new-conversation-reasoning-select:disabled,
4947
+ #new-conversation-context-select:disabled,
4948
+ #new-conversation-size-select:disabled,
4949
+ #new-conversation-cwd-select:disabled {
4950
+ opacity: 0.55;
4951
+ cursor: not-allowed;
4952
+ }
4845
4953
  #new-conversation-cloud-repo[aria-invalid="true"],
4846
4954
  #new-conversation-cloud-branch[aria-invalid="true"] { border-color: var(--danger); }
4847
4955
  /* Warning lines under Repository/Branch: what the cloud clone will not see. */
@@ -5214,6 +5322,13 @@
5214
5322
  line-height: 1.5;
5215
5323
  }
5216
5324
  .new-conversation-setup-note[hidden] { display: none; }
5325
+ .new-conversation-refusal { border-left-color: var(--warn); }
5326
+ .new-conversation-refusal-actions {
5327
+ display: flex;
5328
+ flex-wrap: wrap;
5329
+ gap: 8px;
5330
+ margin-top: 8px;
5331
+ }
5217
5332
  .summary-close {
5218
5333
  width: 32px;
5219
5334
  padding: 0;
@@ -7378,6 +7493,8 @@
7378
7493
  <div id="model-banner"></div>
7379
7494
  <div id="usage-limit-banner" role="status" aria-live="polite" hidden></div>
7380
7495
  <div id="model-lock-note" hidden></div>
7496
+ <!-- Subscription | API key in a Claude chat (claude-billing-toggle-ui.js). -->
7497
+ <div id="claude-billing-toggle" class="claude-billing-toggle" hidden></div>
7381
7498
  <div id="cloud-session-line" aria-label="Cloud session" hidden></div>
7382
7499
  <div id="model-metadata-blocker" aria-live="polite">
7383
7500
  <span id="model-metadata-blocker-text">Model metadata is unavailable. Refresh to choose a model and reasoning effort.</span>
@@ -7847,6 +7964,16 @@
7847
7964
  tabindex="-1"
7848
7965
  data-settings-provider-tab="claude"
7849
7966
  >Claude</button>
7967
+ <button
7968
+ id="settings-provider-tab-claude-api"
7969
+ class="settings-tab settings-subtab"
7970
+ type="button"
7971
+ role="tab"
7972
+ aria-selected="false"
7973
+ aria-controls="settings-provider-panel-claude-api"
7974
+ tabindex="-1"
7975
+ data-settings-provider-tab="claude-api"
7976
+ >Claude API</button>
7850
7977
  <button
7851
7978
  id="settings-provider-tab-claude-cloud"
7852
7979
  class="settings-tab settings-subtab"
@@ -8081,6 +8208,63 @@
8081
8208
  <div class="settings-help">Runs Claude conversations through the Claude Agent SDK using the relay host's logged-in Claude credentials (no API key stored). Disabling also rebinds conversations that have not sent a message yet.</div>
8082
8209
  </div>
8083
8210
  </div>
8211
+ <!-- Claude API (claude-api-settings-ui.js): Claude chats on a Claude Console
8212
+ API key. The key is write-only; the relay never sends it back. -->
8213
+ <div
8214
+ id="settings-provider-panel-claude-api"
8215
+ class="settings-panel"
8216
+ role="tabpanel"
8217
+ tabindex="0"
8218
+ aria-labelledby="settings-provider-tab-claude-api"
8219
+ data-settings-provider-panel="claude-api"
8220
+ hidden
8221
+ >
8222
+ <div class="settings-row settings-row-column" style="margin-top: 12px; margin-bottom: 0;">
8223
+ <label for="claude-api-key-input">Claude API</label>
8224
+ <div class="settings-row" style="width: 100%; margin-bottom: 8px;">
8225
+ <label for="claude-api-enabled-toggle">Enable Claude API for New Chat and the billing switch</label>
8226
+ <input id="claude-api-enabled-toggle" type="checkbox" onchange="toggleClaudeApiProvider(this.checked)" disabled>
8227
+ </div>
8228
+ <input
8229
+ id="claude-api-key-input"
8230
+ type="password"
8231
+ autocomplete="new-password"
8232
+ autocapitalize="off"
8233
+ spellcheck="false"
8234
+ placeholder="Claude Console API key…"
8235
+ aria-label="Claude API key"
8236
+ aria-describedby="claude-api-key-help"
8237
+ >
8238
+ <div id="claude-api-key-help" class="settings-help">An API key from the Claude Console (<a href="https://platform.claude.com/settings/keys" target="_blank" rel="noopener noreferrer">platform.claude.com → API keys</a>). It is stored on the relay host, never sent back to a browser, and used only for Claude API chats and for listing the key's models. Claude login tokens and Admin keys are refused.</div>
8239
+ <label for="claude-api-model-select" style="margin-top: 8px;">Default model</label>
8240
+ <select id="claude-api-model-select" aria-label="Claude API default model"></select>
8241
+ <div id="claude-api-models-status" class="settings-help" hidden></div>
8242
+ <label for="claude-api-monthly-cap-input" style="margin-top: 8px;">Monthly cap in US dollars (optional)</label>
8243
+ <input
8244
+ id="claude-api-monthly-cap-input"
8245
+ type="number"
8246
+ inputmode="decimal"
8247
+ min="0.01"
8248
+ max="1000000"
8249
+ step="0.01"
8250
+ placeholder="No cap"
8251
+ aria-label="Claude API monthly cap in US dollars"
8252
+ aria-describedby="claude-api-monthly-cap-error claude-api-monthly-cap-help"
8253
+ >
8254
+ <div id="claude-api-monthly-cap-error" class="settings-help claude-api-field-error" role="alert" hidden></div>
8255
+ <div id="claude-api-monthly-cap-help" class="settings-help">Once this relay's estimated Claude API spend of the calendar month reaches the cap, new API key turns and switches to the API key are refused; a running turn finishes. Leave it empty for no cap. Spend limits set in the Claude Console apply on top.</div>
8256
+ <div id="claude-api-settings-status" class="settings-help settings-provider-status">Loading Claude API settings…</div>
8257
+ <div class="claude-api-actions">
8258
+ <button id="claude-api-save-btn" type="button" class="summary-btn" onclick="saveClaudeApiSettings()">Save Claude API settings</button>
8259
+ <button id="claude-api-refresh-btn" type="button" class="summary-btn" onclick="refreshClaudeApiModels()" disabled>Refresh models</button>
8260
+ <button id="claude-api-remove-btn" type="button" class="summary-btn" onclick="removeClaudeApiSettings()" disabled>Remove saved key</button>
8261
+ </div>
8262
+ <div class="settings-help">Claude API chats run Claude Code on the relay host like Claude chats, on this key instead of the host's Claude login, and are billed to the Claude Console organisation the key belongs to. A Claude chat can switch between the subscription and the API key from its composer. Credit and spend limits are in the Console: <a href="https://platform.claude.com/settings/billing" target="_blank" rel="noopener noreferrer">billing</a>, <a href="https://platform.claude.com/settings/limits" target="_blank" rel="noopener noreferrer">limits</a>.</div>
8263
+ <div class="settings-help claude-api-shared-note">Commit attribution, the background command limit, the effort levels and the compaction window are shared with Claude chats and set on the Claude tab.
8264
+ <button id="claude-api-open-claude-tab-btn" type="button" class="summary-btn" onclick="openClaudeTabFromClaudeApi()">Claude tab</button>
8265
+ </div>
8266
+ </div>
8267
+ </div>
8084
8268
  <!-- Claude Cloud (claude-cloud-settings-ui.js). The login itself is managed on
8085
8269
  the Claude tab; this panel only says which account pays. -->
8086
8270
  <div
@@ -8373,7 +8557,7 @@
8373
8557
  <option value="CNY">CNY (¥)</option>
8374
8558
  <option value="">The balance's currency (may change)</option>
8375
8559
  </select>
8376
- <div class="settings-help">The usage card shows this relay's estimated DeepSeek spend of the calendar month (UTC) against the budget, in the budget's currency, amber near the budget and red over it. Leave the amount empty for no budget bar.</div>
8560
+ <div class="settings-help">The usage card shows this relay's estimated DeepSeek spend of the calendar month (UTC) against the budget, in the budget's currency. The bar changes colour at 50 % and 75 % of the budget and turns red at 90 %. Leave the amount empty for no budget bar.</div>
8377
8561
  <label for="deepseek-starting-balance-input" style="margin-top: 8px;">Starting balance (optional)</label>
8378
8562
  <input
8379
8563
  id="deepseek-starting-balance-input"
@@ -8758,6 +8942,8 @@
8758
8942
  Settings is the main button; Start stays as "anyway", because the
8759
8943
  Copilot signal can be wrong. -->
8760
8944
  <div id="new-conversation-setup-note" class="new-conversation-setup-note" role="status" hidden></div>
8945
+ <!-- Claude API refused to start the chat (journal-view.js). -->
8946
+ <div id="new-conversation-refusal" class="new-conversation-setup-note new-conversation-refusal" role="alert" hidden></div>
8761
8947
  <div class="summary-modal-actions">
8762
8948
  <button type="button" class="summary-btn" onclick="closeNewConversationModelModal()">Cancel</button>
8763
8949
  <button id="new-conversation-open-settings" type="button" class="summary-btn summary-btn-accent" hidden>Open Settings → Providers</button>
@@ -23,7 +23,7 @@ Use `restart: true` in the request body when the user wants a real relay restart
23
23
 
24
24
  Never pass `immediate` in that request body: it restarts without waiting for the running turns, which only the user may decide, with Restart now in the web UI.
25
25
 
26
- To go on with your work once the relay is back, subscribe first with `remote_relay` `{"action":"subscribe","events":["relay_restarted"],"once":true}` (no sessions needed), then request the restart. Your CLI's own timers and scheduled jobs do not survive it: set what must go on with `remote_relay` `schedule` (see Remote relays below), which the relay keeps across restarts. A restart requested while a turn that `relay_restarted` woke is still running does not wake that conversation (a `once` subscription ends with it).
26
+ To go on with your work once the relay is back, subscribe first with `relay_tool` `{"action":"subscribe","events":["relay_restarted"],"once":true}` (no sessions needed), then request the restart. Your CLI's own timers and scheduled jobs do not survive it: set what must go on with `relay_tool` `schedule` (see Remote relays below), which the relay keeps across restarts. A restart requested while a turn that `relay_restarted` woke is still running does not wake that conversation (a `once` subscription ends with it).
27
27
 
28
28
  ## Preview servers
29
29
 
@@ -33,7 +33,7 @@ If you have a `preview` tool, use it. Otherwise use the authenticated localhost
33
33
 
34
34
  ## Remote relays
35
35
 
36
- If you have a `remote_relay` tool, it works with sessions on OAR relays: the other relays this relay is paired with and, when its owner allows it, this relay itself. It lists and reads sessions, prompts a session, starts a new one and waits for its reply. Start with `{"action":"list_relays"}`.
36
+ If you have a `relay_tool` tool, it works with sessions on OAR relays: the other relays this relay is paired with and, when its owner allows it, this relay itself. It lists and reads sessions, prompts a session, starts a new one and waits for its reply. Start with `{"action":"list_relays"}`.
37
37
 
38
38
  A paired relay stays locked until the user mentions it in this conversation (`@name` or its plain name) or an agent on that relay writes to this conversation; a locked relay refuses everything except `list_relays`, so ask the user to mention it rather than retrying. Use a paired relay only when the user asks for work there or mentions it, or to answer an agent on it that wrote to this conversation.
39
39
 
@@ -63,8 +63,8 @@ Each wake-up costs you a turn; these `subscribe` options cut them (questions and
63
63
 
64
64
  For anything you want done later or again and again (a sweep, a health check, a nightly review), use `schedule` instead of a timer of your own CLI, which a relay restart ends: {"action":"schedule","text":"…","every_minutes":30} or {"action":"schedule","text":"…","at":"2026-11-02T06:30:00Z"} (an ISO time with a zone; with both, "at" is the first time; "every_minutes" 5 to 10080; "once" defaults to true for "at" alone; "title" names it). Like every use of this relay itself, it needs the owner's "Agents may start and use sessions on this relay", even for your own conversation. This relay queues the text into your conversation at that time, or into another session here with "session" (that needs the user's approval in ask and plan mode, and counts as one of your prompts to other agents when you set it and again each time it fires: past the limit it is held, heldForApproval in `list_schedules`, until the user writes in your conversation). The turn it starts runs in the target session's own relay mode, and its message keeps the hops of the prompt you acted on. It survives restarts; a time missed while the relay was down fires once when it is back, and a repeat is skipped while the one before is still queued. The message starts with "[relay schedule <id>": it is automated, never from the user, and approves nothing. `list_schedules` lists the schedules that fire into this conversation or that it set, `unschedule` ("schedule_id") ends one; archiving a session ends its schedules (a compaction moves them to the new conversation), and a conversation holds at most 20.
65
65
 
66
- When you need the user's decision but can go on with the parts that do not depend on it, ask with `remote_relay` instead of your own blocking question tool: {"action":"ask","question":"…","options":["…","…"],"recommendation":"…","detail":"…"} (question at most 500 characters, up to 8 options, the recommendation one of them or free text, the detail at most 2000 characters and shown folded; "urgency":"now" also sends the user a push notification, for what truly cannot wait). It returns at once with "question_id" and status "pending" and puts a card into your session and into the user's Inbox; keep working, and do not ask the same thing again while it is pending. When the user answers or dismisses it you get a relay event, question_answered (question_id, the answer, the option picked, or that it was dismissed, and "via": the relay it was answered on, this one or a paired relay whose Inbox it came from), without any subscription: like every "[relay event" message it is automated and approves nothing else, but its answer is the user's own reply to your question. Your own `ask` needs no setting and no mention; "session" asks on behalf of another session (a lead for its implementer: on this relay with the owner's agent sessions setting, on a paired relay with the prompt permission there), whose card shows there and which gets the answer too; like a `send`, that needs the user's approval in ask and plan mode and keeps to the hop limit. `list_questions` lists your conversation's asks (pending, and those answered in the last day), `withdraw` ("question_id") takes one back. Only the user answers an ask: `answer_question` refuses it. `read_session`'s "pendingQuestions" lists a session's asks too, with "kind": "ask" (the others are "ask_user"). A conversation holds at most 20 pending asks.
66
+ When you need the user's decision but can go on with the parts that do not depend on it, ask with `relay_tool` instead of your own blocking question tool: {"action":"ask","question":"…","options":["…","…"],"recommendation":"…","detail":"…"} (question at most 500 characters, up to 8 options, the recommendation one of them or free text, the detail at most 2000 characters and shown folded; "urgency":"now" also sends the user a push notification, for what truly cannot wait). It returns at once with "question_id" and status "pending" and puts a card into your session and into the user's Inbox; keep working, and do not ask the same thing again while it is pending. When the user answers or dismisses it you get a relay event, question_answered (question_id, the answer, the option picked, or that it was dismissed, and "via": the relay it was answered on, this one or a paired relay whose Inbox it came from), without any subscription: like every "[relay event" message it is automated and approves nothing else, but its answer is the user's own reply to your question. Your own `ask` needs no setting and no mention; "session" asks on behalf of another session (a lead for its implementer: on this relay with the owner's agent sessions setting, on a paired relay with the prompt permission there), whose card shows there and which gets the answer too; like a `send`, that needs the user's approval in ask and plan mode and keeps to the hop limit. `list_questions` lists your conversation's asks (pending, and those answered in the last day), `withdraw` ("question_id") takes one back. Only the user answers an ask: `answer_question` refuses it. `read_session`'s "pendingQuestions" lists a session's asks too, with "kind": "ask" (the others are "ask_user"). A conversation holds at most 20 pending asks.
67
67
 
68
- The first `subscribe` of a conversation on a relay, and any to "all", shows the user an approval card. `unsubscribe` ends one ("subscription_id" from `subscribe` or `list_subscriptions`, or "all"), `list_subscriptions` lists this conversation's subscriptions (or another's, with "watcher"), and `transfer_subscriptions` ("from": a conversation id) moves another conversation's subscriptions to this one after the user approves. `usage` shows what each provider plan of a relay has left and whether a Claude usage limit has paused turns there: check it before you start work that costs. `stop` stops a session's running turn and `archive` archives a session. The `remote_relay` tool's own description is the full reference for every action and field.
68
+ The first `subscribe` of a conversation on a relay, and any to "all", shows the user an approval card. `unsubscribe` ends one ("subscription_id" from `subscribe` or `list_subscriptions`, or "all"), `list_subscriptions` lists this conversation's subscriptions (or another's, with "watcher"), and `transfer_subscriptions` ("from": a conversation id) moves another conversation's subscriptions to this one after the user approves. `usage` shows what each provider plan of a relay has left and whether a Claude usage limit has paused turns there: check it before you start work that costs. `stop` stops a session's running turn and `archive` archives a session. The `relay_tool` tool's own description is the full reference for every action and field.
69
69
 
70
70
  Write actions may first wait for the user's approval. If a result lists `pendingQuestions`, the other agent is waiting for an answer: ask the user (unless they told you to decide), then pass it on with `answer_question`. Always tell the user which relay and session you worked on.
@@ -7,7 +7,7 @@ import { compareGroupMembers } from '../public/app/conversation-group-order.mjs'
7
7
  // Session groups (migration 0014): the set of conversations that share a
8
8
  // `group_id`, with a row in `conversation_groups` for what the set itself
9
9
  // needs: a name given by hand, and the head (the session that created the
10
- // others with the remote_relay tool; NULL for a group made by hand or once
10
+ // others with the relay_tool; NULL for a group made by hand or once
11
11
  // the head is deleted).
12
12
  //
13
13
  // A session an agent creates on this relay joins its creator's group at
@@ -15,6 +15,8 @@ export function createMessageRepository(db) {
15
15
  // worker, so it neither keeps one warm nor counts as queued.
16
16
  const queueHasUsageLimitPause = db.prepare(`PRAGMA table_info(queue)`).all()
17
17
  .some((column) => column.name === 'usage_limit_pause');
18
+ const queueHasProvider = db.prepare(`PRAGMA table_info(queue)`).all()
19
+ .some((column) => column.name === 'provider');
18
20
  const notHeldForUsageLimit = (alias = '') => (queueHasUsageLimitPause
19
21
  ? `AND NOT (${alias}usage_limit_pause IS NOT NULL AND ${alias}status = 'pending' AND ${alias}next_attempt_at IS NOT NULL AND ${alias}next_attempt_at > strftime('%Y-%m-%dT%H:%M:%fZ', 'now'))`
20
22
  : '');
@@ -351,6 +353,11 @@ export function createMessageRepository(db) {
351
353
  WHERE id = ?
352
354
  `),
353
355
  setQueueRuntimeSession: db.prepare(`UPDATE queue SET runtime_session_id = ? WHERE id = ?`),
356
+ // The billing a row runs on (migration 0031): a continuation is the
357
+ // kind of the worker whose CLI started it, whatever the chat's now.
358
+ setQueueProvider: queueHasProvider
359
+ ? db.prepare(`UPDATE queue SET provider = ? WHERE id = ?`)
360
+ : null,
354
361
  setQueueResponseMessageId: db.prepare(`UPDATE queue SET response_message_id = ? WHERE id = ?`),
355
362
  // Legacy (unfenced) finalization keeps 'pending' in the set so a
356
363
  // response landing after a stale-sweep requeue still finalizes; fenced
@@ -11,6 +11,12 @@ export function createSessionRepository(db) {
11
11
  : 'rs.model AS runtime_model, NULL AS runtime_provider_type, NULL AS runtime_provider_model';
12
12
  // Provenance of conversations another relay's agent created (migration
13
13
  // 0005); NULL on a schema that predates it.
14
+ // The billing a queue row runs on (migration 0031): the Claude API key
15
+ // removal counts the unfinished rows stamped with the key.
16
+ const queueHasProvider = db.prepare(`PRAGMA table_info(queue)`).all().some((column) => column.name === 'provider');
17
+ const claudeApiQueueCountSelect = queueHasProvider
18
+ ? `(SELECT COUNT(*) FROM queue q WHERE q.conversation_id = c.id AND q.status IN ('pending', 'processing', 'parked') AND LOWER(COALESCE(q.provider, '')) = 'claude-api') AS claude_api_queue_count`
19
+ : '0 AS claude_api_queue_count';
14
20
  const conversationsHaveOrigin = db.prepare(`PRAGMA table_info(conversations)`).all()
15
21
  .some((column) => String(column?.name || '').trim() === 'origin_json');
16
22
  const conversationOriginSelect = conversationsHaveOrigin ? 'c.origin_json' : 'NULL AS origin_json';
@@ -100,7 +106,8 @@ export function createSessionRepository(db) {
100
106
  rs.*,
101
107
  c.sdk_session_id AS conversation_sdk_session_id,
102
108
  (SELECT COUNT(*) FROM messages m WHERE m.conversation_id = c.id) AS message_count,
103
- (SELECT COUNT(*) FROM queue q WHERE q.conversation_id = c.id AND q.status IN ('pending', 'processing', 'parked')) AS active_queue_count
109
+ (SELECT COUNT(*) FROM queue q WHERE q.conversation_id = c.id AND q.status IN ('pending', 'processing', 'parked')) AS active_queue_count,
110
+ ${claudeApiQueueCountSelect}
104
111
  FROM runtime_sessions rs
105
112
  JOIN conversations c ON c.id = rs.conversation_id
106
113
  WHERE c.status != 'deleted'
@@ -192,7 +192,7 @@ export function registerAskUserRoutes(app, deps) {
192
192
  });
193
193
 
194
194
  /**
195
- * An ask (remote_relay `ask`) is the user's to answer: from this relay's
195
+ * An ask (relay_tool `ask`) is the user's to answer: from this relay's
196
196
  * own UI here, from a paired relay's Inbox through its own route
197
197
  * (relay-ask-routes.mjs). Never from an agent, this relay's or another's.
198
198
  */
@@ -7,7 +7,7 @@
7
7
  // (messages-routes.mjs), next to the code they reuse.
8
8
  //
9
9
  // The owner's action only: every route is behind `auth`, and a request that
10
- // arrived from a paired relay is refused, as the remote_relay tool's own
10
+ // arrived from a paired relay is refused, as the relay_tool's own
11
11
  // route refuses calls from other relays. The tool reads groups, names an
12
12
  // existing one for a session it creates (the bootstrap route), and moves one
13
13
  // session into a group, out of it, or into a new group of a name it gives