@oar-sh/oar 0.9.23 → 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 (59) hide show
  1. package/CHANGELOG.md +165 -0
  2. package/README.md +9 -9
  3. package/bin/oar.js +54 -4
  4. package/package.json +9 -7
  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-preview-tool.mjs +5 -2
  17. package/server/claude-worker/claude-remote-relay-tool.mjs +5 -2
  18. package/server/claude-worker/claude-sdk-adapter.mjs +4 -1
  19. package/server/copilot-worker/copilot-sdk-session-process.mjs +7 -4
  20. package/server/cursor-worker/cursor-sdk-adapter.mjs +5 -3
  21. package/server/public/app/agent-sdk-ui.js +349 -0
  22. package/server/public/app/api-client.js +91 -8
  23. package/server/public/app/bootstrap.js +204 -69
  24. package/server/public/app/model-refresh-ui.mjs +413 -0
  25. package/server/public/app/no-conversation-state.mjs +5 -2
  26. package/server/public/app/provider-readiness.mjs +67 -4
  27. package/server/public/app/settings-modal.js +167 -0
  28. package/server/public/app/socket-handlers.js +21 -0
  29. package/server/public/index.html +95 -2
  30. package/server/routes/agent-sdk-routes.mjs +73 -0
  31. package/server/routes/messages-routes.mjs +91 -17
  32. package/server/routes/sessions-routes.mjs +235 -109
  33. package/server/server-runtime.mjs +395 -81
  34. package/server/services/about-service.mjs +26 -6
  35. package/server/services/agent-sdk-launch-gate.mjs +124 -0
  36. package/server/services/agent-sdk-paths.mjs +307 -0
  37. package/server/services/agent-sdk-store.mjs +1211 -0
  38. package/server/services/claude-api-settings-service.mjs +54 -17
  39. package/server/services/cli-install-service.mjs +3 -2
  40. package/server/services/copilot-model-discovery-service.mjs +7 -1
  41. package/server/services/database-file.mjs +17 -1
  42. package/server/services/deepseek-settings-service.mjs +58 -20
  43. package/server/services/earlier-database-service.mjs +4 -2
  44. package/server/services/model-refresh-scheduler.mjs +153 -0
  45. package/server/services/model-variant-catalog-service.mjs +141 -9
  46. package/server/services/oar-cli-helpers.mjs +72 -9
  47. package/server/services/oar-state-migration-service.mjs +2 -12
  48. package/server/services/oar-state-root.mjs +17 -0
  49. package/server/services/provider-model-refresh.mjs +326 -0
  50. package/server/services/provider-model-state.mjs +395 -0
  51. package/server/services/relay-singleton-guard.mjs +72 -10
  52. package/server/services/session-worker-launch-service.mjs +13 -0
  53. package/server/services/session-worker-supervisor-service.mjs +19 -1
  54. package/server/services/session-worker-websocket-service.mjs +15 -0
  55. package/server/services/update-install-service.mjs +2 -2
  56. package/server/services/worker-launch-note-service.mjs +14 -2
  57. package/server/services/worker-recycle-service.mjs +145 -11
  58. package/shared/model-refresh-schedule.mjs +120 -0
  59. package/shared/worker-runtime/direct-openai-image.mjs +8 -4
@@ -1526,14 +1526,61 @@ function resolveGeneratedImageExtension(mimeType) {
1526
1526
  return GENERATED_IMAGE_EXTENSIONS[normalized] || 'png';
1527
1527
  }
