@oar-sh/oar 0.9.20 → 0.9.22

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 (131) hide show
  1. package/CHANGELOG.md +325 -0
  2. package/README.md +13 -13
  3. package/bin/oar.js +2 -2
  4. package/package.json +1 -1
  5. package/server/claude-cloud-worker/claude-cloud-session-process.mjs +231 -1
  6. package/server/claude-cloud-worker/claude-cloud-session-worker.mjs +31 -2
  7. package/server/claude-cloud-worker/claude-cloud-worker-steering.mjs +34 -0
  8. package/server/claude-worker/claude-sdk-adapter.mjs +149 -4
  9. package/server/claude-worker/claude-session-process.mjs +16 -4
  10. package/server/claude-worker/claude-session-worker-main.mjs +18 -2
  11. package/server/copilot-worker/copilot-sdk-session-process.mjs +75 -1
  12. package/server/copilot-worker/copilot-sdk-session-worker.mjs +18 -2
  13. package/server/cursor-worker/cursor-session-worker.mjs +21 -2
  14. package/server/cursor-worker/cursor-worker-steering.mjs +20 -0
  15. package/server/db-schema.mjs +19 -0
  16. package/server/grok-worker/grok-background-tasks.mjs +10 -0
  17. package/server/grok-worker/grok-session-worker.mjs +22 -2
  18. package/server/grok-worker/grok-turn-runner.mjs +4 -0
  19. package/server/grok-worker/grok-worker-steering.mjs +23 -0
  20. package/server/migrations/0037-relay-asks-queue-id.mjs +66 -0
  21. package/server/migrations/0038-turn-seq.mjs +92 -0
  22. package/server/public/app/about-ui.js +1 -1
  23. package/server/public/app/about-view.mjs +19 -0
  24. package/server/public/app/action-confirmations.js +1 -1
  25. package/server/public/app/activity-replay-state.mjs +160 -42
  26. package/server/public/app/api-client.js +11 -2
  27. package/server/public/app/ask-user-view.js +24 -9
  28. package/server/public/app/attachments-view.js +1 -1
  29. package/server/public/app/back-guard.mjs +31 -5
  30. package/server/public/app/bootstrap.js +618 -72
  31. package/server/public/app/chat-actions-menu-state.mjs +141 -4
  32. package/server/public/app/chat-header-meta.mjs +27 -0
  33. package/server/public/app/claude-billing-toggle-ui.js +2 -1
  34. package/server/public/app/claude-cloud-conversation-ui.js +2 -2
  35. package/server/public/app/composer-settings-label.mjs +90 -0
  36. package/server/public/app/composer-settings-ui.js +442 -0
  37. package/server/public/app/context-menu.mjs +36 -5
  38. package/server/public/app/context-usage-view.mjs +68 -0
  39. package/server/public/app/conversation-deep-link.mjs +15 -11
  40. package/server/public/app/conversation-group-drag.mjs +12 -2
  41. package/server/public/app/conversation-group-order.mjs +9 -2
  42. package/server/public/app/conversation-group-state.mjs +47 -10
  43. package/server/public/app/conversation-origin-ui.js +17 -1
  44. package/server/public/app/conversation-view.js +309 -49
  45. package/server/public/app/cwd-menu-placement.mjs +18 -6
  46. package/server/public/app/cwd-picker.js +36 -6
  47. package/server/public/app/emoji-view.js +1 -1
  48. package/server/public/app/font-scaling.js +26 -0
  49. package/server/public/app/header-priority-actions.mjs +238 -0
  50. package/server/public/app/held-details.mjs +202 -0
  51. package/server/public/app/hidden-messages-view.mjs +4 -0
  52. package/server/public/app/inbox-ui.js +88 -81
  53. package/server/public/app/inbox-view.mjs +35 -19
  54. package/server/public/app/journal-view.js +328 -58
  55. package/server/public/app/live-activity-cap.mjs +103 -0
  56. package/server/public/app/phone-landscape-ui.js +225 -0
  57. package/server/public/app/phone-landscape.mjs +99 -0
  58. package/server/public/app/phone-typing-state.mjs +104 -0
  59. package/server/public/app/pinned-messages-view.mjs +5 -0
  60. package/server/public/app/push-settings.js +2 -2
  61. package/server/public/app/pwa-install.js +24 -3
  62. package/server/public/app/questions-panel-ui.js +634 -0
  63. package/server/public/app/questions-stub-view.mjs +40 -0
  64. package/server/public/app/questions-view.mjs +89 -0
  65. package/server/public/app/relay-board-view.js +12 -2
  66. package/server/public/app/relay-event-subscriptions-ui.js +71 -22
  67. package/server/public/app/relay-event-view.mjs +38 -0
  68. package/server/public/app/relay-restart-ui.js +2 -2
  69. package/server/public/app/relay-schedule-view.mjs +32 -6
  70. package/server/public/app/relay-schedules-ui.js +69 -20
  71. package/server/public/app/relay-thoughts.mjs +15 -1
  72. package/server/public/app/remote-relay-origin-view.mjs +41 -23
  73. package/server/public/app/session-history-ui.js +251 -0
  74. package/server/public/app/session-history.mjs +599 -0
  75. package/server/public/app/settings-modal.js +67 -6
  76. package/server/public/app/settings-tabs.js +12 -0
  77. package/server/public/app/share-state.js +1 -1
  78. package/server/public/app/shared-conversations-settings.js +2 -2
  79. package/server/public/app/socket-handlers.js +47 -13
  80. package/server/public/app/status-view.mjs +18 -2
  81. package/server/public/app/store.js +87 -16
  82. package/server/public/app/stream-view-preference.mjs +60 -0
  83. package/server/public/app/subagent-fold.mjs +6 -3
  84. package/server/public/app/subagent-timeline-view.mjs +12 -5
  85. package/server/public/app/subagent-viewer.mjs +35 -8
  86. package/server/public/app/tab-strip-fade.mjs +101 -0
  87. package/server/public/app/tap-actions.js +66 -0
  88. package/server/public/app/thoughts-preference.mjs +79 -0
  89. package/server/public/app/turn-stream-view.mjs +124 -0
  90. package/server/public/app/usage-limit-ui.js +63 -2
  91. package/server/public/app/viewer-history.mjs +13 -2
  92. package/server/public/index.html +2603 -1767
  93. package/server/relay-tools.md +1 -0
  94. package/server/repositories/message-repository.mjs +33 -0
  95. package/server/repositories/question-repository.mjs +51 -7
  96. package/server/repositories/relay-turn-rows.mjs +56 -0
  97. package/server/repositories/session-repository.mjs +19 -0
  98. package/server/routes/ask-user-routes.mjs +34 -5
  99. package/server/routes/claude-cloud-routes.mjs +19 -0
  100. package/server/routes/messages-routes.mjs +92 -15
  101. package/server/routes/relay-ask-routes.mjs +15 -5
  102. package/server/routes/sessions-routes.mjs +117 -12
  103. package/server/server-runtime.mjs +367 -170
  104. package/server/services/about-service.mjs +22 -53
  105. package/server/services/claude-billing-swap-service.mjs +7 -20
  106. package/server/services/claude-cloud-session-service.mjs +47 -5
  107. package/server/services/cloudflared-tunnel-service.mjs +7 -0
  108. package/server/services/idle-timers.mjs +184 -0
  109. package/server/services/queue-wake-hook.mjs +145 -0
  110. package/server/services/queue-waker.mjs +127 -0
  111. package/server/services/relay-ask-service.mjs +64 -5
  112. package/server/services/relay-event-service.mjs +4 -1
  113. package/server/services/remote-relay-dispatcher.mjs +84 -18
  114. package/server/services/session-activity-tracker.mjs +1 -1
  115. package/server/services/session-history-refresh-service.mjs +4 -0
  116. package/server/services/session-launch-mutex-service.mjs +23 -0
  117. package/server/services/session-worker-registry-service.mjs +36 -0
  118. package/server/services/session-worker-supervisor-service.mjs +36 -0
  119. package/server/services/session-worker-websocket-service.mjs +183 -31
  120. package/server/services/ssh-tunnel-manager-service.mjs +7 -0
  121. package/server/services/worker-recycle-service.mjs +772 -0
  122. package/server/services/worker-replacement.mjs +197 -0
  123. package/shared/ask-user-bridge.mjs +9 -6
  124. package/shared/build-id.mjs +128 -0
  125. package/shared/relay-asks.mjs +10 -0
  126. package/shared/relay-events.mjs +1 -0
  127. package/shared/worker-runtime/heartbeat.mjs +31 -0
  128. package/shared/worker-runtime/simple-steering-state.mjs +38 -0
  129. package/shared/worker-runtime/worker-keepalive.mjs +108 -0
  130. package/shared/worker-runtime/worker-websocket-link.mjs +44 -10
  131. package/shared/worker-runtime/working-directory.mjs +1 -1
