@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
@@ -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
- if (signal?.aborted) return remoteRelayCancelledResult();
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
+ }