@oar-sh/oar 0.9.15 → 0.9.17
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/CHANGELOG.md +305 -0
- package/README.md +12 -11
- package/package.json +2 -1
- package/server/claude-worker/claude-attachments.mjs +7 -4
- package/server/claude-worker/claude-remote-relay-tool.mjs +36 -1
- package/server/claude-worker/claude-sdk-adapter.mjs +167 -9
- package/server/claude-worker/claude-session-process.mjs +131 -33
- package/server/claude-worker/claude-session-worker-main.mjs +266 -0
- package/server/claude-worker/claude-session-worker.mjs +2 -253
- package/server/claude-worker/claude-turn-failure.mjs +8 -0
- package/server/claude-worker/claude-turn-publisher.mjs +282 -13
- package/server/claude-worker/claude-usage-totals.mjs +108 -0
- package/server/claude-worker/sdk-message-normalizer.mjs +55 -0
- package/server/copilot-worker/copilot-sdk-adapter.mjs +18 -4
- package/server/copilot-worker/copilot-sdk-session-process.mjs +73 -7
- package/server/db-schema.mjs +115 -0
- package/server/deepseek-worker/deepseek-claude-backend.mjs +433 -0
- package/server/deepseek-worker/deepseek-session-worker.mjs +37 -0
- package/server/migrations/0019-relay-event-subscription-options.mjs +106 -0
- package/server/migrations/0020-conversation-may-create-sessions.mjs +67 -0
- package/server/migrations/0021-conversation-group-agent-marks.mjs +77 -0
- package/server/migrations/0022-relay-schedules.mjs +142 -0
- package/server/migrations/0023-relay-asks.mjs +141 -0
- 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/public/app/api-client.js +53 -0
- package/server/public/app/ask-user-view.js +3 -0
- package/server/public/app/bootstrap.js +162 -16
- package/server/public/app/context-tier-options.mjs +4 -0
- package/server/public/app/context-usage-view.mjs +17 -0
- package/server/public/app/conversation-group-order.mjs +35 -0
- package/server/public/app/conversation-provider-indicator.mjs +7 -0
- package/server/public/app/conversation-view.js +2 -0
- package/server/public/app/deepseek-settings-ui.js +342 -0
- package/server/public/app/inbox-ui.js +367 -0
- package/server/public/app/inbox-view.mjs +370 -0
- package/server/public/app/journal-view.js +54 -5
- package/server/public/app/model-selector-options.mjs +11 -0
- package/server/public/app/model-vision-support.mjs +2 -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 +4 -1
- package/server/public/app/relay-error-ctas.mjs +8 -0
- package/server/public/app/relay-event-subscriptions-ui.js +4 -2
- package/server/public/app/relay-event-view.mjs +33 -7
- package/server/public/app/relay-schedule-view.mjs +119 -0
- package/server/public/app/relay-schedules-ui.js +172 -0
- 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 +37 -3
- package/server/public/app/store.js +18 -5
- package/server/public/index.html +567 -35
- package/server/relay-tools.md +18 -3
- package/server/repositories/conversation-group-repository.mjs +110 -17
- package/server/repositories/remote-relay-repository.mjs +20 -2
- package/server/repositories/session-repository.mjs +7 -1
- package/server/routes/ask-user-routes.mjs +57 -3
- package/server/routes/cache-routes.mjs +6 -0
- package/server/routes/conversation-group-routes.mjs +19 -3
- package/server/routes/deepseek-usage-import-routes.mjs +72 -0
- package/server/routes/messages-routes.mjs +195 -18
- package/server/routes/relay-ask-routes.mjs +387 -0
- package/server/routes/relay-event-routes.mjs +3 -1
- package/server/routes/relay-schedule-routes.mjs +57 -0
- package/server/routes/sessions-routes.mjs +932 -97
- package/server/server-runtime.mjs +203 -11
- package/server/services/about-service.mjs +1 -0
- package/server/services/cache-rebuild-service.mjs +19 -2
- package/server/services/claude-session-leftovers.mjs +226 -0
- package/server/services/conversation-preferences-service.mjs +17 -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-contract.mjs +100 -5
- package/server/services/plan-usage-deepseek.mjs +931 -0
- package/server/services/plan-usage-service.mjs +431 -0
- package/server/services/provider-model-selection.mjs +1 -0
- package/server/services/provider-reasoning-effort.mjs +15 -2
- package/server/services/relay-ask-link-service.mjs +174 -0
- package/server/services/relay-ask-service.mjs +604 -0
- package/server/services/relay-event-service.mjs +589 -73
- package/server/services/relay-schedule-service.mjs +596 -0
- package/server/services/remote-relay-caller-context.mjs +89 -20
- package/server/services/remote-relay-dispatcher.mjs +958 -31
- package/server/services/session-worker-launch-service.mjs +233 -82
- package/server/services/worker-secret-redaction.mjs +318 -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 +2 -2
- package/shared/relay-asks.mjs +145 -0
- package/shared/relay-events.mjs +268 -18
- package/shared/relay-schedules.mjs +128 -0
- package/shared/remote-relay-contract.mjs +554 -26
- package/shared/remote-relay-tool-core.mjs +16 -5
- package/shared/steer-settle-failure.mjs +1 -0
- package/shared/worker-runtime/direct-openai-image.mjs +128 -0
- package/shared/worker-runtime/worker-log-file.mjs +60 -0
|
@@ -162,21 +162,32 @@ export async function executeRemoteRelayTool(input, { api, conversationId = '',
|
|
|
162
162
|
}
|
|
163
163
|
const parsed = validateRemoteRelayToolInput(input);
|
|
164
164
|
if (!parsed.ok) return parsed;
|
|
165
|
-
|
|
165
|
+
// Only this check sees what the agent wrote: the relay gets the arguments
|
|
166
|
+
// the action takes, so the ones it does not take are named here.
|
|
167
|
+
const withIgnored = (result) => withIgnoredArguments(result, parsed.ignored);
|
|
168
|
+
if (signal?.aborted) return withIgnored(remoteRelayCancelledResult());
|
|
166
169
|
try {
|
|
167
170
|
const response = await api('POST', REMOTE_RELAY_TOOL_ENDPOINT, {
|
|
168
171
|
conversationId: conversation,
|
|
169
172
|
action: parsed.action,
|
|
170
173
|
args: parsed.args,
|
|
171
174
|
}, { longCall: true, ...(signal ? { signal } : {}) });
|
|
172
|
-
if (response && typeof response === 'object' && !Array.isArray(response)) return response;
|
|
173
|
-
return { ok: true, result: response ?? null };
|
|
175
|
+
if (response && typeof response === 'object' && !Array.isArray(response)) return withIgnored(response);
|
|
176
|
+
return withIgnored({ ok: true, result: response ?? null });
|
|
174
177
|
} catch (error) {
|
|
175
|
-
if (signal?.aborted || isAbortError(error)) return remoteRelayCancelledResult();
|
|
176
|
-
return remoteRelayErrorResult(error);
|
|
178
|
+
if (signal?.aborted || isAbortError(error)) return withIgnored(remoteRelayCancelledResult());
|
|
179
|
+
return withIgnored(remoteRelayErrorResult(error));
|
|
177
180
|
}
|
|
178
181
|
}
|
|
179
182
|
|
|
183
|
+
/** `result` with `ignored` (the arguments the action did not take) merged in. */
|
|
184
|
+
export function withIgnoredArguments(result, ignored) {
|
|
185
|
+
const extra = Array.isArray(ignored) ? ignored.map(toText).filter(Boolean) : [];
|
|
186
|
+
if (!extra.length || !result || typeof result !== 'object' || Array.isArray(result)) return result;
|
|
187
|
+
const known = Array.isArray(result.ignored) ? result.ignored.map(toText).filter(Boolean) : [];
|
|
188
|
+
return { ...result, ignored: [...new Set([...known, ...extra])].sort() };
|
|
189
|
+
}
|
|
190
|
+
|
|
180
191
|
/**
|
|
181
192
|
* Whether a worker should register the tool: at least one remote is paired,
|
|
182
193
|
* or this relay lets agents start and use sessions on it (the local target).
|
|
@@ -85,6 +85,7 @@ export function steerAgentLabelForProvider(providerType) {
|
|
|
85
85
|
case 'cursor': return 'Cursor';
|
|
86
86
|
case 'grok': return 'Grok';
|
|
87
87
|
case 'claude-cloud': return 'Claude Cloud';
|
|
88
|
+
case 'deepseek': return 'DeepSeek';
|
|
88
89
|
default: return DEFAULT_STEER_AGENT_LABEL;
|
|
89
90
|
}
|
|
90
91
|
}
|
|
@@ -0,0 +1,128 @@
|
|
|
1
|
+
// A turn on an OpenAI image model (`gpt-image-*`, `dall-e-*`) is no chat
|
|
2
|
+
// turn: no chat runtime can serve it, and one that is handed it fails ("No
|
|
3
|
+
// available capacity was found for the model", or a request rejected as
|
|
4
|
+
// invalid). Both Copilot engines therefore send it straight to the relay's
|
|
5
|
+
// image routes instead, and this module is the one copy of that path.
|
|
6
|
+
//
|
|
7
|
+
// With image continuity on (the default) the relay created an image operation
|
|
8
|
+
// when the message was sent, and `POST /api/image-operations/:id/execute` does
|
|
9
|
+
// everything: the provider call, the generated-image reply, and on a failure
|
|
10
|
+
// the failure note or the requeue. The caller only reports the outcome. Without
|
|
11
|
+
// an operation the worker calls `POST /api/openai/images/generate` and
|
|
12
|
+
// publishes the reply itself.
|
|
13
|
+
|
|
14
|
+
export const DIRECT_IMAGE_OUTCOMES = Object.freeze(['completed', 'executing', 'terminal', 'uncertain', 'retryable']);
|
|
15
|
+
|
|
16
|
+
// The code of the error thrown when the provider refused the request outright:
|
|
17
|
+
// a retry would be refused the same way, so the turn fails instead of being
|
|
18
|
+
// requeued.
|
|
19
|
+
export const OPENAI_IMAGE_TERMINAL_CODE = 'RELAY_OPENAI_IMAGE_TERMINAL';
|
|
20
|
+
|
|
21
|
+
export function shouldUseDirectOpenAIImageApi(message = {}) {
|
|
22
|
+
const providerType = String(message?.providerType || '').trim().toLowerCase();
|
|
23
|
+
if (providerType !== 'openai') return false;
|
|
24
|
+
const model = String(message?.providerModel || message?.model || '').trim().toLowerCase();
|
|
25
|
+
return model.startsWith('gpt-image-') || model.startsWith('dall-e-');
|
|
26
|
+
}
|
|
27
|
+
|
|
28
|
+
export function isDirectOpenAIImageTerminalStatus(status) {
|
|
29
|
+
const numeric = Number(status);
|
|
30
|
+
if (!Number.isFinite(numeric)) return false;
|
|
31
|
+
return [400, 401, 403, 404, 409, 422, 429, 503].includes(numeric);
|
|
32
|
+
}
|
|
33
|
+
|
|
34
|
+
export function isOpenAIImageTerminalError(error) {
|
|
35
|
+
return String(error?.code || '').trim() === OPENAI_IMAGE_TERMINAL_CODE;
|
|
36
|
+
}
|
|
37
|
+
|
|
38
|
+
const isAutoModel = (value) => String(value || '').trim().toLowerCase() === 'auto';
|
|
39
|
+
|
|
40
|
+
/**
|
|
41
|
+
* Run one image turn. `requestOptions` goes to every `api` call that may wait
|
|
42
|
+
* on the provider (a worker passes `{ longCall: true }`: an edit can take
|
|
43
|
+
* minutes); `responseFields` is merged into the reply the worker publishes
|
|
44
|
+
* itself (the attempt fence).
|
|
45
|
+
*
|
|
46
|
+
* Resolves `{ via: 'operation', outcome }` or `{ via: 'direct', outcome:
|
|
47
|
+
* 'completed', imageCount, model }`. Throws an error with `code`
|
|
48
|
+
* `OPENAI_IMAGE_TERMINAL_CODE` when the provider refused the direct request,
|
|
49
|
+
* and a plain error for anything else (the caller requeues).
|
|
50
|
+
*/
|
|
51
|
+
export async function runDirectOpenAIImageTurn({
|
|
52
|
+
api,
|
|
53
|
+
message,
|
|
54
|
+
requestOptions = {},
|
|
55
|
+
responseFields = {},
|
|
56
|
+
} = {}) {
|
|
57
|
+
if (message?.imageOperationId) {
|
|
58
|
+
const result = await api(
|
|
59
|
+
'POST',
|
|
60
|
+
`/api/image-operations/${encodeURIComponent(message.imageOperationId)}/execute`,
|
|
61
|
+
{ messageId: message.id },
|
|
62
|
+
requestOptions,
|
|
63
|
+
).catch((error) => {
|
|
64
|
+
// The route answers a failure it already handled (published or
|
|
65
|
+
// requeued) with 409 or 503 and the outcome in the body.
|
|
66
|
+
if (error?.status === 409 || error?.status === 503) {
|
|
67
|
+
return error?.body || error?.payload || { outcome: error?.status === 503 ? 'retryable' : 'terminal' };
|
|
68
|
+
}
|
|
69
|
+
throw error;
|
|
70
|
+
});
|
|
71
|
+
const outcome = String(result?.outcome || '').trim().toLowerCase();
|
|
72
|
+
if (!DIRECT_IMAGE_OUTCOMES.includes(outcome)) {
|
|
73
|
+
throw new Error('Image operation returned an invalid outcome');
|
|
74
|
+
}
|
|
75
|
+
return { via: 'operation', outcome };
|
|
76
|
+
}
|
|
77
|
+
|
|
78
|
+
const requestedModel = isAutoModel(message?.model) ? '' : String(message?.model || '').trim();
|
|
79
|
+
const directModel = String(message?.providerModel || requestedModel || message?.model || '').trim();
|
|
80
|
+
const prompt = String(message?.text || '').trim();
|
|
81
|
+
if (!directModel) throw new Error('OpenAI image request is missing model');
|
|
82
|
+
if (!prompt) throw new Error('OpenAI image request is missing prompt');
|
|
83
|
+
await api('POST', '/api/activity', {
|
|
84
|
+
messageId: message.id,
|
|
85
|
+
conversationId: message.conversationId,
|
|
86
|
+
mode: message.relayMode || 'agent',
|
|
87
|
+
text: `Calling OpenAI image API directly (${directModel})`,
|
|
88
|
+
}).catch(() => {});
|
|
89
|
+
let directResult;
|
|
90
|
+
try {
|
|
91
|
+
directResult = await api('POST', '/api/openai/images/generate', {
|
|
92
|
+
messageId: message.id,
|
|
93
|
+
conversationId: message.conversationId,
|
|
94
|
+
model: directModel,
|
|
95
|
+
prompt,
|
|
96
|
+
n: message?.n,
|
|
97
|
+
size: message?.size || message?.contextTier,
|
|
98
|
+
quality: message?.quality || message?.reasoningEffort,
|
|
99
|
+
attachments: Array.isArray(message?.attachments) ? message.attachments : [],
|
|
100
|
+
}, requestOptions);
|
|
101
|
+
} catch (error) {
|
|
102
|
+
if (isDirectOpenAIImageTerminalStatus(error?.status)) {
|
|
103
|
+
const terminalError = new Error(
|
|
104
|
+
`OpenAI image request failed: ${error?.detail || `HTTP ${error?.status || 'error'}`}`,
|
|
105
|
+
);
|
|
106
|
+
terminalError.code = OPENAI_IMAGE_TERMINAL_CODE;
|
|
107
|
+
terminalError.status = error?.status;
|
|
108
|
+
terminalError.detail = error?.detail || null;
|
|
109
|
+
throw terminalError;
|
|
110
|
+
}
|
|
111
|
+
throw error;
|
|
112
|
+
}
|
|
113
|
+
const generatedImages = Array.isArray(directResult?.generatedImages) ? directResult.generatedImages : [];
|
|
114
|
+
if (!generatedImages.length) {
|
|
115
|
+
throw new Error('OpenAI image API returned no images');
|
|
116
|
+
}
|
|
117
|
+
const model = String(directResult?.model || directModel).trim() || directModel;
|
|
118
|
+
await api('POST', '/api/response', {
|
|
119
|
+
messageId: message.id,
|
|
120
|
+
conversationId: message.conversationId,
|
|
121
|
+
text: '',
|
|
122
|
+
generatedImages,
|
|
123
|
+
model,
|
|
124
|
+
modelOrigin: isAutoModel(message?.model) ? 'auto' : 'manual',
|
|
125
|
+
...responseFields,
|
|
126
|
+
});
|
|
127
|
+
return { via: 'direct', outcome: 'completed', imageCount: generatedImages.length, model };
|
|
128
|
+
}
|
|
@@ -95,3 +95,63 @@ export function installWorkerLogFile({
|
|
|
95
95
|
},
|
|
96
96
|
};
|
|
97
97
|
}
|
|
98
|
+
|
|
99
|
+
const REDACTED_SECRET_TEXT = '[redacted]';
|
|
100
|
+
// As the relay's redaction of worker posts (server/services/worker-secret-redaction.mjs):
|
|
101
|
+
// a shorter value would mangle ordinary text.
|
|
102
|
+
const MIN_REDACTED_SECRET_LENGTH = 8;
|
|
103
|
+
|
|
104
|
+
/**
|
|
105
|
+
* Replaces each secret's exact text with `[redacted]` in what the worker
|
|
106
|
+
* writes to stdout/stderr, on every platform: the console, the Linux
|
|
107
|
+
* launcher's redirect into the worker log, and the Windows copy above (install
|
|
108
|
+
* this after installWorkerLogFile so the copy gets the redacted text). A
|
|
109
|
+
* secret split across two writes is not caught; the worker logs whole lines.
|
|
110
|
+
* Returns null when there is no secret of at least 8 characters.
|
|
111
|
+
*/
|
|
112
|
+
export function installWorkerLogRedaction({
|
|
113
|
+
secrets = [],
|
|
114
|
+
processImpl = process,
|
|
115
|
+
} = {}) {
|
|
116
|
+
const values = [...new Set((Array.isArray(secrets) ? secrets : [])
|
|
117
|
+
.map((value) => String(value ?? '').trim())
|
|
118
|
+
.filter((value) => value.length >= MIN_REDACTED_SECRET_LENGTH))]
|
|
119
|
+
.sort((a, b) => b.length - a.length);
|
|
120
|
+
if (!values.length) return null;
|
|
121
|
+
|
|
122
|
+
const redact = (text) => {
|
|
123
|
+
let output = text;
|
|
124
|
+
for (const value of values) {
|
|
125
|
+
if (output.includes(value)) output = output.split(value).join(REDACTED_SECRET_TEXT);
|
|
126
|
+
}
|
|
127
|
+
return output;
|
|
128
|
+
};
|
|
129
|
+
|
|
130
|
+
const restores = [];
|
|
131
|
+
for (const stream of [processImpl.stdout, processImpl.stderr]) {
|
|
132
|
+
if (!stream || typeof stream.write !== 'function') continue;
|
|
133
|
+
const originalWrite = stream.write;
|
|
134
|
+
const redactingWrite = function redactingWrite(chunk, encoding, callback) {
|
|
135
|
+
if (typeof chunk === 'string') {
|
|
136
|
+
return originalWrite.call(this, redact(chunk), encoding, callback);
|
|
137
|
+
}
|
|
138
|
+
if (chunk instanceof Uint8Array) {
|
|
139
|
+
const bytes = Buffer.from(chunk.buffer, chunk.byteOffset, chunk.byteLength);
|
|
140
|
+
if (values.some((value) => bytes.includes(value))) {
|
|
141
|
+
const done = typeof encoding === 'function' ? encoding : callback;
|
|
142
|
+
return originalWrite.call(this, redact(bytes.toString('utf8')), 'utf8', done);
|
|
143
|
+
}
|
|
144
|
+
}
|
|
145
|
+
return originalWrite.call(this, chunk, encoding, callback);
|
|
146
|
+
};
|
|
147
|
+
stream.write = redactingWrite;
|
|
148
|
+
restores.push(() => {
|
|
149
|
+
if (stream.write === redactingWrite) stream.write = originalWrite;
|
|
150
|
+
});
|
|
151
|
+
}
|
|
152
|
+
return {
|
|
153
|
+
uninstall() {
|
|
154
|
+
for (const restore of restores) restore();
|
|
155
|
+
},
|
|
156
|
+
};
|
|
157
|
+
}
|