@oar-sh/oar 0.9.22 → 0.9.24
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/CHANGELOG.md +258 -0
- package/README.md +9 -9
- package/bin/oar.js +82 -4
- package/package.json +16 -15
- package/server/agent-sdk-loader.mjs +116 -0
- package/server/agent-sdks/claude/entry.mjs +4 -0
- package/server/agent-sdks/claude/package-lock.json +1438 -0
- package/server/agent-sdks/claude/package.json +13 -0
- package/server/agent-sdks/cursor/entry-sqlite.mjs +2 -0
- package/server/agent-sdks/cursor/entry.mjs +3 -0
- package/server/agent-sdks/cursor/package-lock.json +171 -0
- package/server/agent-sdks/cursor/package.json +11 -0
- package/server/agent-sdks/koffi/entry.cjs +2 -0
- package/server/agent-sdks/koffi/package-lock.json +368 -0
- package/server/agent-sdks/koffi/package.json +10 -0
- package/server/claude-worker/claude-cli-pinned-env.mjs +4 -1
- package/server/claude-worker/claude-preview-tool.mjs +5 -2
- package/server/claude-worker/claude-remote-relay-tool.mjs +5 -2
- package/server/claude-worker/claude-sdk-adapter.mjs +4 -1
- package/server/copilot-worker/copilot-sdk-session-process.mjs +7 -4
- package/server/cursor-worker/cursor-sdk-adapter.mjs +5 -3
- package/server/public/app/agent-sdk-ui.js +349 -0
- package/server/public/app/api-client.js +91 -8
- package/server/public/app/bootstrap.js +204 -69
- package/server/public/app/model-refresh-ui.mjs +413 -0
- package/server/public/app/no-conversation-state.mjs +5 -2
- package/server/public/app/provider-readiness.mjs +67 -4
- package/server/public/app/settings-modal.js +167 -0
- package/server/public/app/socket-handlers.js +21 -0
- package/server/public/index.html +95 -2
- package/server/routes/agent-sdk-routes.mjs +73 -0
- package/server/routes/messages-routes.mjs +102 -37
- package/server/routes/sessions-routes.mjs +236 -110
- package/server/server-runtime.mjs +416 -121
- package/server/services/about-service.mjs +26 -6
- package/server/services/agent-sdk-launch-gate.mjs +124 -0
- package/server/services/agent-sdk-paths.mjs +307 -0
- package/server/services/agent-sdk-store.mjs +1211 -0
- package/server/services/claude-api-settings-service.mjs +54 -17
- package/server/services/cli-install-service.mjs +3 -2
- package/server/services/copilot-model-discovery-service.mjs +7 -1
- package/server/services/database-file.mjs +17 -1
- package/server/services/deepseek-settings-service.mjs +58 -20
- package/server/services/earlier-database-service.mjs +5 -3
- package/server/services/file-serving.mjs +13 -0
- package/server/services/model-refresh-scheduler.mjs +153 -0
- package/server/services/model-variant-catalog-service.mjs +141 -9
- package/server/services/native-addon-build-service.mjs +224 -0
- package/server/services/oar-cli-helpers.mjs +76 -10
- package/server/services/oar-state-migration-service.mjs +2 -12
- package/server/services/oar-state-root.mjs +17 -0
- package/server/services/provider-model-refresh.mjs +326 -0
- package/server/services/provider-model-state.mjs +395 -0
- package/server/services/relay-console-log.mjs +2 -3
- package/server/services/relay-singleton-guard.mjs +72 -10
- package/server/services/remote-relay-loopback.mjs +3 -3
- package/server/services/sdk-session-sync-service.mjs +1 -1
- package/server/services/session-worker-launch-service.mjs +13 -0
- package/server/services/session-worker-supervisor-service.mjs +19 -1
- package/server/services/session-worker-websocket-service.mjs +15 -0
- package/server/services/update-install-service.mjs +34 -3
- package/server/services/worker-launch-note-service.mjs +14 -2
- package/server/services/worker-recycle-service.mjs +145 -11
- package/shared/model-refresh-schedule.mjs +120 -0
- package/shared/worker-runtime/direct-openai-image.mjs +8 -4
- package/server/services/package-data-rescue-preinstall.mjs +0 -22
- package/server/services/package-data-rescue.mjs +0 -139
- package/server/tty-console-bootstrap.mjs +0 -75
|
@@ -163,6 +163,13 @@ export function shouldRequireNewOpenAIConversation({
|
|
|
163
163
|
);
|
|
164
164
|
}
|
|
165
165
|
|
|
166
|
+
// The path a `{*splat}` route matched: express 5 hands it over as its
|
|
167
|
+
// decoded segments, or not at all when the path ends at the slash.
|
|
168
|
+
export function splatPath(req) {
|
|
169
|
+
const splat = req.params?.splat;
|
|
170
|
+
return Array.isArray(splat) ? splat.join('/') : String(splat || '');
|
|
171
|
+
}
|
|
172
|
+
|
|
166
173
|
function normalizeRequestedProviderType(value = '') {
|
|
167
174
|
const normalized = String(value || '').trim().toLowerCase();
|
|
168
175
|
if (normalized === 'openai' || normalized === 'openai-byok' || normalized === 'openai-image') return 'openai';
|
|
@@ -1519,14 +1526,61 @@ function resolveGeneratedImageExtension(mimeType) {
|
|
|
1519
1526
|
return GENERATED_IMAGE_EXTENSIONS[normalized] || 'png';
|
|
1520
1527
|
}
|
|
1521
1528
|
|
|
1529
|
+
// Base64 payloads are checked with a character loop, never with a regular
|
|
1530
|
+
// expression over the whole string. V8 compiles a regexp without
|
|
1531
|
+
// optimisation once the process has generated more than 1 MB of regexp code
|
|
1532
|
+
// and holds more than 16 MB of executable memory (TooMuchRegExpCode in
|
|
1533
|
+
// regexp.cc), which a relay reaches after some uptime. An unoptimised
|
|
1534
|
+
// `[...]+` then pushes one backtrack frame per character and overflows the
|
|
1535
|
+
// 64 MiB regexp stack at 4,194,304 characters, a PNG of about 3 MB, with
|
|
1536
|
+
// "Maximum call stack size exceeded": every gpt-image reply above that size
|
|
1537
|
+
// was dropped after the provider had billed it, and the row was retried four
|
|
1538
|
+
// more times (2026-11-07). A fresh process optimises the same regexp and
|
|
1539
|
+
// never shows it.
|
|
1540
|
+
// An empty string is not base64 here: every caller checks for a missing
|
|
1541
|
+
// payload first and answers it with its own message.
|
|
1542
|
+
export function isBase64Text(value) {
|
|
1543
|
+
const text = String(value || '');
|
|
1544
|
+
if (!text) return false;
|
|
1545
|
+
for (let i = 0; i < text.length; i += 1) {
|
|
1546
|
+
const c = text.charCodeAt(i);
|
|
1547
|
+
if (
|
|
1548
|
+
(c >= 48 && c <= 57) || (c >= 65 && c <= 90) || (c >= 97 && c <= 122)
|
|
1549
|
+
|| c === 43 || c === 47 || c === 61 // + / =
|
|
1550
|
+
|| c === 32 || c === 9 || c === 10 || c === 13 || c === 11 || c === 12 // ASCII whitespace
|
|
1551
|
+
) continue;
|
|
1552
|
+
return false;
|
|
1553
|
+
}
|
|
1554
|
+
return true;
|
|
1555
|
+
}
|
|
1556
|
+
|
|
1557
|
+
/**
|
|
1558
|
+
* `data:<mime>;base64,<payload>` taken apart without a regexp over the
|
|
1559
|
+
* payload: `{ mimeType, payload }`, or null when the string is not that
|
|
1560
|
+
* shape (no comma, no `;base64` right before it, a parameter in the type,
|
|
1561
|
+
* an empty payload).
|
|
1562
|
+
*/
|
|
1563
|
+
export function splitBase64DataUrl(dataUrl) {
|
|
1564
|
+
const text = String(dataUrl || '');
|
|
1565
|
+
if (!/^data:/i.test(text)) return null;
|
|
1566
|
+
const comma = text.indexOf(',');
|
|
1567
|
+
if (comma < 0) return null;
|
|
1568
|
+
const header = text.slice('data:'.length, comma);
|
|
1569
|
+
const payload = text.slice(comma + 1);
|
|
1570
|
+
if (!/;base64$/i.test(header) || !payload) return null;
|
|
1571
|
+
const mimeType = header.slice(0, -';base64'.length);
|
|
1572
|
+
if (!mimeType || mimeType.includes(';') || mimeType.includes(',')) return null;
|
|
1573
|
+
return { mimeType, payload };
|
|
1574
|
+
}
|
|
1575
|
+
|
|
1522
1576
|
function parseGeneratedImageData(raw) {
|
|
1523
1577
|
const dataUrl = String(raw?.dataUrl || raw?.data_url || '').trim();
|
|
1524
1578
|
if (dataUrl.startsWith('data:')) {
|
|
1525
|
-
const
|
|
1526
|
-
if (!
|
|
1579
|
+
const parts = splitBase64DataUrl(dataUrl);
|
|
1580
|
+
if (!parts) throw new Error('Invalid generated image data URL');
|
|
1527
1581
|
return {
|
|
1528
|
-
mimeType: String(raw?.mimeType || raw?.mime_type ||
|
|
1529
|
-
dataBase64:
|
|
1582
|
+
mimeType: String(raw?.mimeType || raw?.mime_type || parts.mimeType || '').trim().toLowerCase(),
|
|
1583
|
+
dataBase64: parts.payload.trim(),
|
|
1530
1584
|
};
|
|
1531
1585
|
}
|
|
1532
1586
|
return {
|
|
@@ -1545,7 +1599,7 @@ export function normalizeGeneratedImageResponses(rawGeneratedImages, { maxImages
|
|
|
1545
1599
|
const mimeType = parsed.mimeType || 'image/png';
|
|
1546
1600
|
if (!mimeType.startsWith('image/')) throw new Error('Generated image type must be image/*');
|
|
1547
1601
|
if (!parsed.dataBase64) throw new Error('Generated image payload is missing base64 data');
|
|
1548
|
-
if (
|
|
1602
|
+
if (!isBase64Text(parsed.dataBase64)) {
|
|
1549
1603
|
throw new Error('Generated image payload is not valid base64');
|
|
1550
1604
|
}
|
|
1551
1605
|
const buffer = Buffer.from(parsed.dataBase64, 'base64');
|
|
@@ -1571,7 +1625,7 @@ export function shouldAcceptAssistantResponsePayload({ text = '', terminalFailur
|
|
|
1571
1625
|
function parseOpenAIImageBase64Value(value) {
|
|
1572
1626
|
const raw = String(value || '').trim();
|
|
1573
1627
|
if (!raw) return '';
|
|
1574
|
-
if (
|
|
1628
|
+
if (!isBase64Text(raw)) {
|
|
1575
1629
|
throw new Error('OpenAI image payload contains invalid base64 data');
|
|
1576
1630
|
}
|
|
1577
1631
|
return Buffer.from(raw, 'base64').toString('base64');
|
|
@@ -1662,10 +1716,10 @@ export function resolveOpenAIImageEditAttachment(rawAttachments, {
|
|
|
1662
1716
|
let bytes = null;
|
|
1663
1717
|
|
|
1664
1718
|
if (dataUrl.startsWith('data:')) {
|
|
1665
|
-
const
|
|
1666
|
-
if (!
|
|
1667
|
-
mimeType = mimeType || String(
|
|
1668
|
-
const payload = parseOpenAIImageBase64Value(
|
|
1719
|
+
const parts = splitBase64DataUrl(dataUrl);
|
|
1720
|
+
if (!parts) throw new Error('Invalid image attachment data URL');
|
|
1721
|
+
mimeType = mimeType || String(parts.mimeType || '').trim().toLowerCase();
|
|
1722
|
+
const payload = parseOpenAIImageBase64Value(parts.payload);
|
|
1669
1723
|
bytes = Buffer.from(payload, 'base64');
|
|
1670
1724
|
} else {
|
|
1671
1725
|
const filePath = String(firstImage.path || '').trim();
|
|
@@ -2142,8 +2196,6 @@ export function registerMessagesRoutes(app, deps) {
|
|
|
2142
2196
|
deepseekClaudeConfigDir = '',
|
|
2143
2197
|
} = deps;
|
|
2144
2198
|
|
|
2145
|
-
const ttyConsoleActive = runtimeState?.ttyConsoleActive === true;
|
|
2146
|
-
|
|
2147
2199
|
// ─── Sub-agent runs: header, timeline, live events ─────────────────────────
|
|
2148
2200
|
const subagentTimeline = createSubagentTimelineService({ db, stmts });
|
|
2149
2201
|
|
|
@@ -2255,18 +2307,6 @@ export function registerMessagesRoutes(app, deps) {
|
|
|
2255
2307
|
LIMIT 1
|
|
2256
2308
|
`);
|
|
2257
2309
|
|
|
2258
|
-
function isAbnormalWorkerTelemetry(payload = {}, level = 'log') {
|
|
2259
|
-
const normalizedLevel = String(level || 'log').trim().toLowerCase();
|
|
2260
|
-
if (normalizedLevel === 'warn' || normalizedLevel === 'error') return true;
|
|
2261
|
-
const event = String(payload?.event || '').trim().toLowerCase();
|
|
2262
|
-
const state = String(payload?.state || '').trim().toLowerCase();
|
|
2263
|
-
if (state === 'error') return true;
|
|
2264
|
-
return event.includes('error')
|
|
2265
|
-
|| event.includes('failed')
|
|
2266
|
-
|| event.includes('failure')
|
|
2267
|
-
|| event.includes('blocked');
|
|
2268
|
-
}
|
|
2269
|
-
|
|
2270
2310
|
function readBridgeIdentity(req) {
|
|
2271
2311
|
return relayBridgeOwnerService?.normalizeIdentity?.({
|
|
2272
2312
|
pid: req.headers['x-relay-process-pid'],
|
|
@@ -2340,8 +2380,6 @@ export function registerMessagesRoutes(app, deps) {
|
|
|
2340
2380
|
const payload = extra && typeof extra === 'object'
|
|
2341
2381
|
? { ...envelope, ...extra }
|
|
2342
2382
|
: envelope;
|
|
2343
|
-
const shouldLog = !ttyConsoleActive || isAbnormalWorkerTelemetry(payload, level);
|
|
2344
|
-
if (!shouldLog) return;
|
|
2345
2383
|
const method = typeof console[level] === 'function' ? level : 'log';
|
|
2346
2384
|
console[method](`[${ts()}] WORKER ${JSON.stringify(payload)}`);
|
|
2347
2385
|
}
|
|
@@ -4021,8 +4059,8 @@ export function registerMessagesRoutes(app, deps) {
|
|
|
4021
4059
|
});
|
|
4022
4060
|
});
|
|
4023
4061
|
|
|
4024
|
-
app.get('/api/files
|
|
4025
|
-
const requestedPath =
|
|
4062
|
+
app.get('/api/files/{*splat}', auth, (req, res) => {
|
|
4063
|
+
const requestedPath = splatPath(req).trim();
|
|
4026
4064
|
const rootOverride = resolveScopedWorkspaceRootPath(req);
|
|
4027
4065
|
const filePath = resolveWorkspaceFilePath(requestedPath, rootOverride);
|
|
4028
4066
|
if (!filePath) return res.status(400).json({ error: 'Invalid file path' });
|
|
@@ -4043,8 +4081,8 @@ export function registerMessagesRoutes(app, deps) {
|
|
|
4043
4081
|
});
|
|
4044
4082
|
});
|
|
4045
4083
|
|
|
4046
|
-
app.get('/api/files-preview
|
|
4047
|
-
const requestedPath =
|
|
4084
|
+
app.get('/api/files-preview/{*splat}', auth, (req, res) => {
|
|
4085
|
+
const requestedPath = splatPath(req).trim();
|
|
4048
4086
|
const normalizedPath = normalizeWorkspaceRelativePath(requestedPath);
|
|
4049
4087
|
const rootOverride = resolveScopedWorkspaceRootPath(req);
|
|
4050
4088
|
const filePath = resolveWorkspaceFilePath(requestedPath, rootOverride);
|
|
@@ -7815,9 +7853,16 @@ export function registerMessagesRoutes(app, deps) {
|
|
|
7815
7853
|
});
|
|
7816
7854
|
} catch (error) {
|
|
7817
7855
|
const status = Number(error?.status || 0);
|
|
7818
|
-
|
|
7856
|
+
// `relayProcessing`: the provider answered 2xx and the relay could not
|
|
7857
|
+
// process the reply. The generation is billed, a retry pays again and
|
|
7858
|
+
// fails the same way, so the row fails like a refusal.
|
|
7859
|
+
const relayProcessing = generatedPayload?.relayProcessing === true;
|
|
7860
|
+
const terminal = (status >= 400 && status < 500) || relayProcessing;
|
|
7819
7861
|
const uncertain = !!generatedPayload?.generatedImages;
|
|
7820
7862
|
const outcome = terminal ? 'terminal' : (uncertain ? 'uncertain' : 'retryable');
|
|
7863
|
+
const errorMessage = relayProcessing
|
|
7864
|
+
? `The relay could not process the image OpenAI returned (${error?.message || 'unknown error'}). The generation was billed; the message was not retried.`
|
|
7865
|
+
: String(error?.message || 'Image operation failed');
|
|
7821
7866
|
stmts.finishAttempt.run({
|
|
7822
7867
|
id: acquired.attempt.id,
|
|
7823
7868
|
status: terminal ? 'failed_terminal' : (uncertain ? 'uncertain' : 'failed_retryable'),
|
|
@@ -7825,8 +7870,10 @@ export function registerMessagesRoutes(app, deps) {
|
|
|
7825
7870
|
providerConversationId: null,
|
|
7826
7871
|
providerResponseId: null,
|
|
7827
7872
|
httpStatus: status || null,
|
|
7828
|
-
errorCode: terminal
|
|
7829
|
-
|
|
7873
|
+
errorCode: terminal
|
|
7874
|
+
? (relayProcessing ? 'IMAGE_REPLY_UNPROCESSED' : 'IMAGE_PROVIDER_REJECTED')
|
|
7875
|
+
: (uncertain ? 'IMAGE_FINALIZATION_UNCERTAIN' : 'IMAGE_PROVIDER_UNAVAILABLE'),
|
|
7876
|
+
errorMessage: errorMessage.slice(0, 500),
|
|
7830
7877
|
completedAt: new Date().toISOString(),
|
|
7831
7878
|
});
|
|
7832
7879
|
if (outcome === 'retryable') {
|
|
@@ -7840,8 +7887,10 @@ export function registerMessagesRoutes(app, deps) {
|
|
|
7840
7887
|
}
|
|
7841
7888
|
} else {
|
|
7842
7889
|
const failure = {
|
|
7843
|
-
code: outcome === 'uncertain'
|
|
7844
|
-
|
|
7890
|
+
code: outcome === 'uncertain'
|
|
7891
|
+
? 'IMAGE_FINALIZATION_UNCERTAIN'
|
|
7892
|
+
: (relayProcessing ? 'IMAGE_REPLY_UNPROCESSED' : 'IMAGE_PROVIDER_REJECTED'),
|
|
7893
|
+
message: errorMessage.slice(0, 500),
|
|
7845
7894
|
retryable: false,
|
|
7846
7895
|
};
|
|
7847
7896
|
const failResponse = await fetch(`${selfBaseUrl}${remotePath}/api/response`, {
|
|
@@ -7956,7 +8005,13 @@ export function registerMessagesRoutes(app, deps) {
|
|
|
7956
8005
|
try {
|
|
7957
8006
|
payload = JSON.parse(rawText);
|
|
7958
8007
|
} catch {
|
|
7959
|
-
|
|
8008
|
+
// After a 2xx the generation is billed; the callers fail the row
|
|
8009
|
+
// instead of paying for it again. An error status with a body that
|
|
8010
|
+
// is not JSON stays a provider failure.
|
|
8011
|
+
return res.status(502).json({
|
|
8012
|
+
error: 'OpenAI image API returned invalid JSON',
|
|
8013
|
+
...(response.ok ? { relayProcessing: true } : {}),
|
|
8014
|
+
});
|
|
7960
8015
|
}
|
|
7961
8016
|
}
|
|
7962
8017
|
|
|
@@ -7978,7 +8033,17 @@ export function registerMessagesRoutes(app, deps) {
|
|
|
7978
8033
|
maxImages: MAX_UPLOAD_ATTACHMENTS,
|
|
7979
8034
|
});
|
|
7980
8035
|
} catch (error) {
|
|
7981
|
-
|
|
8036
|
+
// The provider answered 2xx, so the generation is billed; what failed
|
|
8037
|
+
// is the relay's handling of the reply. Logged with the stack, and
|
|
8038
|
+
// flagged so the callers fail the row instead of requeueing it: a
|
|
8039
|
+
// retry pays again and fails the same way.
|
|
8040
|
+
console.error(
|
|
8041
|
+
`[${ts()}] IMAGE reply of ${requestBody.model} not processed: ${error?.message || error} (body ${rawText.length} chars)\n${error?.stack || ''}`,
|
|
8042
|
+
);
|
|
8043
|
+
return res.status(502).json({
|
|
8044
|
+
error: error?.message || 'OpenAI image API returned no images',
|
|
8045
|
+
relayProcessing: true,
|
|
8046
|
+
});
|
|
7982
8047
|
}
|
|
7983
8048
|
|
|
7984
8049
|
return res.json({
|