@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
@@ -43,6 +43,7 @@ import { readStoredClaudeContextUsage, resolveAutoCompactWindowStatus } from '..
43
43
  import { buildContextUsageView } from '../services/context-usage-view.mjs';
44
44
  import { cleanupGeneratedImagesForConversation as cleanupGeneratedImagesForConversationDefault } from '../services/generated-image-cleanup-service.mjs';
45
45
  import { stopSessionWorkerProcesses } from '../services/session-worker-stop-service.mjs';
46
+ import { isWorkerOnOtherBuild } from '../services/worker-recycle-service.mjs';
46
47
  import { createClaudeAuthService } from '../services/claude-auth-service.mjs';
47
48
  import { createCliInstallService } from '../services/cli-install-service.mjs';
48
49
  import { createGrokAuthService } from '../services/grok-auth-service.mjs';
@@ -69,7 +70,7 @@ import { redactPreviewTokens } from '../services/preview-registry-service.mjs';
69
70
  import { createSharePathRedactor } from '../services/share-path-redaction.mjs';
70
71
  import { createSubagentTimelineService, TIMELINE_READ_DEFAULT_LIMIT } from '../services/subagent-timeline-service.mjs';
71
72
  import { sanitizeSubagentRunId } from '../../shared/subagent-run-id.mjs';
72
- import { createKeyedMutex } from '../services/keyed-mutex-service.mjs';
73
+ import { sessionLaunchMutex } from '../services/session-launch-mutex-service.mjs';
73
74
  import { createFixedWindowRateLimiter } from '../services/rate-limit-service.mjs';
74
75
  import { requestHostOf } from '../services/request-boundary.mjs';
75
76
  import { parseRemoteRelayOriginJson } from '../repositories/remote-relay-repository.mjs';
@@ -77,6 +78,7 @@ import { createShareRepository } from '../repositories/share-repository.mjs';
77
78
  import { createShareAccessLog, readShareRequestViewer } from '../services/share-access-log-service.mjs';
78
79
  import { GROUP_NAME_MAX, resolveGroupDisplayName } from '../repositories/conversation-group-repository.mjs';
79
80
  import { conversationGroupPosition, conversationGroupRole, groupActivityTimes, pageConversationRows } from '../public/app/conversation-group-order.mjs';
81
+ import { detailsVersion, parseHeldDetailsParam, thoughtFingerprint } from '../public/app/held-details.mjs';
80
82
  import { admitRemoteRelayRequest, publicRemoteRelayOrigin, readRemoteRelayRequest } from '../services/remote-relay-inbound.mjs';
