@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.
Files changed (68) hide show
  1. package/CHANGELOG.md +258 -0
  2. package/README.md +9 -9
  3. package/bin/oar.js +82 -4
  4. package/package.json +16 -15
  5. package/server/agent-sdk-loader.mjs +116 -0
  6. package/server/agent-sdks/claude/entry.mjs +4 -0
  7. package/server/agent-sdks/claude/package-lock.json +1438 -0
  8. package/server/agent-sdks/claude/package.json +13 -0
  9. package/server/agent-sdks/cursor/entry-sqlite.mjs +2 -0
  10. package/server/agent-sdks/cursor/entry.mjs +3 -0
  11. package/server/agent-sdks/cursor/package-lock.json +171 -0
  12. package/server/agent-sdks/cursor/package.json +11 -0
  13. package/server/agent-sdks/koffi/entry.cjs +2 -0
  14. package/server/agent-sdks/koffi/package-lock.json +368 -0
  15. package/server/agent-sdks/koffi/package.json +10 -0
  16. package/server/claude-worker/claude-cli-pinned-env.mjs +4 -1
  17. package/server/claude-worker/claude-preview-tool.mjs +5 -2
  18. package/server/claude-worker/claude-remote-relay-tool.mjs +5 -2
  19. package/server/claude-worker/claude-sdk-adapter.mjs +4 -1
  20. package/server/copilot-worker/copilot-sdk-session-process.mjs +7 -4
  21. package/server/cursor-worker/cursor-sdk-adapter.mjs +5 -3
  22. package/server/public/app/agent-sdk-ui.js +349 -0
  23. package/server/public/app/api-client.js +91 -8
  24. package/server/public/app/bootstrap.js +204 -69
  25. package/server/public/app/model-refresh-ui.mjs +413 -0
  26. package/server/public/app/no-conversation-state.mjs +5 -2
  27. package/server/public/app/provider-readiness.mjs +67 -4
  28. package/server/public/app/settings-modal.js +167 -0
  29. package/server/public/app/socket-handlers.js +21 -0
  30. package/server/public/index.html +95 -2
  31. package/server/routes/agent-sdk-routes.mjs +73 -0
  32. package/server/routes/messages-routes.mjs +102 -37
  33. package/server/routes/sessions-routes.mjs +236 -110
  34. package/server/server-runtime.mjs +416 -121
  35. package/server/services/about-service.mjs +26 -6
  36. package/server/services/agent-sdk-launch-gate.mjs +124 -0
  37. package/server/services/agent-sdk-paths.mjs +307 -0
  38. package/server/services/agent-sdk-store.mjs +1211 -0
  39. package/server/services/claude-api-settings-service.mjs +54 -17
  40. package/server/services/cli-install-service.mjs +3 -2
  41. package/server/services/copilot-model-discovery-service.mjs +7 -1
  42. package/server/services/database-file.mjs +17 -1
  43. package/server/services/deepseek-settings-service.mjs +58 -20
  44. package/server/services/earlier-database-service.mjs +5 -3
  45. package/server/services/file-serving.mjs +13 -0
  46. package/server/services/model-refresh-scheduler.mjs +153 -0
  47. package/server/services/model-variant-catalog-service.mjs +141 -9
  48. package/server/services/native-addon-build-service.mjs +224 -0
  49. package/server/services/oar-cli-helpers.mjs +76 -10
  50. package/server/services/oar-state-migration-service.mjs +2 -12
  51. package/server/services/oar-state-root.mjs +17 -0
  52. package/server/services/provider-model-refresh.mjs +326 -0
  53. package/server/services/provider-model-state.mjs +395 -0
  54. package/server/services/relay-console-log.mjs +2 -3
  55. package/server/services/relay-singleton-guard.mjs +72 -10
  56. package/server/services/remote-relay-loopback.mjs +3 -3
  57. package/server/services/sdk-session-sync-service.mjs +1 -1
  58. package/server/services/session-worker-launch-service.mjs +13 -0
  59. package/server/services/session-worker-supervisor-service.mjs +19 -1
  60. package/server/services/session-worker-websocket-service.mjs +15 -0
  61. package/server/services/update-install-service.mjs +34 -3
  62. package/server/services/worker-launch-note-service.mjs +14 -2
  63. package/server/services/worker-recycle-service.mjs +145 -11
  64. package/shared/model-refresh-schedule.mjs +120 -0
  65. package/shared/worker-runtime/direct-openai-image.mjs +8 -4
  66. package/server/services/package-data-rescue-preinstall.mjs +0 -22
  67. package/server/services/package-data-rescue.mjs +0 -139
  68. 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 match = dataUrl.match(/^data:([^;,]+);base64,(.+)$/i);
1526
- if (!match) throw new Error('Invalid generated image data URL');
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 || match[1] || '').trim().toLowerCase(),
1529
- dataBase64: String(match[2] || '').trim(),
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 (!/^[a-z0-9+/=\s]+$/i.test(parsed.dataBase64)) {
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 (!/^[a-z0-9+/=\s]+$/i.test(raw)) {
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 match = dataUrl.match(/^data:([^;,]+);base64,(.+)$/i);
1666
- if (!match) throw new Error('Invalid image attachment data URL');
1667
- mimeType = mimeType || String(match[1] || '').trim().toLowerCase();
1668
- const payload = parseOpenAIImageBase64Value(match[2]);
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/*', auth, (req, res) => {
4025
- const requestedPath = String(req.params?.[0] || '').trim();
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/*', auth, (req, res) => {
4047
- const requestedPath = String(req.params?.[0] || '').trim();
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
- const terminal = status >= 400 && status < 500;
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 ? 'IMAGE_PROVIDER_REJECTED' : (uncertain ? 'IMAGE_FINALIZATION_UNCERTAIN' : 'IMAGE_PROVIDER_UNAVAILABLE'),
7829
- errorMessage: String(error?.message || 'Image operation failed').slice(0, 500),
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' ? 'IMAGE_FINALIZATION_UNCERTAIN' : 'IMAGE_PROVIDER_REJECTED',
7844
- message: String(error?.message || 'Image operation failed').slice(0, 500),
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
- return res.status(502).json({ error: 'OpenAI image API returned invalid JSON' });
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
- return res.status(502).json({ error: error?.message || 'OpenAI image API returned no images' });
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({