@oar-sh/oar 0.9.8 → 0.9.10

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 (107) hide show
  1. package/CHANGELOG.md +478 -0
  2. package/README.md +129 -39
  3. package/bin/bat/oar.bat +46 -46
  4. package/bin/oar.js +814 -228
  5. package/package.json +5 -6
  6. package/server/README.md +306 -4
  7. package/server/claude-worker/claude-remote-relay-tool.mjs +10 -0
  8. package/server/claude-worker/claude-session-process.mjs +35 -1
  9. package/server/claude-worker/claude-turn-publisher.mjs +3 -0
  10. package/server/claude-worker/sdk-message-normalizer.mjs +31 -4
  11. package/server/claude-worker/subagent-timeline-recorder.mjs +572 -0
  12. package/server/copilot-worker/copilot-sdk-session-process.mjs +3 -1
  13. package/server/cursor-worker/cursor-sdk-adapter.mjs +56 -2
  14. package/server/db-schema.mjs +44 -0
  15. package/server/migrations/0008-message-pins.mjs +85 -0
  16. package/server/migrations/0009-subagent-timeline.mjs +135 -0
  17. package/server/migrations/0010-subagent-timeline-pruning.mjs +89 -0
  18. package/server/migrations/0011-relay-event-subscriptions.mjs +125 -0
  19. package/server/migrations/0012-cross-relay-event-subscriptions.mjs +116 -0
  20. package/server/public/app/agent-exchange-fold-view.mjs +248 -0
  21. package/server/public/app/api-client.js +91 -0
  22. package/server/public/app/archive-confirm.mjs +28 -0
  23. package/server/public/app/ask-user-view.js +3 -0
  24. package/server/public/app/attachments-view.js +1024 -131
  25. package/server/public/app/back-guard.mjs +58 -0
  26. package/server/public/app/background-tasks-view.mjs +18 -26
  27. package/server/public/app/bootstrap.js +230 -35
  28. package/server/public/app/chat-actions-menu-state.mjs +116 -0
  29. package/server/public/app/claude-cloud-conversation-ui.js +10 -22
  30. package/server/public/app/composer-control-state.mjs +16 -0
  31. package/server/public/app/composer-keyboard.mjs +54 -0
  32. package/server/public/app/conversation-view.js +471 -110
  33. package/server/public/app/explorer-edit-state.mjs +68 -0
  34. package/server/public/app/external-link-policy.mjs +4 -1
  35. package/server/public/app/file-version.mjs +58 -0
  36. package/server/public/app/host-suspend-ui.js +6 -3
  37. package/server/public/app/journal-view.js +161 -27
  38. package/server/public/app/no-conversation-state.mjs +58 -0
  39. package/server/public/app/pinned-messages-view.mjs +253 -0
  40. package/server/public/app/provider-readiness.mjs +120 -0
  41. package/server/public/app/pwa-install.js +1 -1
  42. package/server/public/app/relay-board-view.js +3 -0
  43. package/server/public/app/relay-dot-state.mjs +8 -1
  44. package/server/public/app/relay-event-subscriptions-ui.js +184 -0
  45. package/server/public/app/relay-event-view.mjs +234 -0
  46. package/server/public/app/router.js +8 -2
  47. package/server/public/app/settings-modal.js +53 -1
  48. package/server/public/app/socket-handlers.js +65 -28
  49. package/server/public/app/store.js +25 -2
  50. package/server/public/app/subagent-row.mjs +138 -0
  51. package/server/public/app/subagent-timeline-view.mjs +186 -0
  52. package/server/public/app/subagent-viewer-watch.mjs +50 -0
  53. package/server/public/app/subagent-viewer.mjs +534 -0
  54. package/server/public/app/task-row-format.mjs +29 -0
  55. package/server/public/app/thinking-follow.js +4 -0
  56. package/server/public/app/transcript-jump-bottom-view.mjs +290 -0
  57. package/server/public/app/transcript-jump-bottom.mjs +99 -0
  58. package/server/public/app/transcript-reload-freshness.mjs +25 -0
  59. package/server/public/app/update-ui.js +1 -1
  60. package/server/public/app/viewer-chat-gallery.mjs +59 -0
  61. package/server/public/app/viewer-controls-state.mjs +59 -0
  62. package/server/public/app/viewer-gallery.mjs +46 -0
  63. package/server/public/app/viewer-gestures.mjs +105 -0
  64. package/server/public/app/viewer-history.mjs +45 -0
  65. package/server/public/app/viewer-tap.mjs +112 -0
  66. package/server/public/index.html +700 -59
  67. package/server/repositories/message-pin-statements.mjs +39 -0
  68. package/server/repositories/message-repository.mjs +34 -3
  69. package/server/repositories/question-repository.mjs +9 -3
  70. package/server/repositories/remote-relay-repository.mjs +6 -2
  71. package/server/repositories/subagent-timeline-statements.mjs +170 -0
  72. package/server/routes/ask-user-routes.mjs +3 -0
  73. package/server/routes/messages-routes.mjs +548 -111
  74. package/server/routes/relay-event-routes.mjs +157 -0
  75. package/server/routes/remote-relay-routes.mjs +15 -0
  76. package/server/routes/sessions-routes.mjs +164 -24
  77. package/server/server-runtime.mjs +274 -62
  78. package/server/services/cloudflared-tunnel-service.mjs +139 -41
  79. package/server/services/explorer-entry-name.mjs +26 -0
  80. package/server/services/explorer-mutations.mjs +122 -0
  81. package/server/services/file-serving.mjs +155 -0
  82. package/server/services/message-pin-list.mjs +52 -0
  83. package/server/services/oar-cli-helpers.mjs +139 -4
  84. package/server/services/oar-cli-menu.mjs +240 -0
  85. package/server/services/oar-cli-wsl.mjs +129 -0
  86. package/server/services/preview-proxy-server.mjs +41 -23
  87. package/server/services/push-dispatch-service.mjs +18 -0
  88. package/server/services/relay-event-link-service.mjs +289 -0
  89. package/server/services/relay-event-service.mjs +1935 -0
  90. package/server/services/relay-shutdown-gate.mjs +31 -0
  91. package/server/services/remote-relay-caller-context.mjs +52 -7
  92. package/server/services/remote-relay-dispatcher.mjs +494 -2
  93. package/server/services/remote-relay-registry-service.mjs +11 -1
  94. package/server/services/safe-served-content.mjs +61 -11
  95. package/server/services/session-activity-tracker.mjs +487 -0
  96. package/server/services/session-history-refresh-service.mjs +31 -0
  97. package/server/services/stale-attempt-log-limiter.mjs +96 -0
  98. package/server/services/stale-recovery-clock-guard.mjs +57 -0
  99. package/server/services/stale-subagent-run-sweep.mjs +109 -0
  100. package/server/services/subagent-record-retention.mjs +164 -0
  101. package/server/services/subagent-timeline-service.mjs +328 -0
  102. package/server/services/subagent-viewer-rooms.mjs +119 -0
  103. package/server/services/upgrade-socket-guard.mjs +16 -0
  104. package/shared/relay-events.mjs +371 -0
  105. package/shared/remote-relay-contract.mjs +157 -5
  106. package/shared/subagent-record-retention.mjs +31 -0
  107. package/shared/subagent-timeline.mjs +79 -0
