@oar-sh/oar 0.9.17 → 0.9.19

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 (104) hide show
  1. package/.github/extensions/web-relay/server-lifecycle/managed-server.mjs +13 -1
  2. package/.github/extensions/web-relay/skills/tool-activity.mjs +5 -4
  3. package/CHANGELOG.md +234 -0
  4. package/README.md +13 -12
  5. package/package.json +1 -1
  6. package/server/claude-api-worker/claude-api-backend.mjs +345 -0
  7. package/server/claude-api-worker/claude-api-session-worker.mjs +29 -0
  8. package/server/claude-worker/claude-cli-pinned-env.mjs +150 -0
  9. package/server/claude-worker/claude-remote-relay-tool.mjs +5 -5
  10. package/server/claude-worker/claude-sdk-adapter.mjs +35 -9
  11. package/server/claude-worker/claude-session-process.mjs +166 -21
  12. package/server/claude-worker/claude-session-worker-main.mjs +3 -0
  13. package/server/claude-worker/claude-subscription-key-check.mjs +64 -0
  14. package/server/claude-worker/claude-turn-publisher.mjs +117 -5
  15. package/server/claude-worker/claude-usage-totals.mjs +53 -0
  16. package/server/claude-worker/sdk-message-normalizer.mjs +1 -1
  17. package/server/copilot-worker/copilot-remote-relay-tool.mjs +2 -2
  18. package/server/copilot-worker/copilot-sdk-event-normalizer.mjs +1 -1
  19. package/server/copilot-worker/copilot-sdk-session-process.mjs +9 -9
  20. package/server/cursor-worker/cursor-remote-relay-tool.mjs +3 -3
  21. package/server/cursor-worker/cursor-turn-runner.mjs +4 -4
  22. package/server/cursor-worker/sdk-message-normalizer.mjs +2 -2
  23. package/server/db-schema.mjs +33 -1
  24. package/server/deepseek-worker/deepseek-claude-backend.mjs +14 -130
  25. package/server/grok-worker/acp-client.mjs +1 -1
  26. package/server/grok-worker/grok-turn-runner.mjs +4 -4
  27. package/server/grok-worker/sdk-message-normalizer.mjs +1 -1
  28. package/server/mcp/oar-mcp-server.mjs +11 -5
  29. package/server/migrations/0031-queue-provider.mjs +143 -0
  30. package/server/migrations/0032-runtime-session-billing-switch.mjs +94 -0
  31. package/server/migrations/0033-claude-api-usage-checkpoints.mjs +138 -0
  32. package/server/migrations/0034-claude-api-usage-totals.mjs +112 -0
  33. package/server/public/app/activity-replay-state.mjs +4 -3
  34. package/server/public/app/agent-sessions-settings-ui.js +1 -1
  35. package/server/public/app/agent-sessions-settings.mjs +1 -1
  36. package/server/public/app/api-client.js +52 -0
  37. package/server/public/app/bootstrap.js +153 -27
  38. package/server/public/app/claude-api-settings-ui.js +362 -0
  39. package/server/public/app/claude-billing-toggle-ui.js +224 -0
  40. package/server/public/app/claude-billing-toggle.mjs +112 -0
  41. package/server/public/app/context-tier-options.mjs +6 -2
  42. package/server/public/app/context-usage-view.mjs +1 -1
  43. package/server/public/app/conversation-provider-indicator.mjs +11 -0
  44. package/server/public/app/conversation-view.js +29 -3
  45. package/server/public/app/inbox-view.mjs +1 -1
  46. package/server/public/app/journal-view.js +127 -10
  47. package/server/public/app/model-selector-options.mjs +17 -1
  48. package/server/public/app/new-conversation-model-choice.mjs +21 -0
  49. package/server/public/app/provider-readiness.mjs +5 -2
  50. package/server/public/app/relay-error-ctas.mjs +50 -0
  51. package/server/public/app/relay-schedules-ui.js +1 -1
  52. package/server/public/app/settings-modal.js +11 -0
  53. package/server/public/app/settings-tabs.js +1 -1
  54. package/server/public/app/socket-handlers.js +18 -1
  55. package/server/public/app/tool-activity-emoji.mjs +4 -3
  56. package/server/public/app/usage-limit-ui.js +136 -5
  57. package/server/public/index.html +189 -3
  58. package/server/relay-heap-guard.mjs +338 -0
  59. package/server/relay-self-restart.mjs +33 -7
  60. package/server/relay-tools.md +6 -6
  61. package/server/repositories/conversation-group-repository.mjs +1 -1
  62. package/server/repositories/message-repository.mjs +7 -0
  63. package/server/repositories/session-repository.mjs +8 -1
  64. package/server/routes/ask-user-routes.mjs +1 -1
  65. package/server/routes/conversation-group-routes.mjs +1 -1
  66. package/server/routes/messages-routes.mjs +678 -31
  67. package/server/routes/relay-ask-routes.mjs +3 -3
  68. package/server/routes/relay-event-routes.mjs +2 -2
  69. package/server/routes/relay-schedule-routes.mjs +1 -1
  70. package/server/routes/remote-relay-routes.mjs +3 -3
  71. package/server/routes/sessions-routes.mjs +365 -53
  72. package/server/server-runtime.mjs +373 -26
  73. package/server/services/about-service.mjs +1 -0
  74. package/server/services/background-work-view.mjs +2 -1
  75. package/server/services/claude-api-settings-service.mjs +575 -0
  76. package/server/services/claude-api-usage-service.mjs +361 -0
  77. package/server/services/claude-billing-swap-service.mjs +274 -0
  78. package/server/services/claude-billing-switch-service.mjs +343 -0
  79. package/server/services/plan-usage-claude-api.mjs +445 -0
  80. package/server/services/plan-usage-service.mjs +5 -0
  81. package/server/services/provider-model-selection.mjs +3 -2
  82. package/server/services/provider-reasoning-effort.mjs +9 -0
  83. package/server/services/relay-ask-service.mjs +1 -1
  84. package/server/services/relay-event-service.mjs +3 -2
  85. package/server/services/relay-schedule-service.mjs +1 -1
  86. package/server/services/remote-relay-caller-context.mjs +7 -0
  87. package/server/services/remote-relay-dispatcher.mjs +111 -17
  88. package/server/services/remote-relay-registry-service.mjs +1 -1
  89. package/server/services/session-worker-launch-service.mjs +61 -2
  90. package/server/services/session-worker-registry-service.mjs +28 -1
  91. package/server/services/session-worker-websocket-service.mjs +17 -0
  92. package/server/services/usage-limit-pause-service.mjs +55 -4
  93. package/server/services/worker-secret-redaction.mjs +2 -0
  94. package/shared/anthropic-api.mjs +202 -0
  95. package/shared/provider-routing.mjs +5 -3
  96. package/shared/relay-asks.mjs +11 -5
  97. package/shared/relay-events.mjs +2 -2
  98. package/shared/relay-schedules.mjs +2 -2
  99. package/shared/remote-relay-contract.mjs +54 -12
  100. package/shared/remote-relay-tool-core.mjs +23 -6
  101. package/shared/steer-settle-failure.mjs +1 -0
  102. package/shared/worker-runtime/api-client.mjs +2 -2
  103. package/shared/worker-runtime/heartbeat.mjs +5 -0
  104. package/shared/worker-runtime/worker-websocket-link.mjs +6 -0
