@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.
Files changed (109) hide show
  1. package/.github/extensions/web-relay/polling/polling-loop.mjs +14 -87
  2. package/CHANGELOG.md +305 -0
  3. package/README.md +12 -11
  4. package/package.json +2 -1
  5. package/server/claude-worker/claude-attachments.mjs +7 -4
  6. package/server/claude-worker/claude-remote-relay-tool.mjs +36 -1
  7. package/server/claude-worker/claude-sdk-adapter.mjs +167 -9
  8. package/server/claude-worker/claude-session-process.mjs +131 -33
  9. package/server/claude-worker/claude-session-worker-main.mjs +266 -0
  10. package/server/claude-worker/claude-session-worker.mjs +2 -253
  11. package/server/claude-worker/claude-turn-failure.mjs +8 -0
  12. package/server/claude-worker/claude-turn-publisher.mjs +282 -13
  13. package/server/claude-worker/claude-usage-totals.mjs +108 -0
  14. package/server/claude-worker/sdk-message-normalizer.mjs +55 -0
  15. package/server/copilot-worker/copilot-sdk-adapter.mjs +18 -4
  16. package/server/copilot-worker/copilot-sdk-session-process.mjs +73 -7
  17. package/server/db-schema.mjs +115 -0
  18. package/server/deepseek-worker/deepseek-claude-backend.mjs +433 -0
  19. package/server/deepseek-worker/deepseek-session-worker.mjs +37 -0
  20. package/server/migrations/0019-relay-event-subscription-options.mjs +106 -0
  21. package/server/migrations/0020-conversation-may-create-sessions.mjs +67 -0
  22. package/server/migrations/0021-conversation-group-agent-marks.mjs +77 -0
  23. package/server/migrations/0022-relay-schedules.mjs +142 -0
  24. package/server/migrations/0023-relay-asks.mjs +141 -0
  25. package/server/migrations/0024-deepseek-usage-totals.mjs +151 -0
  26. package/server/migrations/0025-deepseek-usage-requests-daily.mjs +179 -0
  27. package/server/migrations/0026-deepseek-usage-served-models.mjs +222 -0
  28. package/server/migrations/0027-deepseek-usage-checkpoint-runs.mjs +101 -0
  29. package/server/migrations/0028-deepseek-usage-checkpoint-request-runs.mjs +99 -0
  30. package/server/migrations/0029-deepseek-balance-rises.mjs +99 -0
  31. package/server/migrations/0030-deepseek-usage-actuals.mjs +123 -0
  32. package/server/public/app/api-client.js +53 -0
  33. package/server/public/app/ask-user-view.js +3 -0
  34. package/server/public/app/bootstrap.js +162 -16
  35. package/server/public/app/context-tier-options.mjs +4 -0
  36. package/server/public/app/context-usage-view.mjs +17 -0
  37. package/server/public/app/conversation-group-order.mjs +35 -0
  38. package/server/public/app/conversation-provider-indicator.mjs +7 -0
  39. package/server/public/app/conversation-view.js +2 -0
  40. package/server/public/app/deepseek-settings-ui.js +342 -0
  41. package/server/public/app/inbox-ui.js +367 -0
  42. package/server/public/app/inbox-view.mjs +370 -0
  43. package/server/public/app/journal-view.js +54 -5
  44. package/server/public/app/model-selector-options.mjs +11 -0
  45. package/server/public/app/model-vision-support.mjs +2 -0
  46. package/server/public/app/no-conversation-state.mjs +1 -1
  47. package/server/public/app/plan-usage-view.mjs +165 -13
  48. package/server/public/app/provider-readiness.mjs +4 -1
  49. package/server/public/app/relay-error-ctas.mjs +8 -0
  50. package/server/public/app/relay-event-subscriptions-ui.js +4 -2
  51. package/server/public/app/relay-event-view.mjs +33 -7
  52. package/server/public/app/relay-schedule-view.mjs +119 -0
  53. package/server/public/app/relay-schedules-ui.js +172 -0
  54. package/server/public/app/settings-modal.js +11 -0
  55. package/server/public/app/settings-tabs.js +1 -1
  56. package/server/public/app/socket-handlers.js +37 -3
  57. package/server/public/app/store.js +18 -5
  58. package/server/public/index.html +567 -35
  59. package/server/relay-tools.md +18 -3
  60. package/server/repositories/conversation-group-repository.mjs +110 -17
  61. package/server/repositories/remote-relay-repository.mjs +20 -2
  62. package/server/repositories/session-repository.mjs +7 -1
  63. package/server/routes/ask-user-routes.mjs +57 -3
  64. package/server/routes/cache-routes.mjs +6 -0
  65. package/server/routes/conversation-group-routes.mjs +19 -3
  66. package/server/routes/deepseek-usage-import-routes.mjs +72 -0
  67. package/server/routes/messages-routes.mjs +195 -18
  68. package/server/routes/relay-ask-routes.mjs +387 -0
  69. package/server/routes/relay-event-routes.mjs +3 -1
  70. package/server/routes/relay-schedule-routes.mjs +57 -0
  71. package/server/routes/sessions-routes.mjs +932 -97
  72. package/server/server-runtime.mjs +203 -11
  73. package/server/services/about-service.mjs +1 -0
  74. package/server/services/cache-rebuild-service.mjs +19 -2
  75. package/server/services/claude-session-leftovers.mjs +226 -0
  76. package/server/services/conversation-preferences-service.mjs +17 -0
  77. package/server/services/deepseek-balance-service.mjs +102 -0
  78. package/server/services/deepseek-settings-service.mjs +380 -0
  79. package/server/services/deepseek-usage-import.mjs +471 -0
  80. package/server/services/model-variant-catalog-service.mjs +1 -0
  81. package/server/services/plan-usage-contract.mjs +100 -5
  82. package/server/services/plan-usage-deepseek.mjs +931 -0
  83. package/server/services/plan-usage-service.mjs +431 -0
  84. package/server/services/provider-model-selection.mjs +1 -0
  85. package/server/services/provider-reasoning-effort.mjs +15 -2
  86. package/server/services/relay-ask-link-service.mjs +174 -0
  87. package/server/services/relay-ask-service.mjs +604 -0
  88. package/server/services/relay-event-service.mjs +589 -73
  89. package/server/services/relay-schedule-service.mjs +596 -0
  90. package/server/services/remote-relay-caller-context.mjs +89 -20
  91. package/server/services/remote-relay-dispatcher.mjs +958 -31
  92. package/server/services/session-worker-launch-service.mjs +233 -82
  93. package/server/services/worker-secret-redaction.mjs +318 -0
  94. package/shared/context-window-fallbacks.mjs +3 -0
  95. package/shared/deepseek-api.mjs +137 -0
  96. package/shared/deepseek-models.mjs +114 -0
  97. package/shared/deepseek-pricing.mjs +146 -0
  98. package/shared/host-claude-session-env.mjs +101 -0
  99. package/shared/model-id.mjs +7 -0
  100. package/shared/preview-tool-core.mjs +1 -1
  101. package/shared/provider-routing.mjs +2 -2
  102. package/shared/relay-asks.mjs +145 -0
  103. package/shared/relay-events.mjs +268 -18
  104. package/shared/relay-schedules.mjs +128 -0
  105. package/shared/remote-relay-contract.mjs +554 -26
  106. package/shared/remote-relay-tool-core.mjs +16 -5
  107. package/shared/steer-settle-failure.mjs +1 -0
  108. package/shared/worker-runtime/direct-openai-image.mjs +128 -0
  109. package/shared/worker-runtime/worker-log-file.mjs +60 -0
