@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
@@ -0,0 +1,174 @@
1
+ 'use strict';
2
+
3
+ /**
4
+ * The asking side of questions that don't block on paired relays: while an
5
+ * ask this relay's agent put on a paired relay is open there (a mirror row,
6
+ * relay-ask-service.mjs), this relay asks that relay every few seconds how it
7
+ * stands (POST /api/relay-asks/remote/status) and closes what closed there;
8
+ * the ask service tells the conversation that asked. Only this relay calls,
9
+ * so a pairing in one direction is enough. A relay that does not answer is
10
+ * asked less often until it does; nothing is lost meanwhile, the asks wait
11
+ * there.
12
+ */
13
+
14
+ import { REMOTE_RELAY_ERROR_CODES } from '../../shared/remote-relay-contract.mjs';
15
+ import { RELAY_ASK_LIMITS } from '../../shared/relay-asks.mjs';
16
+
17
+ // The routes a relay serves its paired relays for asks (routes/relay-ask-routes.mjs).
18
+ export const RELAY_ASK_REMOTE_PATHS = Object.freeze({
19
+ ask: '/api/relay-asks/remote/ask',
20
+ status: '/api/relay-asks/remote/status',
21
+ withdraw: '/api/relay-asks/remote/withdraw',
22
+ answer: '/api/relay-asks/remote/answer',
23
+ dismiss: '/api/relay-asks/remote/dismiss',
24
+ inbox: '/api/relay-asks/remote/inbox',
25
+ });
26
+
27
+ const LOG_PREFIX = '[relay-asks]';
28
+
29
+ function toText(value) {
30
+ return String(value ?? '').trim();
31
+ }
32
+
33
+ /**
34
+ * A paired relay that predates asks answers their routes with Express's HTML
35
+ * 404: no JSON, no code. A 404 of these routes always carries one.
36
+ */
37
+ export function isMissingRelayAskRoute(error) {
38
+ return toText(error?.code) === REMOTE_RELAY_ERROR_CODES.notFound && !toText(error?.remoteBody?.code);
39
+ }
40
+
41
+ export function relayWithoutAsksText(name) {
42
+ return `Relay "${toText(name) || 'the paired relay'}" runs an OAR version without questions that don't block; it needs an update before its sessions can be asked for from here.`;
43
+ }
44
+
45
+ export function createRelayAskLinkService({
46
+ asks,
47
+ // `get(id)`: the registry entry (address and token) of a paired relay.
48
+ registry,
49
+ // `request(relay, method, path, { body, timeoutMs })`: remote-relay-client.mjs.
50
+ client,
51
+ setTimer = (fn, ms) => {
52
+ const timer = setTimeout(fn, ms);
53
+ timer.unref?.();
54
+ return timer;
55
+ },
56
+ clearTimer = (timer) => clearTimeout(timer),
57
+ logger = console,
58
+ pollMs = RELAY_ASK_LIMITS.remotePollMs,
59
+ failingPollMs = RELAY_ASK_LIMITS.remotePollFailingMs,
60
+ timeoutMs = 15_000,
61
+ } = {}) {
62
+ if (!asks) throw new Error('asks is required');
63
+ // paired relay id → { timer, running, again }
64
+ const links = new Map();
65
+ let started = false;
66
+
67
+ function warn(line) {
68
+ try { logger?.warn?.(`${LOG_PREFIX} ${line}`); } catch {}
69
+ }
70
+
71
+ function linkState(relayId) {
72
+ if (!links.has(relayId)) links.set(relayId, { timer: null, running: null, again: false });
73
+ return links.get(relayId);
74
+ }
75
+
76
+ function forget(relayId) {
77
+ const link = links.get(relayId);
78
+ if (link?.timer) clearTimer(link.timer);
79
+ links.delete(relayId);
80
+ }
81
+
82
+ function schedule(relayId, delayMs) {
83
+ if (!started) return;
84
+ const link = linkState(relayId);
85
+ if (link.timer) clearTimer(link.timer);
86
+ link.timer = setTimer(() => {
87
+ link.timer = null;
88
+ void fetchNow(relayId);
89
+ }, Math.max(0, delayMs));
90
+ }
91
+
92
+ async function fetchOnce(relayId) {
93
+ const ids = asks.pendingMirrorIds(relayId);
94
+ if (!ids.length) return { ok: true, idle: true };
95
+ const relay = registry?.get?.(relayId) || null;
96
+ if (!relay) {
97
+ asks.noteRelayRemoved(relayId);
98
+ return { ok: false, removed: true };
99
+ }
100
+ let response;
101
+ try {
102
+ response = await client.request(relay, 'POST', RELAY_ASK_REMOTE_PATHS.status, { body: { ids }, timeoutMs });
103
+ } catch (error) {
104
+ return { ok: false, error: toText(error?.message || error).slice(0, 300) || 'no answer' };
105
+ }
106
+ if (!response || !Array.isArray(response.asks)) return { ok: false, error: 'unexpected answer' };
107
+ asks.applyRemoteStatus(relayId, { relayName: toText(relay.name), statuses: response.asks });
108
+ return { ok: true };
109
+ }
110
+
111
+ /** Asks a paired relay now (one already running is followed by one more). Never throws. */
112
+ function fetchNow(relayId) {
113
+ const id = toText(relayId);
114
+ const link = linkState(id);
115
+ if (link.timer) {
116
+ clearTimer(link.timer);
117
+ link.timer = null;
118
+ }
119
+ if (link.running) {
120
+ link.again = true;
121
+ return link.running;
122
+ }
123
+ link.running = (async () => {
124
+ let result;
125
+ try {
126
+ result = await fetchOnce(id);
127
+ } catch (error) {
128
+ warn(`status from paired relay ${id.slice(0, 12)} failed: ${toText(error?.message || error).slice(0, 200)}`);
129
+ result = { ok: false };
130
+ } finally {
131
+ link.running = null;
132
+ }
133
+ if (result.removed) {
134
+ forget(id);
135
+ } else if (link.again) {
136
+ link.again = false;
137
+ schedule(id, 0);
138
+ } else if (asks.pendingMirrorIds(id).length) {
139
+ schedule(id, result.ok ? pollMs : failingPollMs);
140
+ } else {
141
+ forget(id);
142
+ }
143
+ return result;
144
+ })();
145
+ return link.running;
146
+ }
147
+
148
+ /** An ask was just put on that relay: look for its answer from now on. */
149
+ function noteAsked(relayId) {
150
+ const id = toText(relayId);
151
+ if (!id || links.get(id)?.timer || links.get(id)?.running) return;
152
+ schedule(id, pollMs);
153
+ }
154
+
155
+ function start() {
156
+ if (started) return;
157
+ started = true;
158
+ let delay = 0;
159
+ for (const relayId of asks.mirrorRelays()) {
160
+ schedule(relayId, delay);
161
+ delay += 250;
162
+ }
163
+ }
164
+
165
+ function stop() {
166
+ started = false;
167
+ for (const link of links.values()) {
168
+ if (link.timer) clearTimer(link.timer);
169
+ link.timer = null;
170
+ }
171
+ }
172
+
173
+ return { start, stop, fetchNow, noteAsked, isPolling: (relayId) => links.has(toText(relayId)) };
174
+ }