@@ -0,0 +1,31 @@
1
+ 'use strict';
2
+
3
+ // A queued relay shutdown or restart waits for the turns that are processing,
4
+ // and only for those. While it waits, no pending row is handed to a worker:
5
+ // a new turn would postpone the restart again, and on a busy relay forever
6
+ // (live: a turn started after the restart was queued, each time). Pending and
7
+ // parked rows stay in the database and run after the restart.
8
+
9
+ export const RELAY_SHUTDOWN_QUEUED_REASON = 'relay_shutdown_queued';
10
+
11
+ /** Whether the runtime holds a shutdown or restart request that has not started yet. */
12
+ export function isRelayShutdownQueued(runtimeState) {
13
+ if (!runtimeState) return false;
14
+ if (runtimeState.relayShutdownQueued === true) return true;
15
+ return String(runtimeState.relayShutdown?.status || '') === 'queued';
16
+ }
17
+
18
+ /** The queued shutdown may go ahead: nothing is processing (pending and parked rows wait it out). */
19
+ export function queuedShutdownCanProceed(counts) {
20
+ return Number(counts?.processingCount || 0) <= 0;
21
+ }
22
+
23
+ /** What a worker asking for work gets while a shutdown is queued: nothing, and why. */
24
+ export function relayShutdownGateReply(runtimeState) {
25
+ return {
26
+ message: null,
27
+ paused: true,
28
+ reason: RELAY_SHUTDOWN_QUEUED_REASON,
29
+ relayShutdown: runtimeState?.relayShutdown || null,
30
+ };
31
+ }
@@ -35,6 +35,7 @@ const LOG_PREFIX = '[remote-relays]';
35
35
  const APPROVAL_HEADER = 'Remote relay';