@@ -1,6 +1,7 @@
1
1
  import { spawn } from 'child_process';
2
2
  import path from 'path';
3
3
  import { RELAY_RESTART_EXIT_CODE, RELAY_STOP_EXIT_CODE } from './relay-exit-codes.mjs';
4
+ import { relayRuntimeNodeFlags, relayRuntimeNodeFlagsOrNone } from './relay-heap-guard.mjs';
4
5
 
5
6
  // Role selection travels on argv, never in the environment. The runtime's env is
6
7
  // inherited by tmux worker sessions and by the Copilot CLI it launches, and the
@@ -34,8 +35,11 @@ export function spawnRelayRuntime({
34
35
  detached = false,
35
36
  windowsHide = true,
36
37
  logger = console,
38
+ // The heap limit and heap snapshot flags (relay-heap-guard.mjs).
39
+ nodeFlags = relayRuntimeNodeFlags,
37
40
  } = {}) {
38
- const child = spawnImpl(process.execPath, [...execArgv, scriptPath, RELAY_RUNTIME_FLAG, ...args], {
41
+ const { flags } = relayRuntimeNodeFlagsOrNone(nodeFlags, { env, execArgv, serverDir: path.dirname(String(scriptPath || '')), logger });
42
+ const child = spawnImpl(process.execPath, [...flags, ...execArgv, scriptPath, RELAY_RUNTIME_FLAG, ...args], {
39
43
  cwd,
40
44
  env,
41
45
  detached,
@@ -84,8 +88,14 @@ export async function runDirectRelaySupervisor({
84
88
  exitImpl = (code) => process.exit(code),
85
89
  logger = console,
86
90
  installSignalHandlers = true,
91
+ signals = process,
92
+ nodeFlags = relayRuntimeNodeFlags,
87
93
  restartDelayMs = 500,
88
94
  maxCrashRestarts = 3,
95
+ // A runtime that ran this long before it crashed starts the count afresh:
96
+ // crashes days apart are not a crash loop.
97
+ crashCountResetMs = 10 * 60 * 1000,
98
+ now = Date.now,
89
99
  } = {}) {
90
100
  let runtimeProc = null;
91
101
  let shuttingDown = false;
@@ -102,13 +112,20 @@ export async function runDirectRelaySupervisor({
102
112
  const handleSigTerm = () => requestShutdown('SIGTERM', 0);
103
113
 
104
114
  if (installSignalHandlers) {
105
- process.on('SIGINT', handleSigInt);
106
- process.on('SIGTERM', handleSigTerm);
115
+ signals.on('SIGINT', handleSigInt);
116
+ signals.on('SIGTERM', handleSigTerm);
107
117
  }
108
118
 
109
119
  try {
110
120
  let crashCount = 0;
111
121
  while (true) {
122
+ // A Ctrl-C reaches the runtime (same process group) before this
123
+ // supervisor's handler can run: the runtime's signal exit looks like a
124
+ // crash, and the shutdown arrives during the delay. Never relaunch then.
125
+ if (shuttingDown) {
126
+ return await exitImpl(shutdownExitCode);
127
+ }
128
+ const startedAt = now();
112
129
  runtimeProc = spawnRelayRuntime({
113
130
  env,
114
131
  cwd,
@@ -119,6 +136,7 @@ export async function runDirectRelaySupervisor({
119
136
  stdio,
120
137
  detached: false,
121
138
  logger,
139
+ nodeFlags,
122
140
  });
123
141
  if (runtimeProc.stdout?.on) {
124
142
  runtimeProc.stdout.on('data', (chunk) => process.stdout.write(chunk));
@@ -139,7 +157,13 @@ export async function runDirectRelaySupervisor({
139
157
  return await exitImpl(1);
140
158
  }
141
159
 
142
- const exitCode = Number.isInteger(Number(result.code)) ? Number(result.code) : null;
160
+ // A runtime killed by a signal (an out-of-memory abort is SIGABRT, the
161
+ // kernel's OOM killer SIGKILL) exits with code null: a crash. Number(null)
162
+ // is 0, which once ended the supervisor as if the runtime stopped cleanly.
163
+ const exitCode = result.code !== null && result.code !== undefined && Number.isInteger(Number(result.code))
164
+ ? Number(result.code)
165
+ : null;
166
+ const signal = exitCode === null ? (result.signal || null) : null;
143
167
  if (exitCode === RELAY_RESTART_EXIT_CODE) {
144
168
  logger?.log?.(`[relay] runtime requested restart; relaunching ${path.basename(String(scriptPath || ''))}...`);
145
169
  crashCount = 0;
@@ -156,8 +180,10 @@ export async function runDirectRelaySupervisor({
156
180
  return await exitImpl(RELAY_STOP_EXIT_CODE);
157
181
  }
158
182
 
183
+ if (now() - startedAt >= crashCountResetMs) crashCount = 0;
159
184
  const attempt = crashCount + 1;
160
- logger?.error?.(`[relay] runtime crashed with exit code ${exitCode ?? 'null'} (crash ${attempt}/${maxCrashRestarts}).`);
185
+ const how = signal ? `signal ${signal}` : `exit code ${exitCode ?? 'null'}`;
186
+ logger?.error?.(`[relay] runtime crashed with ${how} (crash ${attempt}/${maxCrashRestarts}).`);
161
187
  if (attempt >= maxCrashRestarts) {
162
188
  logger?.error?.('[relay] crash limit reached; supervisor stopping.');
163
189
  return await exitImpl(1);
@@ -168,8 +194,8 @@ export async function runDirectRelaySupervisor({
168
194
 
169
195
  } finally {
170
196
  if (installSignalHandlers) {
171
- process.off('SIGINT', handleSigInt);
172
- process.off('SIGTERM', handleSigTerm);
197
+ signals.off('SIGINT', handleSigInt);
198
+ signals.off('SIGTERM', handleSigTerm);
173
199
  }
174
200
  }
175
201
  }
@@ -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,18 +33,18 @@ 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
 
40
40
  This relay itself is listed with "self": true when its owner switched agent sessions on, and is addressed by its name or as "this"; it needs no mention. Use it to hand parts of your task to other sessions:
41
41
 
42
- - `create_session` starts a session with any provider that `relay_info` lists: a local one in a folder ("cwd"), or provider "claude-cloud" with "repo" (a GitHub URL or owner/repo) and an optional "branch". At most 4 sessions you started may work at the same time; a fifth is refused until one finishes.
42
+ - `create_session` starts a session with any provider that `relay_info` lists: a local one in a folder ("cwd"), or provider "claude-cloud" with "repo" (a GitHub URL or owner/repo) and an optional "branch". The provider ids are the target relay's, as `relay_info` shows them; an id it does not know is refused with the list it has. A session started before a relay update can use providers the update added, without a restart. At most 4 sessions you started may work at the same time; a fifth is refused until one finishes.
43
43
  - The first `create_session` on this relay shows the user an approval card, once per conversation. A session that an agent created cannot create sessions itself (one level only), unless the owner allowed it for that session: `list_sessions` and `read_session` then show "mayCreateSessions": true. Only the owner sets that.
44
44
  - Sessions you start here are grouped under this session in the user's sidebar. `list_sessions` lists in the sidebar's order (a group sorts by its newest session, head first) and shows the group as a "group" field (id, name, role); `read_session` adds the group's live members with their state. `create_session` takes "group" (a group id, or its name in any case, from `relay_info`'s "groups") to put the new session into an existing group instead, here or on a paired relay; an unknown group is refused with the groups listed and nothing is created, and the result carries the "group". Renaming and dissolving groups is the user's.
45
45
  - `update_session` ("session" plus any of "model", "effort", "mode", "auto_compact_window", "title", "group", "thinking_enabled", "thinking_display", "cwd") changes another session's settings for its next turns without prompting it, here or on a paired relay, and answers the settings and "changed"; a group name that does not exist makes one, "none" takes the session out (a group the user made stays, even empty; one update_session made goes once empty), a new "cwd" restarts its CLI there (refused while it works), "auto_compact_window" 0 is the model default. The model is checked as for `send`. A failed call still lists in "changed" what it wrote before the failing step. A mode set this way lasts until the next `send` that names another mode or comes from a caller in another mode: a `send` without "mode" passes the caller's own mode.
46
46
  - Only a session your conversation started may be updated freely; any other (the user's, another agent's, the one that started you) needs the user's approval card once per conversation and relay (refused: `REMOTE_RELAY_PERMISSION`); never your own conversation.
47
- - A "model" for `create_session` or `send` must be an id `relay_info` lists for the provider (Claude also takes "opus", "sonnet", "haiku"). An unknown id is refused with the close matches and the valid ids (`Unknown model 'gpt-5-4' for Copilot on relay "…"; did you mean 'gpt-5.4'? Valid ids: … Nothing was sent.`), and nothing is created or sent: pick from the list instead of guessing. If the answer has a "modelNote" saying the model could not be checked, the relay had no model list yet.
47
+ - A "model" for `create_session` or `send` must be an id `relay_info` lists for the provider (Claude also takes "opus", "sonnet", "haiku"). An unknown id is refused with the close matches and the valid ids (`Unknown model 'gpt-5-4' for Copilot on relay "…"; did you mean 'gpt-5.4'? Valid ids: … Nothing was sent.`), and nothing is created or sent: pick from the list instead of guessing. Copilot ids use dots (`claude-opus-5.5`, not the Claude form `claude-opus-5-5`); a hyphen form is refused with the close match named, and a Copilot session never moves to another provider because of its model name. If the answer has a "modelNote" saying the model could not be checked, the relay had no model list yet.
48
48
  - A new session knows nothing of this conversation: write a self-contained prompt. A Claude Cloud session works on a fresh clone in a sandbox at Anthropic, so name the branch it should work on and push.
49
49
  - Follow each session with `wait` (the message id comes back from `create_session` and `send`) and `read_session`; "wait_seconds" is limited to the maximum set on the relay (default 600).
50
50
  - Sessions run on the relay owner's accounts and cost their usage: start only what the task needs.
@@ -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