@oar-sh/oar 0.9.5 → 0.9.7
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.
- package/.github/extensions/web-relay/polling/polling-loop.mjs +12 -32
- package/.github/extensions/web-relay/skills/question-bridge.mjs +14 -39
- package/CHANGELOG.md +340 -0
- package/README.md +196 -26
- package/package.json +2 -1
- package/server/README.md +174 -7
- package/server/claude-cloud-worker/claude-cloud-event-normalizer.mjs +340 -0
- package/server/claude-cloud-worker/claude-cloud-session-process.mjs +1325 -0
- package/server/claude-cloud-worker/claude-cloud-session-worker.mjs +155 -0
- package/server/claude-worker/claude-remote-relay-tool.mjs +2 -0
- package/server/claude-worker/claude-sdk-adapter.mjs +18 -2
- package/server/claude-worker/claude-session-process.mjs +151 -1
- package/server/claude-worker/claude-session-worker.mjs +6 -0
- package/server/claude-worker/claude-turn-failure.mjs +92 -0
- package/server/claude-worker/claude-turn-publisher.mjs +52 -3
- package/server/claude-worker/sdk-message-normalizer.mjs +13 -1
- package/server/copilot-worker/copilot-model-switch.mjs +29 -3
- package/server/copilot-worker/copilot-question-bridge.mjs +2 -4
- package/server/copilot-worker/copilot-runtime-tree.mjs +91 -0
- package/server/copilot-worker/copilot-sdk-adapter.mjs +48 -8
- package/server/copilot-worker/copilot-sdk-session-process.mjs +897 -166
- package/server/cursor-worker/cursor-sdk-adapter.mjs +80 -13
- package/server/cursor-worker/cursor-turn-runner.mjs +5 -2
- package/server/db-schema.mjs +24 -0
- package/server/grok-worker/acp-client.mjs +68 -12
- package/server/grok-worker/grok-sdk-adapter.mjs +4 -0
- package/server/grok-worker/grok-turn-runner.mjs +6 -2
- package/server/migrations/0006-workspace-root-settings.mjs +67 -0
- package/server/migrations/0007-claude-cloud.mjs +82 -0
- package/server/public/app/agent-sessions-settings-ui.js +197 -0
- package/server/public/app/agent-sessions-settings.mjs +134 -0
- package/server/public/app/api-client.js +160 -6
- package/server/public/app/attachments-view.js +20 -1
- package/server/public/app/attribution-control.mjs +46 -0
- package/server/public/app/bootstrap.js +347 -31
- package/server/public/app/claude-cloud-conversation-ui.js +110 -0
- package/server/public/app/claude-cloud-settings-ui.js +374 -0
- package/server/public/app/claude-cloud-ui.mjs +611 -0
- package/server/public/app/composer-control-state.mjs +15 -3
- package/server/public/app/context-menu.mjs +140 -0
- package/server/public/app/context-tier-options.mjs +4 -0
- package/server/public/app/conversation-origin-ui.js +27 -0
- package/server/public/app/conversation-provider-indicator.mjs +22 -4
- package/server/public/app/conversation-view.js +178 -8
- package/server/public/app/cwd-picker.js +12 -0
- package/server/public/app/journal-view.js +704 -28
- package/server/public/app/markdown-strikethrough.mjs +31 -0
- package/server/public/app/math-dollars.mjs +89 -0
- package/server/public/app/model-selector-options.mjs +1 -1
- package/server/public/app/plan-usage-view.mjs +36 -8
- package/server/public/app/relay-board-inline.mjs +70 -0
- package/server/public/app/relay-board-view.js +157 -61
- package/server/public/app/relay-error-ctas.mjs +28 -5
- package/server/public/app/remote-relay-origin-view.mjs +120 -4
- package/server/public/app/router.js +49 -12
- package/server/public/app/settings-modal.js +33 -2
- package/server/public/app/settings-tabs.js +1 -1
- package/server/public/app/socket-handlers.js +60 -0
- package/server/public/app/store.js +4 -1
- package/server/public/app/subagent-fold.mjs +61 -0
- package/server/public/app/usage-limit-ui.js +225 -0
- package/server/public/index.html +471 -8
- package/server/relay-tools.md +13 -1
- package/server/repositories/message-repository.mjs +10 -1
- package/server/repositories/question-repository.mjs +1 -0
- package/server/repositories/remote-relay-repository.mjs +72 -1
- package/server/repositories/session-repository.mjs +47 -1
- package/server/routes/claude-cloud-routes.mjs +140 -0
- package/server/routes/git-routes.mjs +37 -0
- package/server/routes/messages-routes.mjs +202 -26
- package/server/routes/relay-board-routes.mjs +56 -2
- package/server/routes/remote-relay-routes.mjs +15 -1
- package/server/routes/sessions-routes.mjs +538 -72
- package/server/server-runtime.mjs +291 -27
- package/server/services/claude-account-usage-service.mjs +176 -0
- package/server/services/claude-cloud-repo-service.mjs +408 -0
- package/server/services/claude-cloud-session-service.mjs +326 -0
- package/server/services/claude-cloud-settings-service.mjs +269 -0
- package/server/services/git-remote-service.mjs +115 -0
- package/server/services/plan-usage-claude-cloud.mjs +211 -0
- package/server/services/plan-usage-claude.mjs +202 -40
- package/server/services/plan-usage-contract.mjs +39 -3
- package/server/services/plan-usage-service.mjs +82 -2
- package/server/services/preview-proxy-server.mjs +64 -36
- package/server/services/process-list-service.mjs +122 -0
- package/server/services/provider-reasoning-effort.mjs +3 -0
- package/server/services/relay-console-log.mjs +199 -0
- package/server/services/remote-relay-caller-context.mjs +78 -19
- package/server/services/remote-relay-client.mjs +9 -0
- package/server/services/remote-relay-dispatcher.mjs +451 -39
- package/server/services/remote-relay-inbound.mjs +83 -1
- package/server/services/remote-relay-loopback.mjs +245 -0
- package/server/services/remote-relay-registry-service.mjs +52 -0
- package/server/services/session-worker-launch-service.mjs +100 -15
- package/server/services/session-worker-process-service.mjs +410 -138
- package/server/services/session-worker-registry-service.mjs +1 -1
- package/server/services/session-worker-stop-service.mjs +24 -13
- package/server/services/session-worker-supervisor-service.mjs +89 -2
- package/server/services/usage-limit-pause-service.mjs +443 -0
- package/server/services/worker-launch-note-service.mjs +151 -0
- package/shared/ask-user-bridge.mjs +14 -39
- package/shared/claude-attribution.mjs +117 -0
- package/shared/claude-cloud/api-client.mjs +809 -0
- package/shared/claude-cloud/base-url.mjs +47 -0
- package/shared/claude-cloud/credentials.mjs +221 -0
- package/shared/claude-cloud/repo-url.mjs +111 -0
- package/shared/claude-usage-limit.mjs +257 -0
- package/shared/control-poller.mjs +69 -20
- package/shared/failure-note-text.mjs +38 -0
- package/shared/provider-routing.mjs +4 -2
- package/shared/question-wait.mjs +108 -0
- package/shared/remote-relay-contract.mjs +167 -22
- package/shared/remote-relay-tool-core.mjs +11 -8
- package/shared/steer-settle-failure.mjs +17 -0
- package/shared/steer-settle-markers.mjs +8 -0
- package/shared/worker-runtime/process-tree.mjs +483 -0
- package/shared/worker-runtime/relay-errors.mjs +31 -0
- package/shared/worker-runtime/send-and-wait-errors.mjs +4 -4
- package/shared/worker-runtime/turn-liveness.mjs +249 -0
|
@@ -0,0 +1,225 @@
|
|
|
1
|
+
// The Claude subscription's usage limit, as the open conversation sees it: a
|
|
2
|
+
// banner above the composer. Before the limit it warns ("usage at 96 %"); at
|
|
3
|
+
// the limit it says when the window resets; and when a turn of this
|
|
4
|
+
// conversation was paused there, it says when the turn carries on, with
|
|
5
|
+
// Resume now and Cancel.
|
|
6
|
+
//
|
|
7
|
+
// State arrives over the socket (`usage_limit_pause` per conversation,
|
|
8
|
+
// `claude_usage_limit` for the account), with the conversation payload, and
|
|
9
|
+
// with GET /api/status polls, so a client that missed an event catches up.
|
|
10
|
+
|
|
11
|
+
import { showTransientRelayNotice } from './store.js';
|
|
12
|
+
import { cancelUsageLimitPause, resumeUsageLimitPause } from './api-client.js';
|
|
13
|
+
|
|
14
|
+
const BANNER_ID = 'usage-limit-banner';
|
|
15
|
+
const DISMISSED_KEY = 'oar.usageLimitWarningDismissed';
|
|
16
|
+
// The CLI warns from 90 % on; a report below that is not worth a banner.
|
|
17
|
+
const WARNING_FROM = 0.9;
|
|
18
|
+
|
|
19
|
+
let account = null;
|
|
20
|
+
const pauses = new Map();
|
|
21
|
+
let currentConversationId = '';
|
|
22
|
+
let currentProviderType = '';
|
|
23
|
+
let actionInFlight = false;
|
|
24
|
+
let wakeTimer = null;
|
|
25
|
+
|
|
26
|
+
function isFuture(iso, nowMs) {
|
|
27
|
+
const at = Date.parse(String(iso || ''));
|
|
28
|
+
return Number.isFinite(at) && at > nowMs;
|
|
29
|
+
}
|
|
30
|
+
|
|
31
|
+
/** "22:01" today, "Thu 2 Oct, 09:00" on another day. */
|
|
32
|
+
export function formatUsageLimitTime(iso, nowMs = Date.now()) {
|
|
33
|
+
const at = new Date(String(iso || ''));
|
|
34
|
+
if (Number.isNaN(at.getTime())) return '';
|
|
35
|
+
const clock = at.toLocaleTimeString([], { hour: '2-digit', minute: '2-digit' });
|
|
36
|
+
if (new Date(nowMs).toDateString() === at.toDateString()) return clock;
|
|
37
|
+
const day = at.toLocaleDateString([], { weekday: 'short', day: 'numeric', month: 'short' });
|
|
38
|
+
return `${day}, ${clock}`;
|
|
39
|
+
}
|
|
40
|
+
|
|
41
|
+
/** The pause of a conversation while it still holds the turn back. */
|
|
42
|
+
export function activeUsageLimitPause(conversationId, nowMs = Date.now()) {
|
|
43
|
+
const pause = pauses.get(String(conversationId || '')) || null;
|
|
44
|
+
if (!pause) return null;
|
|
45
|
+
// One that resumes by itself is over once its time has come.
|
|
46
|
+
if (pause.auto && !isFuture(pause.resumeAt, nowMs)) return null;
|
|
47
|
+
return pause;
|
|
48
|
+
}
|
|
49
|
+
|
|
50
|
+
export function usageLimitPauseText(pause, nowMs = Date.now()) {
|
|
51
|
+
if (!pause) return '';
|
|
52
|
+
const label = String(pause.label || 'usage limit');
|
|
53
|
+
if (pause.auto) {
|
|
54
|
+
return `⏸ Paused at the Claude ${label} — carries on at ${formatUsageLimitTime(pause.resumeAt, nowMs)}`;
|
|
55
|
+
}
|
|
56
|
+
if (isFuture(pause.resetsAt, nowMs)) {
|
|
57
|
+
return `⏸ Paused at the Claude ${label} — resets ${formatUsageLimitTime(pause.resetsAt, nowMs)}`;
|
|
58
|
+
}
|
|
59
|
+
if (!pause.resetsAt) return `⏸ Paused at the Claude ${label}`;
|
|
60
|
+
return `⏸ Paused at the Claude ${label} — the limit has reset`;
|
|
61
|
+
}
|
|
62
|
+
|
|
63
|
+
/** The account's line: a warning before the limit, the reset at the limit. */
|
|
64
|
+
export function usageLimitAccountText(state, nowMs = Date.now()) {
|
|
65
|
+
if (!state || !isFuture(state.resetsAt, nowMs) || state.isUsingOverage) return '';
|
|
66
|
+
const label = String(state.label || 'usage limit');
|
|
67
|
+
const resets = formatUsageLimitTime(state.resetsAt, nowMs);
|
|
68
|
+
if (state.status === 'rejected') {
|
|
69
|
+
return `Claude ${label} reached — resets ${resets}. A turn sent before that is paused until then.`;
|
|
70
|
+
}
|
|
71
|
+
const utilization = Number(state.utilization);
|
|
72
|
+
if (state.status !== 'allowed_warning' || !Number.isFinite(utilization) || utilization < WARNING_FROM) return '';
|
|
73
|
+
return `Claude usage at ${Math.round(utilization * 100)} % of the ${label} — resets ${resets}`;
|
|
74
|
+
}
|
|
75
|
+
|
|
76
|
+
function dismissedKey(state) {
|
|
77
|
+
return state ? `${state.rateLimitType || ''}|${state.resetsAt || ''}|${state.status || ''}` : '';
|
|
78
|
+
}
|
|
79
|
+
|
|
80
|
+
function isDismissed(state) {
|
|
81
|
+
try { return localStorage.getItem(DISMISSED_KEY) === dismissedKey(state); } catch { return false; }
|
|
82
|
+
}
|
|
83
|
+
|
|
84
|
+
function button(label, className, onClick) {
|
|
85
|
+
const btn = document.createElement('button');
|
|
86
|
+
btn.type = 'button';
|
|
87
|
+
btn.className = `relay-toast-action ${className}`;
|
|
88
|
+
btn.textContent = label;
|
|
89
|
+
btn.disabled = actionInFlight;
|
|
90
|
+
btn.addEventListener('click', (event) => {
|
|
91
|
+
event.preventDefault();
|
|
92
|
+
event.stopPropagation();
|
|
93
|
+
void onClick();
|
|
94
|
+
});
|
|
95
|
+
return btn;
|
|
96
|
+
}
|
|
97
|
+
|
|
98
|
+
async function runPauseAction(action) {
|
|
99
|
+
const conversationId = currentConversationId;
|
|
100
|
+
if (actionInFlight || !conversationId) return;
|
|
101
|
+
actionInFlight = true;
|
|
102
|
+
renderUsageLimitBanner();
|
|
103
|
+
try {
|
|
104
|
+
const result = action === 'resume'
|
|
105
|
+
? await resumeUsageLimitPause(conversationId)
|
|
106
|
+
: await cancelUsageLimitPause(conversationId);
|
|
107
|
+
if (!result?.ok) {
|
|
108
|
+
showTransientRelayNotice(action === 'resume' ? 'Failed to resume the paused turn.' : 'Failed to cancel the paused turn.', 5000);
|
|
109
|
+
return;
|
|
110
|
+
}
|
|
111
|
+
pauses.delete(conversationId);
|
|
112
|
+
if (action === 'resume') {
|
|
113
|
+
showTransientRelayNotice(result.resumed ? 'Paused turn resumed.' : 'No turn was paused.', 4000);
|
|
114
|
+
} else {
|
|
115
|
+
showTransientRelayNotice(result.cancelled ? 'Paused turn cancelled.' : 'No turn was paused.', 4000);
|
|
116
|
+
}
|
|
117
|
+
} finally {
|
|
118
|
+
actionInFlight = false;
|
|
119
|
+
renderUsageLimitBanner();
|
|
120
|
+
}
|
|
121
|
+
}
|
|
122
|
+
|
|
123
|
+
function dismissWarning() {
|
|
124
|
+
try { localStorage.setItem(DISMISSED_KEY, dismissedKey(account)); } catch {}
|
|
125
|
+
renderUsageLimitBanner();
|
|
126
|
+
}
|
|
127
|
+
|
|
128
|
+
// The banner says nothing about time passing, but it ends with it: wake up
|
|
129
|
+
// when the turn carries on or the window resets.
|
|
130
|
+
function syncWakeTimer(nowMs) {
|
|
131
|
+
if (wakeTimer) { clearTimeout(wakeTimer); wakeTimer = null; }
|
|
132
|
+
const pause = pauses.get(currentConversationId) || null;
|
|
133
|
+
const times = [pause?.auto ? pause.resumeAt : pause?.resetsAt, account?.resetsAt]
|
|
134
|
+
.map((iso) => Date.parse(String(iso || '')))
|
|
135
|
+
.filter((at) => Number.isFinite(at) && at > nowMs);
|
|
136
|
+
if (!times.length) return;
|
|
137
|
+
const delay = Math.min(Math.min(...times) - nowMs + 500, 0x7fffffff);
|
|
138
|
+
wakeTimer = setTimeout(() => renderUsageLimitBanner(), delay);
|
|
139
|
+
wakeTimer?.unref?.();
|
|
140
|
+
}
|
|
141
|
+
|
|
142
|
+
export function renderUsageLimitBanner(nowMs = Date.now()) {
|
|
143
|
+
if (typeof document === 'undefined') return;
|
|
144
|
+
const banner = document.getElementById(BANNER_ID);
|
|
145
|
+
if (!banner) return;
|
|
146
|
+
const children = [];
|
|
147
|
+
const pause = activeUsageLimitPause(currentConversationId, nowMs);
|
|
148
|
+
if (pause) {
|
|
149
|
+
const text = document.createElement('span');
|
|
150
|
+
text.className = 'usage-limit-text';
|
|
151
|
+
text.textContent = usageLimitPauseText(pause, nowMs);
|
|
152
|
+
children.push(text, button('Resume now', 'usage-limit-resume', () => runPauseAction('resume')), button('Cancel', 'usage-limit-cancel', () => runPauseAction('cancel')));
|
|
153
|
+
banner.dataset.state = 'paused';
|
|
154
|
+
} else {
|
|
155
|
+
const line = currentConversationId && currentProviderType === 'claude' && !isDismissed(account)
|
|
156
|
+
? usageLimitAccountText(account, nowMs)
|
|
157
|
+
: '';
|
|
158
|
+
if (line) {
|
|
159
|
+
const text = document.createElement('span');
|
|
160
|
+
text.className = 'usage-limit-text';
|
|
161
|
+
text.textContent = line;
|
|
162
|
+
children.push(text, button('Hide', 'usage-limit-dismiss', async () => dismissWarning()));
|
|
163
|
+
banner.dataset.state = account.status === 'rejected' ? 'reached' : 'warning';
|
|
164
|
+
}
|
|
165
|
+
}
|
|
166
|
+
banner.replaceChildren(...children);
|
|
167
|
+
banner.classList.toggle('visible', children.length > 0);
|
|
168
|
+
banner.hidden = children.length === 0;
|
|
169
|
+
if (!children.length) delete banner.dataset.state;
|
|
170
|
+
syncWakeTimer(nowMs);
|
|
171
|
+
}
|
|
172
|
+
|
|
173
|
+
/** `usage_limit_pause`: one conversation's pause, or its end (`pause: null`). */
|
|
174
|
+
export function applyUsageLimitPause(payload) {
|
|
175
|
+
const conversationId = String(payload?.conversationId || '').trim();
|
|
176
|
+
if (!conversationId) return;
|
|
177
|
+
if (payload.pause && typeof payload.pause === 'object') pauses.set(conversationId, payload.pause);
|
|
178
|
+
else pauses.delete(conversationId);
|
|
179
|
+
renderUsageLimitBanner();
|
|
180
|
+
}
|
|
181
|
+
|
|
182
|
+
/** `claude_usage_limit`: the account's latest report, or null past its reset. */
|
|
183
|
+
export function applyClaudeUsageLimit(state) {
|
|
184
|
+
account = state && typeof state === 'object' ? state : null;
|
|
185
|
+
renderUsageLimitBanner();
|
|
186
|
+
}
|
|
187
|
+
|
|
188
|
+
/** GET /api/status carries the account and every pause. */
|
|
189
|
+
export function applyUsageLimitFromStatus(status) {
|
|
190
|
+
if (!status || typeof status !== 'object' || !('usageLimit' in status)) return;
|
|
191
|
+
const usageLimit = status.usageLimit || {};
|
|
192
|
+
account = usageLimit.account && typeof usageLimit.account === 'object' ? usageLimit.account : null;
|
|
193
|
+
pauses.clear();
|
|
194
|
+
for (const pause of (Array.isArray(usageLimit.pauses) ? usageLimit.pauses : [])) {
|
|
195
|
+
const conversationId = String(pause?.conversationId || '').trim();
|
|
196
|
+
if (conversationId) pauses.set(conversationId, pause);
|
|
197
|
+
}
|
|
198
|
+
renderUsageLimitBanner();
|
|
199
|
+
}
|
|
200
|
+
|
|
201
|
+
/** The conversation on screen, with what its payload said about a pause. */
|
|
202
|
+
export function setUsageLimitConversation(conversationId, { pause = undefined, providerType = '' } = {}) {
|
|
203
|
+
currentConversationId = String(conversationId || '').trim();
|
|
204
|
+
currentProviderType = String(providerType || '').trim().toLowerCase();
|
|
205
|
+
if (currentConversationId && pause !== undefined) {
|
|
206
|
+
if (pause && typeof pause === 'object') pauses.set(currentConversationId, pause);
|
|
207
|
+
else pauses.delete(currentConversationId);
|
|
208
|
+
}
|
|
209
|
+
renderUsageLimitBanner();
|
|
210
|
+
}
|
|
211
|
+
|
|
212
|
+
export function initUsageLimitUi() {
|
|
213
|
+
renderUsageLimitBanner();
|
|
214
|
+
}
|
|
215
|
+
|
|
216
|
+
export function __resetUsageLimitUiForTests() {
|
|
217
|
+
account = null;
|
|
218
|
+
pauses.clear();
|
|
219
|
+
currentConversationId = '';
|
|
220
|
+
currentProviderType = '';
|
|
221
|
+
actionInFlight = false;
|
|
222
|
+
if (wakeTimer) { clearTimeout(wakeTimer); wakeTimer = null; }
|
|
223
|
+
try { localStorage.removeItem(DISMISSED_KEY); } catch {}
|
|
224
|
+
renderUsageLimitBanner();
|
|
225
|
+
}
|