81
83
  import {
82
84
  RELAUNCH_COALESCE_WINDOW_MS,
@@ -1452,11 +1454,27 @@ export function buildSessionWorkerStatusPayload({
1452
1454
  // What the last read of the process list did ({ at, durationMs, processes,
1453
1455
  // error }); a slow box shows here before it shows as 502s.
1454
1456
  processList = null,
1457
+ // The relay's own build id (runtimeState.buildId): each worker's is shown
1458
+ // next to it, so a worker still on the code before a deploy stands out.
1459
+ relayBuildId = null,
1460
+ // The worker recycle's status() (stale count, recycles in flight and done,
1461
+ // who waits on what), or null while the recycle is not wired.
1462
+ recycleStatus = null,
1455
1463
  } = {}) {
1456
1464
  const snapshot = supervisorSnapshot && typeof supervisorSnapshot === 'object'
1457
1465
  ? supervisorSnapshot
1458
1466
  : {};
1459
- const workers = Array.isArray(snapshot.workers) ? snapshot.workers : [];
1467
+ const relayBuild = typeof relayBuildId === 'string' && relayBuildId.trim() ? relayBuildId.trim() : null;
1468
+ const workers = (Array.isArray(snapshot.workers) ? snapshot.workers : []).map((worker) => (
1469
+ worker && typeof worker === 'object'
1470
+ ? {
1471
+ ...worker,
1472
+ buildId: typeof worker.buildId === 'string' && worker.buildId ? worker.buildId : null,
1473
+ startedAt: worker.startedAt || null,
1474
+ stale: isWorkerOnOtherBuild(worker, relayBuild),
1475
+ }
1476
+ : worker
1477
+ ));
1460
1478
  const onlineWorkerStatuses = new Set(SESSION_WORKER_ACTIVE_STATUSES);
1461
1479
  const onlineCount = workers.reduce((count, worker) => {
1462
1480
  const status = normalizeWorkerStatusText(worker?.status, 'new');
@@ -1561,6 +1579,8 @@ export function buildSessionWorkerStatusPayload({
1561
1579
  onlineUnassignedProcessCount,
1562
1580
  counts: snapshot.counts && typeof snapshot.counts === 'object' ? snapshot.counts : {},
1563
1581
  workers,
1582
+ buildId: relayBuild,
1583
+ recycle: recycleStatus && typeof recycleStatus === 'object' ? recycleStatus : null,
1564
1584
  pendingStarts: toSafeNonNegativeInt(snapshot.pendingStarts, 0),
1565
1585
  lifecycle: Array.isArray(snapshot.lifecycle) ? snapshot.lifecycle : [],
1566
1586
  integrity,
@@ -2608,9 +2628,10 @@ export function registerSessionsRoutes(app, deps) {
2608
2628
  bucketTtlMs: 60_000,
2609
2629
  maxBuckets: 4_096,
2610
2630
  });
2611
- // Serializes everything that reaches the session-worker launch path, keyed by
2612
- // sdkSessionId, plus a coalescer so a duplicated request replays one result.
2613
- const sessionLaunchMutex = createKeyedMutex();
2631
+ // Everything that reaches the session-worker launch path is serialized by
2632
+ // the relay-wide sessionLaunchMutex (services/session-launch-mutex-service.mjs),
2633
+ // keyed by sdkSessionId, which the worker recycle takes too; plus a
2634
+ // coalescer so a duplicated request replays one result.
2614
2635
  const relaunchCoalescer = createRelaunchCoalescer({ windowMs: RELAUNCH_COALESCE_WINDOW_MS });
2615
2636
 
2616
2637
  function getDeepSeekProviderSettings() {
@@ -4686,6 +4707,59 @@ export function registerSessionsRoutes(app, deps) {
4686
4707
  });
4687
4708
 
4688
4709
  // GET /api/conversation/:id — get paginated conversation history
4710
+ // The details version of each finished reply (held-details.mjs), one query
4711
+ // per table for the whole window (the ids go in as one JSON parameter).
4712
+ // Tool rows are only inserted: their count and highest row id. Thought rows
4713
+ // are updated in place as they stream and when done: their rows are read
4714
+ // (id, done, text) and fingerprinted, so any change shows.
4715
+ let detailsVersionStatements = null;
4716
+ function relayDetailsVersions(messageIds) {
4717
+ const ids = [...new Set(messageIds.filter(Boolean))];
4718
+ const versions = new Map();
4719
+ if (!ids.length || typeof db?.prepare !== 'function') return versions;
4720
+ try {
4721
+ detailsVersionStatements ||= {
4722
+ activity: db.prepare(`
4723
+ SELECT response_message_id AS id, COUNT(*) AS n, MAX(rowid) AS max_rowid
4724
+ FROM relay_activity
4725
+ WHERE response_message_id IN (SELECT value FROM json_each(?))
4726
+ GROUP BY response_message_id
4727
+ `),
4728
+ thought: db.prepare(`
4729
+ SELECT response_message_id AS response_id, rowid AS row_id, done, text
4730
+ FROM relay_thought
4731
+ WHERE response_message_id IN (SELECT value FROM json_each(?))
4732
+ `),
4733
+ };
4734
+ const idsJson = JSON.stringify(ids);
4735
+ const activity = new Map();
4736
+ for (const row of detailsVersionStatements.activity.all(idsJson) || []) {
4737
+ activity.set(String(row?.id || ''), { n: Number(row.n) || 0, max: Number(row.max_rowid) || 0 });
4738
+ }
4739
+ const thoughtRows = new Map();
4740
+ for (const row of detailsVersionStatements.thought.all(idsJson) || []) {
4741
+ const id = String(row?.response_id || '');
4742
+ if (!thoughtRows.has(id)) thoughtRows.set(id, []);
4743
+ thoughtRows.get(id).push({ rowid: row.row_id, done: row.done, text: row.text });
4744
+ }
4745
+ for (const id of ids) {
4746
+ const tools = activity.get(id) || { n: 0, max: 0 };
4747
+ const thoughts = thoughtRows.get(id) || [];
4748
+ versions.set(id, detailsVersion({
4749
+ activityCount: tools.n,
4750
+ activityMaxRowId: tools.max,
4751
+ thoughtCount: thoughts.length,
4752
+ thoughtMaxRowId: thoughts.reduce((max, row) => Math.max(max, Number(row.rowid) || 0), 0),
4753
+ thoughtHash: thoughtFingerprint(thoughts),
4754
+ }));
4755
+ }
4756
+ } catch {
4757
+ // No versions: nothing is held, everything goes out in full.
4758
+ return new Map();
4759
+ }
4760
+ return versions;
4761
+ }
4762
+
4689
4763
  app.get('/api/conversation/:id', auth, (req, res) => {
4690
4764
  const requestedId = String(req.params.id || '').trim();
4691
4765
  const limit = normalizeConversationHistoryLimit(req.query.limit, 20);
@@ -4709,7 +4783,9 @@ export function registerSessionsRoutes(app, deps) {
4709
4783
  supportedRelayModes: SUPPORTED_RELAY_MODES,
4710
4784
  defaultRelayMode: DEFAULT_RELAY_MODE,
4711
4785
  });
4712
- const inFlight = inFlightStateForConversation(resolvedConversationId);
4786
+ // The running turn's tool lines: as many per thread as this page shows
4787
+ // (its "Tool lines shown" setting); none given, the relay's default.
4788
+ const inFlight = inFlightStateForConversation(resolvedConversationId, { activityCap: req.query.activityCap });
4713
4789
  const sdkSessionId = String(conv.sdk_session_id || resolvedConversationId || '').trim();
4714
4790
  const sessionUsageSummary = resolveSessionUsageSummaryForSdkSession({
4715
4791
  sdkSessionId,
@@ -4748,14 +4824,23 @@ export function registerSessionsRoutes(app, deps) {
4748
4824
  .map((row) => [String(row?.response_message_id || '').trim(), String(row?.id || '').trim()])
4749
4825
  .filter(([responseMessageId, sourceMessageId]) => !!responseMessageId && !!sourceMessageId),
4750
4826
  );
4827
+ // The live poll names the finished replies whose tool lines and thoughts
4828
+ // it holds (`heldDetails`, held-details.mjs); those whose details have
4829
+ // not changed since (the same `detailsVersion`) go out without them.
4830
+ const assistantIds = dbMessages.filter((m) => m.role === 'assistant').map((m) => String(m.id));
4831
+ const detailsVersions = relayDetailsVersions(assistantIds);
4832
+ const heldParam = parseHeldDetailsParam(req.query.heldDetails);
4833
+ const heldNow = new Set(heldParam
4834
+ ? assistantIds.filter((id) => heldParam.get(id) === detailsVersions.get(id))
4835
+ : []);
4751
4836
  const relayActivitiesByMessageId = new Map(
4752
4837
  dbMessages
4753
- .filter((m) => m.role === 'assistant')
4838
+ .filter((m) => m.role === 'assistant' && !heldNow.has(String(m.id)))
4754
4839
  .map((m) => [m.id, relayActivityForResponse(m.id)]),
4755
4840
  );
4756
4841
  const relayThoughtsByMessageId = new Map(
4757
4842
  dbMessages
4758
- .filter((m) => m.role === 'assistant')
4843
+ .filter((m) => m.role === 'assistant' && !heldNow.has(String(m.id)))
4759
4844
  .map((m) => [m.id, relayThoughtsForResponse ? relayThoughtsForResponse(m.id) : []]),
4760
4845
  );
4761
4846
  const subagentRunsByMessageId = new Map(
@@ -4796,8 +4881,14 @@ export function registerSessionsRoutes(app, deps) {
4796
4881
  });
4797
4882
  messages = messages.map((message) => {
4798
4883
  if (message.role !== 'assistant') return message;
4799
- const sourceMessageId = responseMessageToSourceId.get(String(message.id || '').trim()) || message.sourceMessageId || undefined;
4800
- return sourceMessageId ? { ...message, sourceMessageId } : message;
4884
+ const id = String(message.id || '').trim();
4885
+ const sourceMessageId = responseMessageToSourceId.get(id) || message.sourceMessageId || undefined;
4886
+ // Held: the page fills these from what it has. Otherwise the version
4887
+ // the page keeps with them.
4888
+ const details = heldNow.has(id)
4889
+ ? { activities: [], thoughts: [], detailsHeld: true }
4890
+ : (detailsVersions.has(id) ? { detailsVersion: detailsVersions.get(id) } : {});
4891
+ return { ...message, ...(sourceMessageId ? { sourceMessageId } : {}), ...details };
4801
4892
  });
4802
4893
  const history = sqlPage
4803
4894
  ? { messages, pageInfo: sqlPage.pageInfo }
@@ -6099,7 +6190,7 @@ export function registerSessionsRoutes(app, deps) {
6099
6190
  // Kill session stops the session's worker, and the delete counts no
6100
6191
  // tasks for a conversation whose worker is gone.
6101
6192
  message: activity.unstoppableTaskCount > 0
6102
- ? 'Background tasks are still running in this conversation, and some have no Stop button. Use Kill session in its ⋯ menu, then delete it.'
6193
+ ? 'Background tasks are still running in this conversation, and some have no Stop button. Use Kill session in its ︙ menu, then delete it.'
6103
6194
  : 'Background tasks are still running in this conversation. Stop them in the task panel, then delete it.',
6104
6195
  },
6105
6196
  };
@@ -6258,7 +6349,7 @@ export function registerSessionsRoutes(app, deps) {
6258
6349
  error: 'conversation-active',
6259
6350
  reason: 'background-tasks',
6260
6351
  message: activity.unstoppableTaskCount > 0
6261
- ? 'Background tasks are still running in this conversation, and some have no Stop button. Use Kill session in its ⋯ menu, then archive it.'
6352
+ ? 'Background tasks are still running in this conversation, and some have no Stop button. Use Kill session in its ︙ menu, then archive it.'
6262
6353
  : 'Background tasks are still running in this conversation. Stop them in the task panel, then archive it.',
6263
6354
  },
6264
6355
  };
@@ -7129,6 +7220,18 @@ export function registerSessionsRoutes(app, deps) {
7129
7220
  }
7130
7221
  });
7131
7222
 
7223
+ // The worker recycle's status, read at request time: the recycle is set on
7224
+ // runtimeState after the routes are registered, and a status that throws
7225
+ // must not take /api/status down with it.
7226
+ function readWorkerRecycleStatus() {
7227
+ if (typeof runtimeState.workerRecycle !== 'function') return null;
7228
+ try {
7229
+ return runtimeState.workerRecycle() || null;
7230
+ } catch {
7231
+ return null;
7232
+ }
7233
+ }
7234
+
7132
7235
  // GET /api/status — overall status
7133
7236
  app.get('/api/status', auth, (req, res) => {
7134
7237
  ensureSessionId(req, res);
@@ -7146,6 +7249,8 @@ export function registerSessionsRoutes(app, deps) {
7146
7249
  supervisorSnapshot: sessionWorkerSupervisor?.snapshot?.({ pendingQuestionSessionIds }) || null,
7147
7250
  queueRows: listSessionWorkerQueueRows.all(...SESSION_WORKER_STATUS_QUEUE_STATES),
7148
7251
  processList: sessionWorkerProcessInspector?.getLastRead?.() || null,
7252
+ relayBuildId: runtimeState.buildId ?? null,
7253
+ recycleStatus: readWorkerRecycleStatus(),
7149
7254
  });
7150
7255
  const activeRuntimeSessionCount = featureFlags?.SESSION_WORKER_ROUTING_ENABLED === true
7151
7256
  ? Number(sessionWorkerStatus?.onlineBoundProcessCount || 0)