@oar-sh/oar 0.9.16 → 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.
- package/.github/extensions/web-relay/polling/polling-loop.mjs +14 -87
- package/.github/extensions/web-relay/skills/tool-activity.mjs +5 -4
- package/CHANGELOG.md +412 -0
- package/README.md +15 -13
- package/package.json +2 -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-attachments.mjs +7 -4
- package/server/claude-worker/claude-cli-pinned-env.mjs +150 -0
- package/server/claude-worker/claude-remote-relay-tool.mjs +3 -3
- package/server/claude-worker/claude-sdk-adapter.mjs +199 -15
- package/server/claude-worker/claude-session-process.mjs +276 -33
- package/server/claude-worker/claude-session-worker-main.mjs +269 -0
- package/server/claude-worker/claude-session-worker.mjs +2 -253
- package/server/claude-worker/claude-subscription-key-check.mjs +64 -0
- package/server/claude-worker/claude-turn-failure.mjs +8 -0
- package/server/claude-worker/claude-turn-publisher.mjs +394 -13
- package/server/claude-worker/claude-usage-totals.mjs +161 -0
- package/server/claude-worker/sdk-message-normalizer.mjs +56 -1
- package/server/copilot-worker/copilot-remote-relay-tool.mjs +2 -2
- package/server/copilot-worker/copilot-sdk-adapter.mjs +18 -4
- package/server/copilot-worker/copilot-sdk-event-normalizer.mjs +1 -1
- package/server/copilot-worker/copilot-sdk-session-process.mjs +80 -14
- 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 +96 -1
- package/server/deepseek-worker/deepseek-claude-backend.mjs +317 -0
- package/server/deepseek-worker/deepseek-session-worker.mjs +37 -0
- 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/0024-deepseek-usage-totals.mjs +151 -0
- package/server/migrations/0025-deepseek-usage-requests-daily.mjs +179 -0
- package/server/migrations/0026-deepseek-usage-served-models.mjs +222 -0
- package/server/migrations/0027-deepseek-usage-checkpoint-runs.mjs +101 -0
- package/server/migrations/0028-deepseek-usage-checkpoint-request-runs.mjs +99 -0
- package/server/migrations/0029-deepseek-balance-rises.mjs +99 -0
- package/server/migrations/0030-deepseek-usage-actuals.mjs +123 -0
- 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 +105 -0
- package/server/public/app/bootstrap.js +243 -29
- 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 +10 -2
- package/server/public/app/context-usage-view.mjs +1 -1
- package/server/public/app/conversation-provider-indicator.mjs +18 -0
- package/server/public/app/conversation-view.js +31 -3
- package/server/public/app/deepseek-settings-ui.js +342 -0
- package/server/public/app/inbox-ui.js +14 -3
- package/server/public/app/inbox-view.mjs +1 -1
- package/server/public/app/journal-view.js +156 -11
- package/server/public/app/model-selector-options.mjs +28 -1
- package/server/public/app/model-vision-support.mjs +2 -0
- package/server/public/app/new-conversation-model-choice.mjs +21 -0
- package/server/public/app/no-conversation-state.mjs +1 -1
- package/server/public/app/plan-usage-view.mjs +165 -13
- package/server/public/app/provider-readiness.mjs +8 -2
- package/server/public/app/relay-error-ctas.mjs +58 -0
- package/server/public/app/relay-schedules-ui.js +1 -1
- package/server/public/app/settings-modal.js +22 -0
- package/server/public/app/settings-tabs.js +1 -1
- package/server/public/app/socket-handlers.js +28 -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 +596 -35
- package/server/relay-tools.md +4 -4
- package/server/repositories/conversation-group-repository.mjs +14 -7
- 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 +3 -2
- package/server/routes/deepseek-usage-import-routes.mjs +72 -0
- package/server/routes/messages-routes.mjs +831 -36
- 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 +670 -63
- package/server/server-runtime.mjs +476 -26
- package/server/services/about-service.mjs +2 -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/claude-session-leftovers.mjs +226 -0
- package/server/services/deepseek-balance-service.mjs +102 -0
- package/server/services/deepseek-settings-service.mjs +380 -0
- package/server/services/deepseek-usage-import.mjs +471 -0
- package/server/services/model-variant-catalog-service.mjs +1 -0
- package/server/services/plan-usage-claude-api.mjs +445 -0
- package/server/services/plan-usage-contract.mjs +100 -5
- package/server/services/plan-usage-deepseek.mjs +931 -0
- package/server/services/plan-usage-service.mjs +436 -0
- package/server/services/provider-model-selection.mjs +4 -2
- package/server/services/provider-reasoning-effort.mjs +24 -2
- 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 +84 -14
- package/server/services/remote-relay-registry-service.mjs +1 -1
- package/server/services/session-worker-launch-service.mjs +294 -84
- 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 +320 -0
- package/shared/anthropic-api.mjs +202 -0
- package/shared/context-window-fallbacks.mjs +3 -0
- package/shared/deepseek-api.mjs +137 -0
- package/shared/deepseek-models.mjs +114 -0
- package/shared/deepseek-pricing.mjs +146 -0
- package/shared/host-claude-session-env.mjs +101 -0
- package/shared/model-id.mjs +7 -0
- package/shared/preview-tool-core.mjs +1 -1
- 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 +29 -9
- package/shared/remote-relay-tool-core.mjs +23 -6
- package/shared/steer-settle-failure.mjs +2 -0
- package/shared/worker-runtime/api-client.mjs +2 -2
- package/shared/worker-runtime/direct-openai-image.mjs +128 -0
- package/shared/worker-runtime/heartbeat.mjs +5 -0
- package/shared/worker-runtime/worker-log-file.mjs +60 -0
- package/shared/worker-runtime/worker-websocket-link.mjs +6 -0
|
@@ -0,0 +1,150 @@
|
|
|
1
|
+
// The CLI's routing and trust variables a chat folder's settings must not
|
|
2
|
+
// set, shared by the backends that run Claude Code on a key of the relay's
|
|
3
|
+
// (DeepSeek, Claude API): each pins them in its live flag settings. And the
|
|
4
|
+
// settings both spawn the CLI with: no hooks (CLAUDE_CLI_KEY_CHAT_SETTINGS).
|
|
5
|
+
//
|
|
6
|
+
// The list comes from the bundled CLI and only from it. On every SDK or CLI
|
|
7
|
+
// bump, re-run both proofs (scripts/deepseek-cli-proof.mjs,
|
|
8
|
+
// scripts/claude-api-cli-proof.mjs) and the env-name grep over that binary
|
|
9
|
+
// (fixed names, grep -a, under timeout and nice; never a regex over the
|
|
10
|
+
// whole binary), and update the list and its source line below.
|
|
11
|
+
|
|
12
|
+
/**
|
|
13
|
+
* Every variable through which the env of the chat folder's settings
|
|
14
|
+
* (.claude/settings.json, settings.local.json) could send the CLI's requests,
|
|
15
|
+
* and with them the key, somewhere else, or change whom the CLI trusts on the
|
|
16
|
+
* way: the endpoints, the provider switches, the credentials, the proxies and
|
|
17
|
+
* the TLS trust. The DeepSeek and the Claude API backends pin each one in
|
|
18
|
+
* their live flag settings (liveFlagSettings), which the CLI applies over the
|
|
19
|
+
* folder's, to the value the CLI was spawned with: the worker's own, else the
|
|
20
|
+
* other case's for a proxy variable, else ''.
|
|
21
|
+
* '' is unset to the CLI for each of these (truthy or trimmed reads), except
|
|
22
|
+
* SSL_CERT_FILE and SSL_CERT_DIR: '' empties OpenSSL's and Node's system
|
|
23
|
+
* store, so those two are pinned only when the worker sets them.
|
|
24
|
+
*
|
|
25
|
+
* Source: the names Claude Code 2.1.304 (the bundled CLI) reads, by a grep
|
|
26
|
+
* of the binary; the CLI's own lists of endpoints (with their selection
|
|
27
|
+
* switches and companions) and credentials are in it as they are there. A
|
|
28
|
+
* deny-list, because a settings layer can only pin names known in advance.
|
|
29
|
+
* Left out, each for its reason: the model variables (same endpoint, same
|
|
30
|
+
* key), what the CLI already drops from a folder's settings or from all
|
|
31
|
+
* settings (CLAUDE_CONFIG_DIR, CLAUDE_CODE_CUSTOM_OAUTH_URL, the OTEL
|
|
32
|
+
* exporter endpoints, ANTHROPIC_UNIX_SOCKET, the host-managed provider
|
|
33
|
+
* switches, the file-descriptor credentials), a provider's own SDK settings
|
|
34
|
+
* (AWS_*, GOOGLE_*, AZURE_*, read only for the provider the switches here
|
|
35
|
+
* turn off), NODE_OPTIONS (at runtime the CLI reads only its system-CA
|
|
36
|
+
* flags, which add no CA), and what routes only other programs' traffic
|
|
37
|
+
* (NPM_CONFIG_*, GIT_*, the resolver's variables).
|
|
38
|
+
*/
|
|
39
|
+
export const CLAUDE_CLI_PINNED_ENV = Object.freeze([
|
|
40
|
+
// Endpoints: the CLI's list, its fallback and the headers sent with them.
|
|
41
|
+
'ANTHROPIC_BASE_URL',
|
|
42
|
+
'_CLAUDE_CODE_ASSUME_FIRST_PARTY_BASE_URL',
|
|
43
|
+
'ANTHROPIC_CUSTOM_HEADERS',
|
|
44
|
+
'ANTHROPIC_BEDROCK_BASE_URL',
|
|
45
|
+
'ANTHROPIC_VERTEX_BASE_URL',
|
|
46
|
+
'ANTHROPIC_FOUNDRY_BASE_URL',
|
|
47
|
+
'ANTHROPIC_AWS_BASE_URL',
|
|
48
|
+
'ANTHROPIC_GOOGLE_CLOUD_BASE_URL',
|
|
49
|
+
'ANTHROPIC_BEDROCK_MANTLE_BASE_URL',
|
|
50
|
+
'CLAUDE_CODE_API_BASE_URL',
|
|
51
|
+
'CLAUDE_CODE_ARTIFACTS_API_BASE_URL',
|
|
52
|
+
'CLAUDE_CODE_ARTIFACTS_API_TOKEN',
|
|
53
|
+
'CLAUDE_CODE_ARTIFACT_ASSET_BASE_URL',
|
|
54
|
+
'CLAUDE_CODE_ARTIFACT_LIVE_BASE_URL',
|
|
55
|
+
'CLAUDE_CODE_ARTIFACT_SYNC_BASE_URL',
|
|
56
|
+
'CLAUDE_CODE_ARTIFACT_VIEWER_BASE_URL',
|
|
57
|
+
'CLAUDE_CODE_MEMORY_API_BASE_URL',
|
|
58
|
+
'CLAUDE_CODE_MEMORY_API_TOKEN',
|
|
59
|
+
// Provider switches.
|
|
60
|
+
'CLAUDE_CODE_USE_BEDROCK',
|
|
61
|
+
'CLAUDE_CODE_USE_VERTEX',
|
|
62
|
+
'CLAUDE_CODE_USE_FOUNDRY',
|
|
63
|
+
'CLAUDE_CODE_USE_ANTHROPIC_AWS',
|
|
64
|
+
'CLAUDE_CODE_USE_ANTHROPIC_GOOGLE_CLOUD',
|
|
65
|
+
'CLAUDE_CODE_USE_MANTLE',
|
|
66
|
+
'CLAUDE_CODE_USE_GATEWAY',
|
|
67
|
+
'CLAUDE_CODE_SKIP_BEDROCK_AUTH',
|
|
68
|
+
'CLAUDE_CODE_SKIP_VERTEX_AUTH',
|
|
69
|
+
'CLAUDE_CODE_SKIP_FOUNDRY_AUTH',
|
|
70
|
+
'CLAUDE_CODE_SKIP_ANTHROPIC_AWS_AUTH',
|
|
71
|
+
'CLAUDE_CODE_SKIP_ANTHROPIC_GOOGLE_CLOUD_AUTH',
|
|
72
|
+
'CLAUDE_CODE_SKIP_MANTLE_AUTH',
|
|
73
|
+
// Credentials: the CLI's list, the OAuth login, the federation profile and
|
|
74
|
+
// the proxy credential helper.
|
|
75
|
+
'ANTHROPIC_API_KEY',
|
|
76
|
+
'ANTHROPIC_AUTH_TOKEN',
|
|
77
|
+
'CLAUDE_CODE_OAUTH_TOKEN',
|
|
78
|
+
'AWS_BEARER_TOKEN_BEDROCK',
|
|
79
|
+
'ANTHROPIC_FOUNDRY_API_KEY',
|
|
80
|
+
'ANTHROPIC_FOUNDRY_AUTH_TOKEN',
|
|
81
|
+
'ANTHROPIC_AWS_API_KEY',
|
|
82
|
+
'CLAUDE_CODE_OAUTH_REFRESH_TOKEN',
|
|
83
|
+
'CLAUDE_CODE_OAUTH_SCOPES',
|
|
84
|
+
'CLAUDE_CODE_OAUTH_CLIENT_ID',
|
|
85
|
+
'ANTHROPIC_PROFILE',
|
|
86
|
+
'ANTHROPIC_IDENTITY_TOKEN',
|
|
87
|
+
'ANTHROPIC_IDENTITY_TOKEN_FILE',
|
|
88
|
+
'ANTHROPIC_FEDERATION_RULE_ID',
|
|
89
|
+
'ANTHROPIC_ORGANIZATION_ID',
|
|
90
|
+
'ANTHROPIC_SERVICE_ACCOUNT_ID',
|
|
91
|
+
'ANTHROPIC_SCOPE',
|
|
92
|
+
'ANTHROPIC_WORKSPACE_ID',
|
|
93
|
+
'CLAUDE_CODE_ENABLE_PROXY_AUTH_HELPER',
|
|
94
|
+
// Proxies: the CLI reads both cases, the lower one first.
|
|
95
|
+
'HTTPS_PROXY',
|
|
96
|
+
'https_proxy',
|
|
97
|
+
'HTTP_PROXY',
|
|
98
|
+
'http_proxy',
|
|
99
|
+
'ALL_PROXY',
|
|
100
|
+
'all_proxy',
|
|
101
|
+
'NO_PROXY',
|
|
102
|
+
'no_proxy',
|
|
103
|
+
'CLAUDE_CODE_HTTPS_PROXY',
|
|
104
|
+
'CLAUDE_CODE_HTTP_PROXY',
|
|
105
|
+
// TLS trust and the client certificate.
|
|
106
|
+
'NODE_TLS_REJECT_UNAUTHORIZED',
|
|
107
|
+
'NODE_EXTRA_CA_CERTS',
|
|
108
|
+
'CLAUDE_CODE_CERT_STORE',
|
|
109
|
+
'CLAUDE_CODE_CLIENT_CERT',
|
|
110
|
+
'CLAUDE_CODE_CLIENT_KEY',
|
|
111
|
+
'CLAUDE_CODE_CLIENT_KEY_PASSPHRASE',
|
|
112
|
+
'SSL_CERT_FILE',
|
|
113
|
+
'SSL_CERT_DIR',
|
|
114
|
+
]);
|
|
115
|
+
// The two of CLAUDE_CLI_PINNED_ENV for which '' is not unset (see there).
|
|
116
|
+
const PINNED_ONLY_WHEN_SET = new Set(['SSL_CERT_FILE', 'SSL_CERT_DIR']);
|
|
117
|
+
// The proxy variables the CLI and the tools it runs read in either case.
|
|
118
|
+
const OTHER_CASE = new Map(
|
|
119
|
+
['HTTPS_PROXY', 'HTTP_PROXY', 'ALL_PROXY', 'NO_PROXY'].flatMap((name) => [
|
|
120
|
+
[name, name.toLowerCase()],
|
|
121
|
+
[name.toLowerCase(), name],
|
|
122
|
+
]),
|
|
123
|
+
);
|
|
124
|
+
|
|
125
|
+
/**
|
|
126
|
+
* The settings both backends spawn every CLI with, on its command line (their
|
|
127
|
+
* flagSettings): no hooks and no statusLine command, neither the user's nor
|
|
128
|
+
* the chat folder's nor a plugin's. Those run as the relay's user with the
|
|
129
|
+
* CLI's environment, the key in it, and a folder's SessionStart hook would run
|
|
130
|
+
* before the live pins land. The SDK has the spawn settings in force before
|
|
131
|
+
* any hook (sdk.d.ts, Options.settings; scripts/claude-cli-hooks-proof.mjs
|
|
132
|
+
* shows it). Hooks in managed settings still run: those are the host admin's.
|
|
133
|
+
* OAR's Claude worker registers no SDK hooks of its own.
|
|
134
|
+
*/
|
|
135
|
+
export const CLAUDE_CLI_KEY_CHAT_SETTINGS = Object.freeze({ disableAllHooks: true });
|
|
136
|
+
|
|
137
|
+
/**
|
|
138
|
+
* The pins of CLAUDE_CLI_PINNED_ENV for a CLI spawned with `cliEnv`: the
|
|
139
|
+
* value it was spawned with, else the other case's (a proxy the worker sets
|
|
140
|
+
* as https_proxy holds for HTTPS_PROXY as well), else ''.
|
|
141
|
+
*/
|
|
142
|
+
export function claudeCliPinnedEnv(cliEnv = {}) {
|
|
143
|
+
const pins = {};
|
|
144
|
+
for (const name of CLAUDE_CLI_PINNED_ENV) {
|
|
145
|
+
const own = cliEnv[name] ?? cliEnv[OTHER_CASE.get(name)];
|
|
146
|
+
if (own !== undefined) pins[name] = String(own);
|
|
147
|
+
else if (!PINNED_ONLY_WHEN_SET.has(name)) pins[name] = '';
|
|
148
|
+
}
|
|
149
|
+
return pins;
|
|
150
|
+
}
|
|
@@ -1,8 +1,8 @@
|
|
|
1
1
|
/**
|
|
2
|
-
* The relay's `
|
|
2
|
+
* The relay's `relay_tool` tool for the Claude worker, shaped as an SDK MCP
|
|
3
3
|
* tool definition (`{ name, description, inputSchema, handler }`) so the
|
|
4
4
|
* adapter can hand it to `createSdkMcpServer` next to the preview tool. On the
|
|
5
|
-
* wire it is `
|
|
5
|
+
* wire it is `mcp__relay__relay_tool`; the module never imports the SDK.
|
|
6
6
|
*/
|
|
7
7
|
|
|
8
8
|
import * as z from 'zod';
|
|
@@ -140,7 +140,7 @@ export function createRemoteRelayToolDefinition({ api, getConversationId = () =>
|
|
|
140
140
|
// A tool exception would surface as a failed MCP call and can abort
|
|
141
141
|
// the turn; a refusal the model can read cannot.
|
|
142
142
|
const message = error?.message || String(error);
|
|
143
|
-
dbg('
|
|
143
|
+
dbg('relay_tool failed', message);
|
|
144
144
|
result = { ok: false, code: REMOTE_RELAY_CALL_FAILED, error: message };
|
|
145
145
|
}
|
|
146
146
|
return { content: [{ type: 'text', text: formatRemoteRelayToolResult(result) }] };
|
|
@@ -6,9 +6,12 @@ import { MEDIA_EMBED_INSTRUCTION_TEXT } from '../../shared/media-embed-instructi
|
|
|
6
6
|
import { REMOTE_RELAY_TOOL_CALL_TIMEOUT_MS } from '../../shared/remote-relay-tool-core.mjs';
|
|
7
7
|
import { createPreviewToolDefinition } from './claude-preview-tool.mjs';
|
|
8
8
|
import { createRemoteRelayToolDefinition } from './claude-remote-relay-tool.mjs';
|
|
9
|
+
import { CLAUDE_LIVE_FLAG_SETTINGS_CODE, CLAUDE_LIVE_FLAG_SETTINGS_GUIDANCE } from './claude-turn-failure.mjs';
|
|
10
|
+
|
|
11
|
+
export { CLAUDE_LIVE_FLAG_SETTINGS_CODE };
|
|
9
12
|
|
|
10
13
|
// The name the in-process MCP server is registered under; it prefixes every
|
|
11
|
-
// tool it carries on the wire (`mcp__relay__preview`, `
|
|
14
|
+
// tool it carries on the wire (`mcp__relay__preview`, `mcp__relay__relay_tool`).
|
|
12
15
|
export const RELAY_MCP_SERVER_NAME = 'relay';
|
|
13
16
|
|
|
14
17
|
// The per-server tool-call timeout the CLI applies to the relay's tools.
|
|
@@ -16,7 +19,7 @@ export const RELAY_MCP_SERVER_NAME = 'relay';
|
|
|
16
19
|
// value, else by the MCP_TOOL_TIMEOUT environment variable, else by 1e8 ms —
|
|
17
20
|
// and SDK servers are exempt from the idle timeout. Pinning it here keeps a
|
|
18
21
|
// host-wide MCP_TOOL_TIMEOUT (set for some other server) from cutting off a
|
|
19
|
-
//
|
|
22
|
+
// relay_tool wait: those run up to wait_seconds (≤ 600 s) plus however long
|
|
20
23
|
// an approval card sits unanswered.
|
|
21
24
|
export const RELAY_MCP_TOOL_TIMEOUT_MS = REMOTE_RELAY_TOOL_CALL_TIMEOUT_MS;
|
|
22
25
|
|
|
@@ -119,6 +122,23 @@ export function claudeSpawnSettings({ ultracode = false, autoCompactWindow = nul
|
|
|
119
122
|
return Object.keys(settings).length ? settings : null;
|
|
120
123
|
}
|
|
121
124
|
|
|
125
|
+
/**
|
|
126
|
+
* The spawn's `settings` with a backend's flag settings on top: null when
|
|
127
|
+
* neither has anything, the relay's object untouched when the backend has
|
|
128
|
+
* nothing to add. `env` is merged by name, so neither side drops the other's
|
|
129
|
+
* variables.
|
|
130
|
+
*/
|
|
131
|
+
export function mergeSpawnSettings(spawnSettings, backendSettings) {
|
|
132
|
+
const own = spawnSettings && typeof spawnSettings === 'object' ? spawnSettings : null;
|
|
133
|
+
const extra = backendSettings && typeof backendSettings === 'object' && Object.keys(backendSettings).length
|
|
134
|
+
? backendSettings
|
|
135
|
+
: null;
|
|
136
|
+
if (!extra) return own;
|
|
137
|
+
const merged = { ...(own || {}), ...extra };
|
|
138
|
+
if (own?.env || extra.env) merged.env = { ...(own?.env || {}), ...(extra.env || {}) };
|
|
139
|
+
return merged;
|
|
140
|
+
}
|
|
141
|
+
|
|
122
142
|
/**
|
|
123
143
|
* Live twin of the attribution spawn setting, for a value (OAR or off). A
|
|
124
144
|
* running CLI takes a new text through the flag layer (it arrives as a fresh
|
|
@@ -206,7 +226,7 @@ export function systemPromptForRelayMode(relayMode, { backgroundCommandLimitMs =
|
|
|
206
226
|
* payload. `getConversationId` is called per tool call because a process
|
|
207
227
|
* outlives the turn it was spawned for.
|
|
208
228
|
*
|
|
209
|
-
* `
|
|
229
|
+
* `relay_tool` is the exception: it rides along only when the worker found
|
|
210
230
|
* at least one paired remote (`remoteRelayTool`), because a tool that can do
|
|
211
231
|
* nothing on a relay without remotes would only cost context on every turn.
|
|
212
232
|
*/
|
|
@@ -229,14 +249,25 @@ export function createRelayMcpServer({ api, getConversationId, dbg, remoteRelayT
|
|
|
229
249
|
* emitted, which also closes the control transport (canUseTool, context
|
|
230
250
|
* usage), so the session process keeps this open while background tasks or
|
|
231
251
|
* queued continuations still need the process alive.
|
|
252
|
+
*
|
|
253
|
+
* `held`: nothing is sent until `release()` (the messages pushed meanwhile
|
|
254
|
+
* wait in order). `fail(error)` instead makes the stream throw it, which the
|
|
255
|
+
* SDK answers by aborting the query; nothing held is ever sent, and neither
|
|
256
|
+
* is it when the stream ends while still held.
|
|
232
257
|
*/
|
|
233
|
-
function createPushableUserMessageStream() {
|
|
258
|
+
function createPushableUserMessageStream({ held = false } = {}) {
|
|
234
259
|
const queued = [];
|
|
235
260
|
let wake = null;
|
|
236
261
|
let ended = false;
|
|
262
|
+
let holding = held;
|
|
263
|
+
let failure = null;
|
|
237
264
|
async function* stream() {
|
|
238
265
|
for (;;) {
|
|
239
|
-
|
|
266
|
+
if (failure) throw failure;
|
|
267
|
+
if (!holding) {
|
|
268
|
+
while (queued.length && !failure) yield queued.shift();
|
|
269
|
+
if (failure) continue;
|
|
270
|
+
}
|
|
240
271
|
if (ended) return;
|
|
241
272
|
await new Promise((resolve) => { wake = resolve; });
|
|
242
273
|
wake = null;
|
|
@@ -257,9 +288,80 @@ function createPushableUserMessageStream() {
|
|
|
257
288
|
ended = true;
|
|
258
289
|
wake?.();
|
|
259
290
|
},
|
|
291
|
+
release() {
|
|
292
|
+
holding = false;
|
|
293
|
+
wake?.();
|
|
294
|
+
},
|
|
295
|
+
fail(error) {
|
|
296
|
+
failure = error;
|
|
297
|
+
wake?.();
|
|
298
|
+
},
|
|
260
299
|
};
|
|
261
300
|
}
|
|
262
301
|
|
|
302
|
+
/** Every string in a settings object, for taking them out of a text. */
|
|
303
|
+
function settingsStrings(value, found = []) {
|
|
304
|
+
if (typeof value === 'string') {
|
|
305
|
+
if (value) found.push(value);
|
|
306
|
+
} else if (value && typeof value === 'object') {
|
|
307
|
+
for (const item of Object.values(value)) settingsStrings(item, found);
|
|
308
|
+
}
|
|
309
|
+
return found;
|
|
310
|
+
}
|
|
311
|
+
|
|
312
|
+
/**
|
|
313
|
+
* The blanked keys of a backend's live flag settings (top-level and `env`
|
|
314
|
+
* values that are ''), for the spawn's `settings` too: an empty value holds
|
|
315
|
+
* no secret, and the CLI acts on some settings at startup, before the live
|
|
316
|
+
* ones land (it runs a folder's apiKeyHelper then; CLI 2.1.304,
|
|
317
|
+
* scripts/deepseek-cli-proof.mjs). Null when there is none.
|
|
318
|
+
*/
|
|
319
|
+
function liveSettingsBlanks(live) {
|
|
320
|
+
if (!live) return null;
|
|
321
|
+
const blanks = {};
|
|
322
|
+
for (const [key, value] of Object.entries(live)) {
|
|
323
|
+
if (value === '') blanks[key] = '';
|
|
324
|
+
}
|
|
325
|
+
const env = Object.entries(live.env && typeof live.env === 'object' ? live.env : {}).filter(([, value]) => value === '');
|
|
326
|
+
if (env.length) blanks.env = Object.fromEntries(env);
|
|
327
|
+
return Object.keys(blanks).length ? blanks : null;
|
|
328
|
+
}
|
|
329
|
+
|
|
330
|
+
/** The spawn's `settings` with the live settings' blanks on top. */
|
|
331
|
+
function withLiveSettingsBlanks(spawnSettings, live) {
|
|
332
|
+
const blanks = liveSettingsBlanks(live);
|
|
333
|
+
if (!blanks) return spawnSettings;
|
|
334
|
+
const settings = { ...(spawnSettings || {}), ...blanks };
|
|
335
|
+
if (spawnSettings?.env || blanks.env) settings.env = { ...(spawnSettings?.env || {}), ...(blanks.env || {}) };
|
|
336
|
+
return settings;
|
|
337
|
+
}
|
|
338
|
+
|
|
339
|
+
/**
|
|
340
|
+
* How long the CLI may take to answer a backend's live flag settings (ms).
|
|
341
|
+
* It answers in well under a second (CLI 2.1.304); one that never does fails
|
|
342
|
+
* the turn like a refusal, and the held message is never sent.
|
|
343
|
+
*/
|
|
344
|
+
export const LIVE_FLAG_SETTINGS_TIMEOUT_MS = 60_000;
|
|
345
|
+
|
|
346
|
+
/**
|
|
347
|
+
* The Error a spawn fails with when the CLI refused its live flag settings.
|
|
348
|
+
* The message names no value; `detail` is the CLI's own words with every
|
|
349
|
+
* value of the settings taken out.
|
|
350
|
+
*/
|
|
351
|
+
function liveFlagSettingsError(cause, settings, label = '') {
|
|
352
|
+
const error = new Error(
|
|
353
|
+
`Claude Code did not take the ${label ? `${label} ` : ''}connection settings before the first message, so nothing was sent.`,
|
|
354
|
+
);
|
|
355
|
+
error.code = CLAUDE_LIVE_FLAG_SETTINGS_CODE;
|
|
356
|
+
error.guidance = CLAUDE_LIVE_FLAG_SETTINGS_GUIDANCE;
|
|
357
|
+
let detail = String(cause?.message || cause || '');
|
|
358
|
+
for (const value of settingsStrings(settings).sort((a, b) => b.length - a.length)) {
|
|
359
|
+
detail = detail.split(value).join('[redacted]');
|
|
360
|
+
}
|
|
361
|
+
error.detail = detail.slice(0, 200);
|
|
362
|
+
return error;
|
|
363
|
+
}
|
|
364
|
+
|
|
263
365
|
/**
|
|
264
366
|
* The only module that imports `@anthropic-ai/claude-agent-sdk`.
|
|
265
367
|
*
|
|
@@ -290,37 +392,75 @@ export function startClaudeSession({
|
|
|
290
392
|
pathToClaudeCodeExecutable = '',
|
|
291
393
|
api = null,
|
|
292
394
|
getConversationId = () => '',
|
|
293
|
-
// Whether this process carries `
|
|
395
|
+
// Whether this process carries `relay_tool` (the worker's registration
|
|
294
396
|
// decision). Fixed for the process: the tool list is part of the spawn.
|
|
295
397
|
remoteRelayTool = false,
|
|
398
|
+
// Another endpoint for the CLI (null = Anthropic through the host's Claude
|
|
399
|
+
// login): it builds the CLI's environment and maps model and effort into
|
|
400
|
+
// its terms (../deepseek-worker/deepseek-claude-backend.mjs).
|
|
401
|
+
backend = null,
|
|
402
|
+
liveFlagSettingsTimeoutMs = LIVE_FLAG_SETTINGS_TIMEOUT_MS,
|
|
296
403
|
queryImpl = query,
|
|
297
404
|
dbg = () => {},
|
|
298
405
|
} = {}) {
|
|
299
406
|
const normalizedModel = String(model || '').trim().toLowerCase();
|
|
300
|
-
|
|
407
|
+
// A backend on Claude's own models (the Claude API backend) takes the
|
|
408
|
+
// model and the effort exactly as Claude does; only another provider's
|
|
409
|
+
// backend maps them into its terms.
|
|
410
|
+
const modelBackend = backend && backend.claudeModels !== true ? backend : null;
|
|
411
|
+
const effort = modelBackend ? '' : normalizeClaudeEffort(reasoningEffort);
|
|
301
412
|
const ultracode = effort === CLAUDE_ULTRACODE_EFFORT;
|
|
302
|
-
const effortOption = ultracode ? 'xhigh' : effort;
|
|
413
|
+
const effortOption = modelBackend ? modelBackend.effortOption(reasoningEffort) : (ultracode ? 'xhigh' : effort);
|
|
414
|
+
const cliModel = modelBackend ? modelBackend.cliModel(model) : String(model || '').trim();
|
|
303
415
|
// Spawn-time twin of the flag-settings helpers: the settings layer is the
|
|
304
416
|
// only way to hand a fresh CLI the session-scoped ultracode flag, the
|
|
305
417
|
// auto-compact window and the thinking pin, and all three share one
|
|
306
|
-
// `settings` object.
|
|
307
|
-
|
|
418
|
+
// `settings` object. A backend's own pins (no hooks for both, the Claude
|
|
419
|
+
// API backend's endpoint and blanked credentials) join them; theirs win a
|
|
420
|
+
// clash.
|
|
421
|
+
const spawnSettings = mergeSpawnSettings(
|
|
422
|
+
claudeSpawnSettings({ ultracode, autoCompactWindow, thinkingEnabled, attribution }),
|
|
423
|
+
typeof backend?.flagSettings === 'function' ? backend.flagSettings() : null,
|
|
424
|
+
);
|
|
308
425
|
// Relay tools need the worker's authenticated API helper; a caller that
|
|
309
426
|
// passes none (tests, probes) gets a session without them.
|
|
310
427
|
const relayMcpServer = typeof api === 'function'
|
|
311
428
|
? createRelayMcpServer({ api, getConversationId, dbg, remoteRelayTool: remoteRelayTool === true })
|
|
312
429
|
: null;
|
|
313
|
-
|
|
430
|
+
// A backend's CLI always gets an environment of its own: the worker's
|
|
431
|
+
// carries the backend's secret and maybe the host's Anthropic settings.
|
|
432
|
+
// The model goes in as well: the backend pins Claude Code's model aliases to
|
|
433
|
+
// it, so a sub-agent asked for as "opus" runs on the session's model. The
|
|
434
|
+
// Bash limits go on top: the backend drops a host session's Bash cap, not
|
|
435
|
+
// the relay's own.
|
|
436
|
+
const backendEnv = backend ? backend.spawnEnv(process.env, { effort: reasoningEffort, model }) : null;
|
|
437
|
+
const limitEnv = claudeSpawnEnv({ backgroundCommandLimitMs, baseEnv: backendEnv || process.env });
|
|
438
|
+
const env = limitEnv || backendEnv;
|
|
439
|
+
// A backend's pins that may not go on the command line (a key): `settings`
|
|
440
|
+
// below is the CLI's `--settings` argument, which other users of the host
|
|
441
|
+
// can read. These go into the same flag layer over the control channel
|
|
442
|
+
// (stdin), and no user message is sent before the CLI has taken them: the
|
|
443
|
+
// first API request of the process comes after (measured on CLI 2.1.304,
|
|
444
|
+
// the CLI proofs in scripts/). The layer outranks the user's, the
|
|
445
|
+
// project's and the local settings and holds through their reloads and
|
|
446
|
+
// through later applyFlagSettings calls of other keys. A refusal ends the
|
|
447
|
+
// process before anything is sent (CLAUDE_LIVE_FLAG_SETTINGS_CODE), and so
|
|
448
|
+
// does no answer within liveFlagSettingsTimeoutMs. Their blanks go on the
|
|
449
|
+
// command line as well, for what the CLI does at startup. They are built
|
|
450
|
+
// from the CLI's environment: a pin holds the value the CLI starts with.
|
|
451
|
+
const liveSettings = typeof backend?.liveFlagSettings === 'function' ? backend.liveFlagSettings(env || process.env) : null;
|
|
452
|
+
const live = liveSettings && typeof liveSettings === 'object' && Object.keys(liveSettings).length ? liveSettings : null;
|
|
453
|
+
const settings = withLiveSettingsBlanks(spawnSettings, live);
|
|
314
454
|
const options = {
|
|
315
455
|
cwd,
|
|
316
456
|
permissionMode: permissionModeForRelayMode(relayMode),
|
|
317
|
-
systemPrompt: systemPromptForRelayMode(relayMode, { backgroundCommandLimitMs:
|
|
457
|
+
systemPrompt: systemPromptForRelayMode(relayMode, { backgroundCommandLimitMs: limitEnv ? backgroundCommandLimitMs : null }),
|
|
318
458
|
includePartialMessages: true,
|
|
319
459
|
forwardSubagentText: true,
|
|
320
460
|
...(effortOption ? { effort: effortOption } : {}),
|
|
321
|
-
...(
|
|
461
|
+
...(settings ? { settings } : {}),
|
|
322
462
|
...(env ? { env } : {}),
|
|
323
|
-
...(normalizedModel && normalizedModel !== 'auto' ? { model:
|
|
463
|
+
...(normalizedModel && normalizedModel !== 'auto' && cliModel ? { model: cliModel } : {}),
|
|
324
464
|
...(String(resume || '').trim() ? { resume: String(resume).trim() } : {}),
|
|
325
465
|
...(abortController ? { abortController } : {}),
|
|
326
466
|
...(typeof canUseTool === 'function' ? { canUseTool } : {}),
|
|
@@ -329,8 +469,52 @@ export function startClaudeSession({
|
|
|
329
469
|
? { pathToClaudeCodeExecutable: String(pathToClaudeCodeExecutable).trim() }
|
|
330
470
|
: {}),
|
|
331
471
|
};
|
|
332
|
-
const { stream, push, end } = createPushableUserMessageStream();
|
|
472
|
+
const { stream, push, end, release, fail } = createPushableUserMessageStream({ held: Boolean(live) });
|
|
333
473
|
const turn = queryImpl({ prompt: stream, options });
|
|
474
|
+
if (live) {
|
|
475
|
+
// The wait ends with the answer, the timeout or the session's abort,
|
|
476
|
+
// whichever comes first; each one clears the others.
|
|
477
|
+
const signal = abortController?.signal || null;
|
|
478
|
+
let timer = null;
|
|
479
|
+
let onAbort = null;
|
|
480
|
+
const settled = () => {
|
|
481
|
+
clearTimeout(timer);
|
|
482
|
+
timer = null;
|
|
483
|
+
if (onAbort) signal?.removeEventListener('abort', onAbort);
|
|
484
|
+
onAbort = null;
|
|
485
|
+
};
|
|
486
|
+
const unanswered = new Promise((resolve, reject) => {
|
|
487
|
+
const waitMs = Math.max(0, Number(liveFlagSettingsTimeoutMs) || 0);
|
|
488
|
+
timer = setTimeout(() => {
|
|
489
|
+
timer = null;
|
|
490
|
+
reject(new Error(`no answer within ${Math.ceil(waitMs / 1000)} s`));
|
|
491
|
+
}, waitMs);
|
|
492
|
+
timer.unref?.();
|
|
493
|
+
if (signal) {
|
|
494
|
+
onAbort = settled;
|
|
495
|
+
if (signal.aborted) settled();
|
|
496
|
+
else signal.addEventListener('abort', onAbort, { once: true });
|
|
497
|
+
}
|
|
498
|
+
});
|
|
499
|
+
turn.liveFlagSettingsApplied = Promise.race([
|
|
500
|
+
Promise.resolve().then(() => {
|
|
501
|
+
if (typeof turn.applyFlagSettings !== 'function') throw new Error('applyFlagSettings is not available');
|
|
502
|
+
return turn.applyFlagSettings(live);
|
|
503
|
+
}),
|
|
504
|
+
unanswered,
|
|
505
|
+
])
|
|
506
|
+
.finally(settled)
|
|
507
|
+
.then(() => {
|
|
508
|
+
release();
|
|
509
|
+
return true;
|
|
510
|
+
}, (error) => {
|
|
511
|
+
const refused = liveFlagSettingsError(error, live, backend?.label || '');
|
|
512
|
+
turn.liveFlagSettingsError = refused;
|
|
513
|
+
dbg('live flag settings refused', refused.detail);
|
|
514
|
+
fail(refused);
|
|
515
|
+
return false;
|
|
516
|
+
});
|
|
517
|
+
}
|
|
334
518
|
applyThinkingDisplay(turn, thinkingDisplay, { dbg });
|
|
335
519
|
turn.pushUserMessage = push;
|
|
336
520
|
// The session process MUST call this on every teardown path or the CLI
|