@@ -1,3 +1,5 @@
1
+ import { buildIdsDiffer } from "../build-id.mjs";
2
+
1
3
  function normalizeText(value) {
2
4
  const text = String(value || "").trim();
3
5
  return text || null;
@@ -61,15 +63,23 @@ export function createWorkerWebSocketLink({
61
63
  getDeliveryHeld = null,
62
64
  // Called with the hello's reason once per connection, on the relay's first
63
65
  // server.hello: from then on the relay (possibly a restarted one that knows
64
- // nothing of this worker) has the socket. Later hellos only ack the
65
- // worker.hello every idle readiness refresh re-sends, and are not reported.
66
+ // nothing of this worker) has the socket. Later hellos only ack a later
67
+ // worker.hello, and are not reported.
66
68
  onServerHello = () => {},
69
+ // Called with "server-hello" on every server.hello and with "deliver" on
70
+ // every queue.deliver: the keepalive keeps its busy cadence for a while
71
+ // after either (worker-keepalive.mjs).
72
+ onRelayFrame = () => {},
67
73
  getSessionId = () => null,
68
74
  getPid = () => null,
69
75
  // The kind of worker this is ("claude", "claude-api", ...), stated in
70
76
  // every hello and ready: the relay hands a Claude chat's row only to a
71
77
  // worker of the row's billing.
72
78
  workerKind = null,
79
+ // The build id of the code this worker started on (shared/build-id.mjs),
80
+ // stated in every hello and ready next to the kind: the relay recycles a
81
+ // worker whose build differs from its own once the worker is idle.
82
+ buildId = null,
73
83
  minBackoffMs = 1000,
74
84
  maxBackoffMs = 8_000,
75
85
  jitterMs = 250,
@@ -82,8 +92,12 @@ export function createWorkerWebSocketLink({
82
92
  // Failed attempts after the first few are logged at most once per this
83
93
  // interval, with a count of the ones in between.
84
94
  failureLogIntervalMs = 60_000,
95
+ // The link's own readiness refresh; 0 leaves it to the caller, which then
96
+ // calls refreshReadiness itself (worker-keepalive.mjs does, on its beat).
85
97
  readyRefreshMs = 10_000,
86
- staleConnectionMs = 30_000,
98
+ // Three missed idle beats (30 s each) without any word from the relay: the
99
+ // connection is given up and made again.
100
+ staleConnectionMs = 90_000,
87
101
  WebSocketImpl = globalThis.WebSocket,
88
102
  setTimeoutImpl = setTimeout,
89
103
  clearTimeoutImpl = clearTimeout,
@@ -121,6 +135,12 @@ export function createWorkerWebSocketLink({
121
135
  let lastPongAt = null;
122
136
  let lastQueueChangedAt = null;
123
137
  let lastDeliveryAt = null;
138
+ // The build id the relay named in its latest server.hello (null when it
139
+ // named none), and whether this connection has already logged that it
140
+ // differs from ours: the relay acks every worker.hello with another
141
+ // server.hello, and the log needs the news only once per connection.
142
+ let relayBuildId = null;
143
+ let buildMismatchLogged = false;
124
144
 
125
145
  function getNowMs() {
126
146
  const value = Number(now());
@@ -175,6 +195,7 @@ export function createWorkerWebSocketLink({
175
195
  sessionId: normalizeText(getSessionId()),
176
196
  pid: normalizePositiveInt(getPid()),
177
197
  ...(normalizeText(workerKind) ? { kind: normalizeText(workerKind) } : {}),
198
+ ...(normalizeText(buildId) ? { buildId: normalizeText(buildId) } : {}),
178
199
  ...(held ? { ready: false } : {}),
179
200
  });
180
201
  if (sent) lastHelloSentAt = getNowMs();
@@ -203,6 +224,7 @@ export function createWorkerWebSocketLink({
203
224
  sessionId: normalizeText(getSessionId()),
204
225
  pid: normalizePositiveInt(getPid()),
205
226
  ...(normalizeText(workerKind) ? { kind: normalizeText(workerKind) } : {}),
227
+ ...(normalizeText(buildId) ? { buildId: normalizeText(buildId) } : {}),
206
228
  });
207
229
  if (sent) lastReadySentAt = getNowMs();
208
230
  return sent;
@@ -295,19 +317,17 @@ export function createWorkerWebSocketLink({
295
317
  notifyUnready(reason);
296
318
  return true;
297
319
  }
298
- if (deliveriesInFlight.size) {
299
- // notifyReady gates itself: it only goes through mid-delivery when the
300
- // worker is steering-ready. Hello stays an idle-only affair.
301
- void notifyReady(reason);
302
- return true;
303
- }
304
- notifyHello(reason);
320
+ // notifyReady gates itself: mid-delivery it only goes through when the
321
+ // worker is steering-ready. No hello: the socket's identity, kind and
322
+ // build were bound by the hello at open, and a ready carries them again;
323
+ // a hello would only draw a second server.hello and a second ready.
305
324
  void notifyReady(reason);
306
325
  return true;
307
326
  }
308
327
 
309
328
  function startReadyRefreshTimer() {
310
329
  clearReadyRefreshTimer();
330
+ if (Number(readyRefreshMs) === 0) return;
311
331
  const intervalMs = Math.max(1000, Number(readyRefreshMs) || 10_000);
312
332
  readyRefreshTimer = setIntervalImpl(() => {
313
333
  refreshReadiness("readiness-refresh");
@@ -393,6 +413,7 @@ export function createWorkerWebSocketLink({
393
413
  clearReadyRefreshTimer();
394
414
  // The next connection may reach a different (restarted) relay.
395
415
  serverCapabilities = new Set();
416
+ buildMismatchLogged = false;
396
417
  }
397
418
  if (stopped || !reconnect) return true;
398
419
  // A newer socket is already under way and owns the reconnect.
@@ -434,12 +455,21 @@ export function createWorkerWebSocketLink({
434
455
  return;
435
456
  }
436
457
  if (payload?.type === "queue.deliver") {
458
+ try { onRelayFrame("deliver"); } catch {}
437
459
  void deliverPending(payload.pending || null, String(payload.reason || "queue-deliver"))
438
460
  .finally(() => notifyReady("delivery-complete"));
439
461
  return;
440
462
  }
441
463
  if (payload?.type === "server.hello") {
464
+ try { onRelayFrame("server-hello"); } catch {}
442
465
  serverCapabilities = new Set(Array.isArray(payload.capabilities) ? payload.capabilities.map(String) : []);
466
+ relayBuildId = normalizeText(payload.buildId);
467
+ // Only a line in the log: the relay decides when to replace this
468
+ // worker, and the worker takes no action of its own on it.
469
+ if (!buildMismatchLogged && buildIdsDiffer(relayBuildId, normalizeText(buildId))) {
470
+ buildMismatchLogged = true;
471
+ dbg(`relay build ${relayBuildId}, mine ${normalizeText(buildId)}: the relay will recycle this worker when idle`);
472
+ }
443
473
  if (!serverHelloSeen) {
444
474
  serverHelloSeen = true;
445
475
  // The handshake is complete: only now does the backoff start over.
@@ -536,12 +566,16 @@ export function createWorkerWebSocketLink({
536
566
  lastPongAt,
537
567
  lastQueueChangedAt,
538
568
  lastDeliveryAt,
569
+ relayBuildId,
539
570
  };
540
571
  }
541
572
 
542
573
  return {
543
574
  notifyReady,
544
575
  notifyUnready,
576
+ refreshReadiness,
577
+ // A delivery in flight or a hold: the keepalive beats at its busy cadence.
578
+ isBusy: () => deliveriesInFlight.size > 0 || deliveryHeld(),
545
579
  serverSupports: (capability) => serverCapabilities.has(String(capability || "")),
546
580
  start,
547
581
  stop,
@@ -16,7 +16,7 @@ import fs from 'node:fs';
16
16
  export const WORKING_DIRECTORY_MISSING_CODE = 'working-directory-missing';
17
17
 
18
18
  export const WORKING_DIRECTORY_GUIDANCE =
19
- 'Restore the folder, or choose another working directory for this conversation (⋯ → Change CWD), '
19
+ 'Restore the folder, or choose another working directory for this conversation (︙ → Change CWD), '
20
20
  + 'then send the message again.';
21
21
 
22
22
  // What a failed launch says, in the words of the SDKs: Node's spawn error, the