36
36
  // The once-per-conversation card for sessions an agent starts on this relay.
37
37
  const LOCAL_SESSIONS_HEADER = 'Agent sessions';
38
+ const WATCH_HEADER = 'Relay events';
38
39
  const APPROVAL_ALLOW = 'Allow';
39
40
  const APPROVAL_DENY = 'Deny';
40
41
  const EXCERPT_MAX = 200;
@@ -137,7 +138,42 @@ function describeLocalSessionTarget(args) {
137
138
  * agent wants to start sessions on this relay itself. Allowing it covers the
138
139
  * later ones of that conversation, so the card says what they cost.
139
140
  */
140
- export function formatRemoteRelayApprovalPrompt({ relay, action, args = {}, caller = null, agentPrompts = 0, localSessions = false } = {}) {
141
+ /**
142
+ * The card for relay event subscriptions (`watch`): the first subscribe of a
143
+ * conversation (`kind: 'subscribe'`, with the sessions or "all"), or a
144
+ * transfer of another conversation's subscriptions (`kind: 'transfer'`).
145
+ * "All sessions" is always spelled out.
146
+ */
147
+ function formatWatchApprovalPrompt(watch) {
148
+ // A paired relay's sessions: named with it, so the user knows where.
149
+ const relayName = toText(watch.relayName);
150
+ const where = relayName ? `relay "${excerpt(relayName)}"` : 'this relay';
151
+ if (watch.kind === 'transfer') {
152
+ const from = watch.from || {};
153
+ const title = toText(from.title) ? `“${excerpt(from.title)}” ` : '';
154
+ const lines = [`This agent wants to take over ${watch.count} event subscription${watch.count === 1 ? '' : 's'} from session ${title}(${shortId(from.id)}) → this session. The old session stops getting their events.`];
155
+ const relays = (Array.isArray(watch.relays) ? watch.relays : []).map((name) => `"${excerpt(name)}"`);
156
+ if (relays.length) lines.push(`Among them: subscriptions on relay${relays.length === 1 ? '' : 's'} ${relays.join(', ')}.`);
157
+ if (watch.all) lines.push(`Among them: ALL sessions of ${relayName ? where : 'a relay'}, including ones created later.`);
158
+ return lines.join('\n\n');
159
+ }
160
+ const lines = [];
161
+ if (watch.all) {
162
+ lines.push(`This agent wants to watch ALL sessions of ${where}, including ones created later, and be woken by their events.`);
163
+ } else {
164
+ const sessions = (Array.isArray(watch.sessions) ? watch.sessions : []).slice(0, 10)
165
+ .map((session) => (toText(session?.title) ? `“${excerpt(session.title)}” (${shortId(session.id)})` : shortId(session?.id)));
166
+ const more = (watch.sessions?.length || 0) - sessions.length;
167
+ lines.push(`This agent wants to watch ${sessions.join(', ')}${more > 0 ? ` and ${more} more` : ''}${relayName ? ` on ${where}` : ''} and be woken by their events.`);
168
+ }
169
+ const events = (Array.isArray(watch.events) ? watch.events : []).join(', ');
170
+ const extras = [watch.once ? 'once' : '', Number(watch.heartbeatMinutes) > 0 ? `heartbeat every ${watch.heartbeatMinutes} min` : ''].filter(Boolean);
171
+ if (events) lines.push(`Events: ${events}${extras.length ? ` · ${extras.join(' · ')}` : ''}`);
172
+ return lines.join('\n\n');
173
+ }
174
+
175
+ export function formatRemoteRelayApprovalPrompt({ relay, action, args = {}, caller = null, agentPrompts = 0, localSessions = false, watch = null } = {}) {
176
+ if (watch && typeof watch === 'object') return formatWatchApprovalPrompt(watch);
141
177
  const local = relay?.local === true;
142
178
  const name = toText(relay?.name) || 'a remote relay';
143
179
  const where = local ? 'this relay' : `relay "${name}"`;
@@ -317,23 +353,25 @@ export function createRemoteRelayCallerContext({
317
353
  * `localSessions` makes it the card for starting sessions on this relay
318
354
  * itself, asked once per conversation (the dispatcher remembers the Allow).
319
355
  */
320
- async function requestApproval({ conversationId, callerContext, relay, action, args = {}, signal, agentPrompts = 0, localSessions = false } = {}) {
356
+ async function requestApproval({ conversationId, callerContext, relay, action, args = {}, signal, agentPrompts = 0, localSessions = false, watch = null } = {}) {
321
357
  const rowId = toText(callerContext?.processingRowId);
322
358
  const row = rowId ? getQueueRow.get(rowId) : null;
323
359
  if (!row || row.status !== 'processing') {
324
360
  return {
325
361
  approved: false,
326
362
  code: REMOTE_RELAY_ERROR_CODES.noTurn,
327
- error: localSessions
328
- ? 'Starting sessions on this relay needs the user\'s approval once per conversation, but the conversation has no running turn to ask on.'
329
- : 'This write action needs the user\'s approval, but the conversation has no running turn to ask on.',
363
+ error: watch
364
+ ? 'Watching sessions needs the user\'s approval, but the conversation has no running turn to ask on.'
365
+ : (localSessions
366
+ ? 'Starting sessions on this relay needs the user\'s approval once per conversation, but the conversation has no running turn to ask on.'
367
+ : 'This write action needs the user\'s approval, but the conversation has no running turn to ask on.'),
330
368
  };
331
369
  }
332
370
 
333
371
  const createdAt = now().toISOString();
334
372
  const questionId = String(uuid());
335
373
  const relayMode = normalizeMode(row.relay_mode);
336
- const prompt = formatRemoteRelayApprovalPrompt({ relay, action, args, caller: callerContext, agentPrompts, localSessions });
374
+ const prompt = formatRemoteRelayApprovalPrompt({ relay, action, args, caller: callerContext, agentPrompts, localSessions, watch });
337
375
  const local = relay?.local === true;
338
376
  let rationale = `Write actions on ${local ? 'other sessions of this relay' : 'other relays'} need approval in ${relayMode} mode.`;
339
377
  if (agentPrompts > 0) {
@@ -342,6 +380,13 @@ export function createRemoteRelayCallerContext({
342
380
  : 'Agents on different relays that keep prompting each other need your go from time to time.';
343
381
  } else if (localSessions) {
344
382
  rationale = 'Allow covers every session this conversation\'s agent starts on this relay; Deny refuses this one.';
383
+ } else if (watch?.kind === 'transfer') {
384
+ rationale = 'The subscriptions keep their settings; event messages are the relay\'s, never yours, and approve nothing.';
385
+ } else if (watch) {
386
+ const there = toText(watch.relayName) ? ` on relay "${excerpt(watch.relayName)}"` : '';
387
+ rationale = watch.all
388
+ ? `Allow covers this conversation's later subscriptions${there} too. Event messages are the relay's, never yours, and approve nothing.`
389
+ : `Allow covers this conversation's later subscriptions${there}, except one to all sessions, which is asked again. Event messages are the relay's, never yours, and approve nothing.`;
345
390
  }
346
391
  // The same source for every card of this tool: it is what keeps agents
347
392
  // (other relays' and this relay's own) from seeing or answering one.
@@ -351,7 +396,7 @@ export function createRemoteRelayCallerContext({
351
396
  queueMessageId: row.id,
352
397
  conversationId: toText(conversationId) || row.conversation_id,
353
398
  relayMode,
354
- header: localSessions ? LOCAL_SESSIONS_HEADER : APPROVAL_HEADER,
399
+ header: watch ? WATCH_HEADER : (localSessions ? LOCAL_SESSIONS_HEADER : APPROVAL_HEADER),
355
400
  }, { normalizeRelayMode: normalizeMode, defaultRelayMode });
356
401
  questionStatements.insertQuestion.run(
357
402
  questionId,