@@ -112,8 +112,13 @@ import { STEER_FOLDED_TEXT, STEER_STOPPED_TEXT, STEER_TURN_FAILED_TEXT } from '.
112
112
  import { buildSteerSettleFailure } from '../../shared/steer-settle-failure.mjs';
113
113
  import { shouldEmitStreamUpdate } from '../../shared/stream-emit-gating.mjs';
114
114
  import { withSteerNote } from '../../shared/steer-note.mjs';
115
- import { isRelayEventDelivery } from '../../shared/relay-events.mjs';
115
+ import { isRelayAutomatedDelivery } from '../../shared/relay-events.mjs';
116
116
  import { isTransientRelayError } from '../../shared/worker-runtime/relay-errors.mjs';
117
+ import {
118
+ isOpenAIImageTerminalError,
119
+ runDirectOpenAIImageTurn,
120
+ shouldUseDirectOpenAIImageApi,
121
+ } from '../../shared/worker-runtime/direct-openai-image.mjs';
117
122
  import {
118
123
  createTurnLiveness,
119
124
  createTurnStalledError,
@@ -556,6 +561,12 @@ export function createCopilotSdkSessionRunner({
556
561
  // terminal error until the publish lands, so recovery can never re-run them
557
562
  // (see publishSettleMarker).
558
563
  const settlingMessages = new Map();
564
+ // Image-model rows running on the relay's image routes (id → message). They
565
+ // never reach the runtime, so no turn owns them; the heartbeat and the crash
566
+ // guard still have to.
567
+ const directImageMessages = new Map();
568
+ // `openai` on a BYOK worker; the failure notes name the provider.
569
+ const workerProvider = String(env.COPILOT_PROVIDER_TYPE || '').trim().toLowerCase();
559
570
  // Settle markers that could not be posted even terminally; the heartbeat
560
571
  // hands them to the relay to fail (id → terminalError).
561
572
  const settleFailedTerminals = new Map();
@@ -3728,7 +3739,7 @@ export function createCopilotSdkSessionRunner({
3728
3739
  // afterwards: a runtime that died or went silent under a Stop used to put
3729
3740
  // the watchdog's failure on every row instead.
3730
3741
  if (failure && !turn.aborted) {
3731
- const classified = classifyCopilotTurnException(failure);
3742
+ const classified = classifyCopilotTurnException(failure, { provider: workerProvider });
3732
3743
  dbg('copilot turn failed', turn.message?.id || '(continuation)', classified.detail);
3733
3744
  const running = turn.currentOwner || turn.primaryEntry;
3734
3745
  for (const entry of pending) {
@@ -3747,7 +3758,7 @@ export function createCopilotSdkSessionRunner({
3747
3758
  return;
3748
3759
  }
3749
3760
  if (result?.isError) {
3750
- const classified = classifyCopilotSessionError(result.errorData || { message: result.errorMessage });
3761
+ const classified = classifyCopilotSessionError(result.errorData || { message: result.errorMessage }, { provider: workerProvider });
3751
3762
  dbg('turn failed', turn.message?.id || '(continuation)', classified.stableCode);
3752
3763
  const running = turn.currentOwner || turn.primaryEntry;
3753
3764
  for (const entry of pending) {
@@ -4117,6 +4128,7 @@ export function createCopilotSdkSessionRunner({
4117
4128
  dbg('continuation output dropped (no relay row)');
4118
4129
  const classified = classifyCopilotTurnException(
4119
4130
  turn.failure || new Error('the relay refused a continuation row for this turn'),
4131
+ { provider: workerProvider },
4120
4132
  );
4121
4133
  for (const entry of turn.entries) {
4122
4134
  if (entry.settled || entry.turn !== turn) continue;
@@ -4456,7 +4468,7 @@ export function createCopilotSdkSessionRunner({
4456
4468
  // Not in a continuation: there the running work is the runtime's own
4457
4469
  // follow-up, not a request of the user's to keep going with.
4458
4470
  // Nor in an event message the relay wrote: the note speaks as the user.
4459
- const prompt = withRelayContext(context?.prefix, turn.kind === 'continuation' || isRelayEventDelivery(message) ? body : withSteerNote(body));
4471
+ const prompt = withRelayContext(context?.prefix, turn.kind === 'continuation' || isRelayAutomatedDelivery(message) ? body : withSteerNote(body));
4460
4472
  // Re-checked AFTER the awaits and before anything is sent or registered.
4461
4473
  // Nothing has been sent yet, so handing the row back to the normal path is
4462
4474
  // free.
@@ -4821,6 +4833,9 @@ export function createCopilotSdkSessionRunner({
4821
4833
  async function handlePendingPayload(pending) {
4822
4834
  const message = pending?.message || null;
4823
4835
  if (!message) return false;
4836
+ // An image model is no chat model: its row goes to the relay's image
4837
+ // routes, whatever the runtime is doing, and never into the runtime.
4838
+ if (shouldUseDirectOpenAIImageApi(message)) return runDirectImageTurn(message);
4824
4839
  // Held (a card is open, a compaction is running, a tool of the main agent
4825
4840
  // is running, the turn-opening prompt is not sent yet): hand the row back
4826
4841
  // with no retry penalty; the relay re-delivers it when the hold ends. An
@@ -4850,7 +4865,7 @@ export function createCopilotSdkSessionRunner({
4850
4865
  if (outcome !== NOT_STEERED) return outcome;
4851
4866
  } catch (error) {
4852
4867
  dbg('steering failed', message.id, error?.message || String(error));
4853
- const classified = classifyCopilotTurnException(error);
4868
+ const classified = classifyCopilotTurnException(error, { provider: workerProvider });
4854
4869
  await publishResponse(message, {
4855
4870
  text: classified.text,
4856
4871
  model: null,
@@ -4863,6 +4878,54 @@ export function createCopilotSdkSessionRunner({
4863
4878
  return runTurn(turn);
4864
4879
  }
4865
4880
 
4881
+ /**
4882
+ * One image-model row (shared/worker-runtime/direct-openai-image.mjs). With
4883
+ * an image operation the relay's execute route publishes the reply, the
4884
+ * failure or the requeue itself; without one the shared helper publishes
4885
+ * the reply, and a refusal by the provider is published here as the row's
4886
+ * failure. Anything else requeues the row, as on the extension engine.
4887
+ */
4888
+ async function runDirectImageTurn(message) {
4889
+ const id = String(message.id || '');
4890
+ directImageMessages.set(id, message);
4891
+ touch();
4892
+ try {
4893
+ const result = await runDirectOpenAIImageTurn({
4894
+ api,
4895
+ message,
4896
+ // An edit can take minutes, longer than fetch waits for headers.
4897
+ requestOptions: { longCall: true },
4898
+ responseFields: attemptFields(message),
4899
+ });
4900
+ dbg('image turn', message.id, result.via, result.outcome);
4901
+ } catch (error) {
4902
+ if (isOpenAIImageTerminalError(error)) {
4903
+ dbg('image turn refused by OpenAI', message.id, error.message);
4904
+ await publishResponse(message, {
4905
+ text: error.message,
4906
+ model: null,
4907
+ terminalError: {
4908
+ kind: 'openai-image-failed',
4909
+ // The code the relay's image operation uses for the same refusal.
4910
+ code: 'image-provider-rejected',
4911
+ stableCode: 'image-provider-rejected',
4912
+ message: error.message,
4913
+ ...(error.detail ? { detail: String(error.detail) } : {}),
4914
+ failedAt: new Date().toISOString(),
4915
+ queueMessageId: id || null,
4916
+ },
4917
+ });
4918
+ } else {
4919
+ dbg('image turn failed; requeueing', message.id, error?.message || String(error));
4920
+ await api('POST', '/api/requeue', { messageId: message.id, ...attemptFields(message) }).catch(() => {});
4921
+ }
4922
+ } finally {
4923
+ directImageMessages.delete(id);
4924
+ touch();
4925
+ }
4926
+ return true;
4927
+ }
4928
+
4866
4929
  // --------------------------------------------------------------- teardown --
4867
4930
 
4868
4931
  function getActiveQueueMessageId() {
@@ -4872,6 +4935,7 @@ export function createCopilotSdkSessionRunner({
4872
4935
  const id = String(entry.message?.id || '');
4873
4936
  if (id && !entry.released) return id;
4874
4937
  }
4938
+ for (const id of directImageMessages.keys()) return id;
4875
4939
  return '';
4876
4940
  }
4877
4941
 
@@ -4911,6 +4975,7 @@ export function createCopilotSdkSessionRunner({
4911
4975
  for (const { message, variant } of settlingMessages.values()) {
4912
4976
  push(message, buildSteerSettleFailure(message, { variant, agentLabel: SETTLE_AGENT_LABEL }));
4913
4977
  }
4978
+ for (const message of directImageMessages.values()) push(message);
4914
4979
  return entries;
4915
4980
  }
4916
4981
 
@@ -4949,8 +5014,9 @@ export function createCopilotSdkSessionRunner({
4949
5014
  // registry and refuses ids it does not know.
4950
5015
  stopBackgroundTask,
4951
5016
  // "Active" spans both ownerships: a settled turn still publishing must
4952
- // keep the worker's idle/shutdown gates closed just like a running one.
4953
- isTurnActive: () => !!activeTurn || publishingTurns.size > 0,
5017
+ // keep the worker's idle/shutdown gates closed just like a running one,
5018
+ // and so must an image row on the relay's image routes.
5019
+ isTurnActive: () => !!activeTurn || publishingTurns.size > 0 || directImageMessages.size > 0,
4954
5020
  dispose,
4955
5021
  // The turn's tokens/cost/TTFT, as posted to `/api/copilot-plan-usage`.
4956
5022
  getLastTurnUsage: () => lastTurnUsage,
@@ -24,6 +24,18 @@ import { migrateConversationGroupPosition } from './migrations/0015-conversation
24
24
  import { migrateShareAccessLog } from './migrations/0016-share-access-log.mjs';
25
25
  import { migrateConversationHistoryIndexes } from './migrations/0017-conversation-history-indexes.mjs';
26
26
  import { migrateBackgroundTaskRows } from './migrations/0018-background-task-rows.mjs';
27
+ import { migrateRelayEventSubscriptionOptions } from './migrations/0019-relay-event-subscription-options.mjs';
28
+ import { migrateConversationMayCreateSessions } from './migrations/0020-conversation-may-create-sessions.mjs';
29
+ import { migrateConversationGroupAgentMarks } from './migrations/0021-conversation-group-agent-marks.mjs';
30
+ import { migrateRelaySchedules } from './migrations/0022-relay-schedules.mjs';
31
+ import { migrateRelayAsks } from './migrations/0023-relay-asks.mjs';
32
+ import { migrateDeepSeekUsageTotals } from './migrations/0024-deepseek-usage-totals.mjs';
33
+ import { migrateDeepSeekUsageRequestsDaily } from './migrations/0025-deepseek-usage-requests-daily.mjs';
34
+ import { migrateDeepSeekUsageServedModels } from './migrations/0026-deepseek-usage-served-models.mjs';
35
+ import { migrateDeepSeekUsageCheckpointRuns } from './migrations/0027-deepseek-usage-checkpoint-runs.mjs';
36
+ import { migrateDeepSeekUsageCheckpointRequestRuns } from './migrations/0028-deepseek-usage-checkpoint-request-runs.mjs';
37
+ import { migrateDeepSeekBalanceRises } from './migrations/0029-deepseek-balance-rises.mjs';
38
+ import { migrateDeepSeekUsageActuals } from './migrations/0030-deepseek-usage-actuals.mjs';
27
39
  import { migrateImageConversationSchema } from './repositories/image-conversation-repository.mjs';
28
40
 
29
41
  // Mirrors DEFAULT_RELAY_MODE in server-runtime.mjs; used only by the one-time
@@ -855,6 +867,109 @@ try {
855
867
  console.warn(`[background-tasks] migration 0018 failed; retrying on next boot: ${error?.message || error}`);
856
868
  }
857
869
 
870
+ // Options of relay event subscriptions (digest, quiet hours, exclude) and the
871
+ // watcher's own prompts: columns on 0011's table and one table. The event
872
+ // service checks for them and refuses the options without them, so a failure
873
+ // is logged and retried next boot like 0018.
874
+ try {
875
+ migrateRelayEventSubscriptionOptions(db);
876
+ } catch (error) {
877
+ console.warn(`[relay-events] migration 0019 failed; retrying on next boot: ${error?.message || error}`);
878
+ }
879
+
880
+ // The owner's "agents in this conversation may start sessions": one column.
881
+ // Without it nobody can turn the switch on (the preferences route refuses
882
+ // it) and the one-level rule holds for every session, so a failure is logged
883
+ // and retried next boot like 0019.
884
+ try {
885
+ migrateConversationMayCreateSessions(db);
886
+ } catch (error) {
887
+ console.warn(`[remote-relays] migration 0020 failed; retrying on next boot: ${error?.message || error}`);
888
+ }
889
+
890
+ // What an agent's update_session may do to a group: two columns. Without
891
+ // them an owner's group an agent emptied is kept until the next boot repair
892
+ // only, so a failure is logged and retried next boot like 0020.
893
+ try {
894
+ migrateConversationGroupAgentMarks(db);
895
+ } catch (error) {
896
+ console.warn(`[session-groups] migration 0021 failed; retrying on next boot: ${error?.message || error}`);
897
+ }
898
+
899
+ // Relay schedules: one new table. The schedule service checks for it and
900
+ // refuses the schedule actions without it, so a failure is logged and
901
+ // retried next boot like 0021.
902
+ try {
903
+ migrateRelaySchedules(db);
904
+ } catch (error) {
905
+ console.warn(`[relay-schedules] migration 0022 failed; retrying on next boot: ${error?.message || error}`);
906
+ }
907
+
908
+ // Questions that don't block (remote_relay ask): one new table. The ask
909
+ // service checks for it and refuses the ask actions without it, so a failure
910
+ // is logged and retried next boot like 0022.
911
+ try {
912
+ migrateRelayAsks(db);
913
+ } catch (error) {
914
+ console.warn(`[relay-asks] migration 0023 failed; retrying on next boot: ${error?.message || error}`);
915
+ }
916
+
917
+ // DeepSeek usage totals: one new table. The plan usage service creates it
918
+ // itself as well, so a failure here is logged and retried next boot like 0023.
919
+ try {
920
+ migrateDeepSeekUsageTotals(db);
921
+ } catch (error) {
922
+ console.warn(`[deepseek-usage] migration 0024 failed; retrying on next boot: ${error?.message || error}`);
923
+ }
924
+
925
+ // DeepSeek search/fetch counts, daily spend and balance reference: four
926
+ // columns on 0024's table and two tables, handled like 0024.
927
+ try {
928
+ migrateDeepSeekUsageRequestsDaily(db);
929
+ } catch (error) {
930
+ console.warn(`[deepseek-usage] migration 0025 failed; retrying on next boot: ${error?.message || error}`);
931
+ }
932
+
933
+ // DeepSeek usage booked under a Claude model name is folded into the DeepSeek
934
+ // model that served it, handled like 0024.
935
+ try {
936
+ migrateDeepSeekUsageServedModels(db);
937
+ } catch (error) {
938
+ console.warn(`[deepseek-usage] migration 0026 failed; retrying on next boot: ${error?.message || error}`);
939
+ }
940
+
941
+ // The DeepSeek usage checkpoint learns the worker process's run id and its
942
+ // search and fetch counts: three columns on 0024's table, handled like 0024.
943
+ try {
944
+ migrateDeepSeekUsageCheckpointRuns(db);
945
+ } catch (error) {
946
+ console.warn(`[deepseek-usage] migration 0027 failed; retrying on next boot: ${error?.message || error}`);
947
+ }
948
+
949
+ // The checkpoint's search and fetch counts get a run id of their own: one
950
+ // column on 0024's table, handled like 0024.
951
+ try {
952
+ migrateDeepSeekUsageCheckpointRequestRuns(db);
953
+ } catch (error) {
954
+ console.warn(`[deepseek-usage] migration 0028 failed; retrying on next boot: ${error?.message || error}`);
955
+ }
956
+
957
+ // The balance reference notes when the balance last rose: one column on
958
+ // 0025's table, handled like 0025.
959
+ try {
960
+ migrateDeepSeekBalanceRises(db);
961
+ } catch (error) {
962
+ console.warn(`[deepseek-usage] migration 0029 failed; retrying on next boot: ${error?.message || error}`);
963
+ }
964
+
965
+ // The imported figures of DeepSeek's usage export: a table of its own, handled
966
+ // like 0024.
967
+ try {
968
+ migrateDeepSeekUsageActuals(db);
969
+ } catch (error) {
970
+ console.warn(`[deepseek-usage] migration 0030 failed; retrying on next boot: ${error?.message || error}`);
971
+ }
972
+
858
973
  const runtimeSessionColumns = db.prepare(`PRAGMA table_info(runtime_sessions)`).all().map((c) => c.name);
859
974
  if (runtimeSessionColumns.length) {
860
975
  if (!runtimeSessionColumns.includes('strategy')) {