1528
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
+
1529
1576
  function parseGeneratedImageData(raw) {
1530
1577
  const dataUrl = String(raw?.dataUrl || raw?.data_url || '').trim();
1531
1578
  if (dataUrl.startsWith('data:')) {
1532
- const match = dataUrl.match(/^data:([^;,]+);base64,(.+)$/i);
1533
- 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');
1534
1581
  return {
1535
- mimeType: String(raw?.mimeType || raw?.mime_type || match[1] || '').trim().toLowerCase(),
1536
- dataBase64: String(match[2] || '').trim(),
1582
+ mimeType: String(raw?.mimeType || raw?.mime_type || parts.mimeType || '').trim().toLowerCase(),
1583
+ dataBase64: parts.payload.trim(),
1537
1584
  };
1538
1585
  }
1539
1586
  return {
@@ -1552,7 +1599,7 @@ export function normalizeGeneratedImageResponses(rawGeneratedImages, { maxImages
1552
1599
  const mimeType = parsed.mimeType || 'image/png';
1553
1600
  if (!mimeType.startsWith('image/')) throw new Error('Generated image type must be image/*');
1554
1601
  if (!parsed.dataBase64) throw new Error('Generated image payload is missing base64 data');
1555
- if (!/^[a-z0-9+/=\s]+$/i.test(parsed.dataBase64)) {
1602
+ if (!isBase64Text(parsed.dataBase64)) {
1556
1603
  throw new Error('Generated image payload is not valid base64');
1557
1604
  }
1558
1605
  const buffer = Buffer.from(parsed.dataBase64, 'base64');
@@ -1578,7 +1625,7 @@ export function shouldAcceptAssistantResponsePayload({ text = '', terminalFailur
1578
1625
  function parseOpenAIImageBase64Value(value) {
1579
1626
  const raw = String(value || '').trim();
1580
1627
  if (!raw) return '';
1581
- if (!/^[a-z0-9+/=\s]+$/i.test(raw)) {
1628
+ if (!isBase64Text(raw)) {
1582
1629
  throw new Error('OpenAI image payload contains invalid base64 data');
1583
1630
  }
1584
1631
  return Buffer.from(raw, 'base64').toString('base64');
@@ -1669,10 +1716,10 @@ export function resolveOpenAIImageEditAttachment(rawAttachments, {
1669
1716
  let bytes = null;
1670
1717
 
1671
1718
  if (dataUrl.startsWith('data:')) {
1672
- const match = dataUrl.match(/^data:([^;,]+);base64,(.+)$/i);
1673
- if (!match) throw new Error('Invalid image attachment data URL');
1674
- mimeType = mimeType || String(match[1] || '').trim().toLowerCase();
1675
- 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);
1676
1723
  bytes = Buffer.from(payload, 'base64');
1677
1724
  } else {
1678
1725
  const filePath = String(firstImage.path || '').trim();
@@ -7806,9 +7853,16 @@ export function registerMessagesRoutes(app, deps) {
7806
7853
  });
7807
7854
  } catch (error) {
7808
7855
  const status = Number(error?.status || 0);
7809
- 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;
7810
7861
  const uncertain = !!generatedPayload?.generatedImages;
7811
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');
7812
7866
  stmts.finishAttempt.run({
7813
7867
  id: acquired.attempt.id,
7814
7868
  status: terminal ? 'failed_terminal' : (uncertain ? 'uncertain' : 'failed_retryable'),
@@ -7816,8 +7870,10 @@ export function registerMessagesRoutes(app, deps) {
7816
7870
  providerConversationId: null,
7817
7871
  providerResponseId: null,
7818
7872
  httpStatus: status || null,
7819
- errorCode: terminal ? 'IMAGE_PROVIDER_REJECTED' : (uncertain ? 'IMAGE_FINALIZATION_UNCERTAIN' : 'IMAGE_PROVIDER_UNAVAILABLE'),
7820
- 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),
7821
7877
  completedAt: new Date().toISOString(),
7822
7878
  });
7823
7879
  if (outcome === 'retryable') {
@@ -7831,8 +7887,10 @@ export function registerMessagesRoutes(app, deps) {
7831
7887
  }
7832
7888
  } else {
7833
7889
  const failure = {
7834
- code: outcome === 'uncertain' ? 'IMAGE_FINALIZATION_UNCERTAIN' : 'IMAGE_PROVIDER_REJECTED',
7835
- 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),
7836
7894
  retryable: false,
7837
7895
  };
7838
7896
  const failResponse = await fetch(`${selfBaseUrl}${remotePath}/api/response`, {
@@ -7947,7 +8005,13 @@ export function registerMessagesRoutes(app, deps) {
7947
8005
  try {
7948
8006
  payload = JSON.parse(rawText);
7949
8007
  } catch {
7950
- 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
+ });
7951
8015
  }
7952
8016
  }
7953
8017
 
@@ -7969,7 +8033,17 @@ export function registerMessagesRoutes(app, deps) {
7969
8033
  maxImages: MAX_UPLOAD_ATTACHMENTS,
7970
8034
  });
7971
8035
  } catch (error) {
7972
- 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
+ });
7973
8047
  }
7974
8048
 
7975
8049
  return res.json({