@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.
- package/.github/extensions/web-relay/server-lifecycle/managed-server.mjs +13 -1
- package/.github/extensions/web-relay/skills/tool-activity.mjs +5 -4
- package/CHANGELOG.md +234 -0
- package/README.md +13 -12
- package/package.json +1 -1
- package/server/claude-api-worker/claude-api-backend.mjs +345 -0
- package/server/claude-api-worker/claude-api-session-worker.mjs +29 -0
- package/server/claude-worker/claude-cli-pinned-env.mjs +150 -0
- package/server/claude-worker/claude-remote-relay-tool.mjs +5 -5
- package/server/claude-worker/claude-sdk-adapter.mjs +35 -9
- package/server/claude-worker/claude-session-process.mjs +166 -21
- package/server/claude-worker/claude-session-worker-main.mjs +3 -0
- package/server/claude-worker/claude-subscription-key-check.mjs +64 -0
- package/server/claude-worker/claude-turn-publisher.mjs +117 -5
- package/server/claude-worker/claude-usage-totals.mjs +53 -0
- package/server/claude-worker/sdk-message-normalizer.mjs +1 -1
- package/server/copilot-worker/copilot-remote-relay-tool.mjs +2 -2
- package/server/copilot-worker/copilot-sdk-event-normalizer.mjs +1 -1
- package/server/copilot-worker/copilot-sdk-session-process.mjs +9 -9
- package/server/cursor-worker/cursor-remote-relay-tool.mjs +3 -3
- package/server/cursor-worker/cursor-turn-runner.mjs +4 -4
- package/server/cursor-worker/sdk-message-normalizer.mjs +2 -2
- package/server/db-schema.mjs +33 -1
- package/server/deepseek-worker/deepseek-claude-backend.mjs +14 -130
- package/server/grok-worker/acp-client.mjs +1 -1
- package/server/grok-worker/grok-turn-runner.mjs +4 -4
- package/server/grok-worker/sdk-message-normalizer.mjs +1 -1
- package/server/mcp/oar-mcp-server.mjs +11 -5
- package/server/migrations/0031-queue-provider.mjs +143 -0
- package/server/migrations/0032-runtime-session-billing-switch.mjs +94 -0
- package/server/migrations/0033-claude-api-usage-checkpoints.mjs +138 -0
- package/server/migrations/0034-claude-api-usage-totals.mjs +112 -0
- package/server/public/app/activity-replay-state.mjs +4 -3
- package/server/public/app/agent-sessions-settings-ui.js +1 -1
- package/server/public/app/agent-sessions-settings.mjs +1 -1
- package/server/public/app/api-client.js +52 -0
- package/server/public/app/bootstrap.js +153 -27
- package/server/public/app/claude-api-settings-ui.js +362 -0
- package/server/public/app/claude-billing-toggle-ui.js +224 -0
- package/server/public/app/claude-billing-toggle.mjs +112 -0
- package/server/public/app/context-tier-options.mjs +6 -2
- package/server/public/app/context-usage-view.mjs +1 -1
- package/server/public/app/conversation-provider-indicator.mjs +11 -0
- package/server/public/app/conversation-view.js +29 -3
- package/server/public/app/inbox-view.mjs +1 -1
- package/server/public/app/journal-view.js +127 -10
- package/server/public/app/model-selector-options.mjs +17 -1
- package/server/public/app/new-conversation-model-choice.mjs +21 -0
- package/server/public/app/provider-readiness.mjs +5 -2
- package/server/public/app/relay-error-ctas.mjs +50 -0
- package/server/public/app/relay-schedules-ui.js +1 -1
- package/server/public/app/settings-modal.js +11 -0
- package/server/public/app/settings-tabs.js +1 -1
- package/server/public/app/socket-handlers.js +18 -1
- package/server/public/app/tool-activity-emoji.mjs +4 -3
- package/server/public/app/usage-limit-ui.js +136 -5
- package/server/public/index.html +189 -3
- package/server/relay-heap-guard.mjs +338 -0
- package/server/relay-self-restart.mjs +33 -7
- package/server/relay-tools.md +6 -6
- package/server/repositories/conversation-group-repository.mjs +1 -1
- package/server/repositories/message-repository.mjs +7 -0
- package/server/repositories/session-repository.mjs +8 -1
- package/server/routes/ask-user-routes.mjs +1 -1
- package/server/routes/conversation-group-routes.mjs +1 -1
- package/server/routes/messages-routes.mjs +678 -31
- package/server/routes/relay-ask-routes.mjs +3 -3
- package/server/routes/relay-event-routes.mjs +2 -2
- package/server/routes/relay-schedule-routes.mjs +1 -1
- package/server/routes/remote-relay-routes.mjs +3 -3
- package/server/routes/sessions-routes.mjs +365 -53
- package/server/server-runtime.mjs +373 -26
- package/server/services/about-service.mjs +1 -0
- package/server/services/background-work-view.mjs +2 -1
- package/server/services/claude-api-settings-service.mjs +575 -0
- package/server/services/claude-api-usage-service.mjs +361 -0
- package/server/services/claude-billing-swap-service.mjs +274 -0
- package/server/services/claude-billing-switch-service.mjs +343 -0
- package/server/services/plan-usage-claude-api.mjs +445 -0
- package/server/services/plan-usage-service.mjs +5 -0
- package/server/services/provider-model-selection.mjs +3 -2
- package/server/services/provider-reasoning-effort.mjs +9 -0
- package/server/services/relay-ask-service.mjs +1 -1
- package/server/services/relay-event-service.mjs +3 -2
- package/server/services/relay-schedule-service.mjs +1 -1
- package/server/services/remote-relay-caller-context.mjs +7 -0
- package/server/services/remote-relay-dispatcher.mjs +111 -17
- package/server/services/remote-relay-registry-service.mjs +1 -1
- package/server/services/session-worker-launch-service.mjs +61 -2
- package/server/services/session-worker-registry-service.mjs +28 -1
- package/server/services/session-worker-websocket-service.mjs +17 -0
- package/server/services/usage-limit-pause-service.mjs +55 -4
- package/server/services/worker-secret-redaction.mjs +2 -0
- package/shared/anthropic-api.mjs +202 -0
- package/shared/provider-routing.mjs +5 -3
- package/shared/relay-asks.mjs +11 -5
- package/shared/relay-events.mjs +2 -2
- package/shared/relay-schedules.mjs +2 -2
- package/shared/remote-relay-contract.mjs +54 -12
- package/shared/remote-relay-tool-core.mjs +23 -6
- package/shared/steer-settle-failure.mjs +1 -0
- package/shared/worker-runtime/api-client.mjs +2 -2
- package/shared/worker-runtime/heartbeat.mjs +5 -0
- 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
|
|
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
|
-
|
|
106
|
-
|
|
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
|
-
|
|
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
|
-
|
|
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
|
-
|
|
172
|
-
|
|
197
|
+
signals.off('SIGINT', handleSigInt);
|
|
198
|
+
signals.off('SIGTERM', handleSigTerm);
|
|
173
199
|
}
|
|
174
200
|
}
|
|
175
201
|
}
|
package/server/relay-tools.md
CHANGED
|
@@ -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 `
|
|
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 `
|
|
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 `
|
|
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 `
|
|
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
|
|
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 (
|
|
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
|
|
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
|