@oar-sh/oar 0.9.9 → 0.9.11

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 (154) hide show
  1. package/CHANGELOG.md +561 -1
  2. package/README.md +162 -909
  3. package/bin/bat/oar.bat +46 -46
  4. package/package.json +14 -14
  5. package/server/README.md +369 -13
  6. package/server/claude-worker/claude-remote-relay-tool.mjs +10 -0
  7. package/server/claude-worker/claude-session-process.mjs +121 -9
  8. package/server/claude-worker/claude-session-worker.mjs +9 -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 +79 -12
  13. package/server/copilot-worker/copilot-sdk-session-worker.mjs +6 -0
  14. package/server/cursor-worker/cursor-sdk-adapter.mjs +56 -2
  15. package/server/db-schema.mjs +95 -2
  16. package/server/grok-worker/acp-client.mjs +2 -1
  17. package/server/grok-worker/acp-host-services.mjs +4 -3
  18. package/server/migrations/0009-subagent-timeline.mjs +135 -0
  19. package/server/migrations/0010-subagent-timeline-pruning.mjs +89 -0
  20. package/server/migrations/0011-relay-event-subscriptions.mjs +125 -0
  21. package/server/migrations/0012-cross-relay-event-subscriptions.mjs +116 -0
  22. package/server/migrations/0013-previews.mjs +130 -0
  23. package/server/migrations/0014-conversation-groups.mjs +89 -0
  24. package/server/migrations/0015-conversation-group-position.mjs +76 -0
  25. package/server/migrations/0016-share-access-log.mjs +110 -0
  26. package/server/public/app/about-ui.js +73 -0
  27. package/server/public/app/about-view.mjs +234 -0
  28. package/server/public/app/action-confirmations.js +31 -14
  29. package/server/public/app/agent-exchange-fold-view.mjs +187 -0
  30. package/server/public/app/api-client.js +312 -16
  31. package/server/public/app/archive-confirm.mjs +28 -0
  32. package/server/public/app/ask-user-view.js +17 -12
  33. package/server/public/app/attachments-view.js +516 -6
  34. package/server/public/app/background-tasks-view.mjs +18 -26
  35. package/server/public/app/bootstrap.js +336 -59
  36. package/server/public/app/chat-actions-menu-state.mjs +126 -0
  37. package/server/public/app/claude-cloud-conversation-ui.js +10 -22
  38. package/server/public/app/composer-control-state.mjs +16 -0
  39. package/server/public/app/composer-keyboard.mjs +54 -0
  40. package/server/public/app/context-menu.mjs +90 -6
  41. package/server/public/app/conversation-group-drag.mjs +191 -0
  42. package/server/public/app/conversation-group-order.mjs +265 -0
  43. package/server/public/app/conversation-group-state.mjs +100 -0
  44. package/server/public/app/conversation-list-drag.mjs +449 -0
  45. package/server/public/app/conversation-turn-state.mjs +81 -0
  46. package/server/public/app/conversation-view.js +436 -117
  47. package/server/public/app/cwd-picker.js +1 -1
  48. package/server/public/app/explorer-edit-state.mjs +68 -0
  49. package/server/public/app/hidden-messages-view.mjs +312 -0
  50. package/server/public/app/host-suspend-ui.js +13 -4
  51. package/server/public/app/journal-view.js +930 -45
  52. package/server/public/app/no-conversation-state.mjs +58 -0
  53. package/server/public/app/preview-cards.mjs +117 -31
  54. package/server/public/app/preview-command.mjs +10 -1
  55. package/server/public/app/provider-readiness.mjs +120 -0
  56. package/server/public/app/relay-board-view.js +19 -3
  57. package/server/public/app/relay-event-subscriptions-ui.js +184 -0
  58. package/server/public/app/relay-event-view.mjs +234 -0
  59. package/server/public/app/relay-restart-error.mjs +16 -0
  60. package/server/public/app/settings-modal.js +93 -9
  61. package/server/public/app/settings-tabs.js +1 -1
  62. package/server/public/app/share-format.mjs +49 -0
  63. package/server/public/app/share-preview-marks.mjs +167 -0
  64. package/server/public/app/share-state.js +109 -0
  65. package/server/public/app/shared-conversations-settings.js +480 -0
  66. package/server/public/app/socket-handlers.js +137 -40
  67. package/server/public/app/store.js +115 -32
  68. package/server/public/app/subagent-row.mjs +138 -0
  69. package/server/public/app/subagent-timeline-view.mjs +186 -0
  70. package/server/public/app/subagent-viewer-watch.mjs +50 -0
  71. package/server/public/app/subagent-viewer.mjs +558 -0
  72. package/server/public/app/task-row-format.mjs +29 -0
  73. package/server/public/app/transcript-jump-bottom-view.mjs +464 -0
  74. package/server/public/app/transcript-jump-bottom.mjs +210 -0
  75. package/server/public/app/transcript-reload-freshness.mjs +25 -0
  76. package/server/public/app/transcript-viewport-anchor.mjs +140 -0
  77. package/server/public/app/update-ui.js +1 -1
  78. package/server/public/app/viewer-history.mjs +5 -3
  79. package/server/public/index.html +774 -12
  80. package/server/relay-tools.md +2 -1
  81. package/server/relay.mjs +2 -1
  82. package/server/repositories/conversation-group-repository.mjs +444 -0
  83. package/server/repositories/message-repository.mjs +30 -3
  84. package/server/repositories/question-repository.mjs +9 -3
  85. package/server/repositories/remote-relay-repository.mjs +6 -2
  86. package/server/repositories/session-repository.mjs +12 -37
  87. package/server/repositories/share-repository.mjs +322 -0
  88. package/server/repositories/share-visibility-statements.mjs +10 -0
  89. package/server/repositories/subagent-timeline-statements.mjs +170 -0
  90. package/server/routes/about-routes.mjs +15 -0
  91. package/server/routes/ask-user-routes.mjs +3 -0
  92. package/server/routes/cache-routes.mjs +4 -0
  93. package/server/routes/conversation-group-routes.mjs +171 -0
  94. package/server/routes/messages-routes.mjs +698 -114
  95. package/server/routes/preview-routes.mjs +9 -1
  96. package/server/routes/relay-event-routes.mjs +157 -0
  97. package/server/routes/remote-relay-routes.mjs +15 -0
  98. package/server/routes/sessions-routes.mjs +843 -193
  99. package/server/server-runtime.mjs +559 -160
  100. package/server/services/about-service.mjs +461 -0
  101. package/server/services/background-task-store.mjs +128 -0
  102. package/server/services/background-work-view.mjs +228 -0
  103. package/server/services/cache-rebuild-service.mjs +30 -0
  104. package/server/services/delete-archive-service.mjs +4 -1
  105. package/server/services/detached-worker-watch-service.mjs +150 -0
  106. package/server/services/explorer-entry-name.mjs +26 -0
  107. package/server/services/explorer-mutations.mjs +122 -0
  108. package/server/services/hidden-message-list.mjs +21 -0
  109. package/server/services/host-suspend-command.mjs +2 -1
  110. package/server/services/message-pin-list.mjs +1 -1
  111. package/server/services/preview-health-probe.mjs +8 -1
  112. package/server/services/preview-proxy-server.mjs +49 -27
  113. package/server/services/preview-registry-service.mjs +676 -26
  114. package/server/services/preview-static-handler.mjs +40 -7
  115. package/server/services/preview-store.mjs +274 -0
  116. package/server/services/push-dispatch-service.mjs +18 -0
  117. package/server/services/rate-limit-service.mjs +12 -1
  118. package/server/services/relay-auth.mjs +183 -0
  119. package/server/services/relay-cli-launcher-service.mjs +3 -1
  120. package/server/services/relay-event-link-service.mjs +289 -0
  121. package/server/services/relay-event-service.mjs +1952 -0
  122. package/server/services/relay-shutdown-gate.mjs +31 -0
  123. package/server/services/remote-relay-caller-context.mjs +52 -7
  124. package/server/services/remote-relay-dispatcher.mjs +551 -7
  125. package/server/services/remote-relay-registry-service.mjs +11 -1
  126. package/server/services/request-boundary.mjs +183 -0
  127. package/server/services/session-activity-tracker.mjs +695 -0
  128. package/server/services/session-history-refresh-service.mjs +7 -0
  129. package/server/services/session-worker-launch-service.mjs +190 -6
  130. package/server/services/session-worker-process-service.mjs +10 -6
  131. package/server/services/share-access-log-service.mjs +123 -0
  132. package/server/services/share-path-redaction.mjs +424 -0
  133. package/server/services/stale-attempt-log-limiter.mjs +96 -0
  134. package/server/services/stale-recovery-clock-guard.mjs +57 -0
  135. package/server/services/stale-subagent-run-sweep.mjs +109 -0
  136. package/server/services/status-event-service.mjs +7 -3
  137. package/server/services/subagent-record-retention.mjs +164 -0
  138. package/server/services/subagent-timeline-service.mjs +328 -0
  139. package/server/services/subagent-viewer-rooms.mjs +119 -0
  140. package/server/services/tunnel-worker-path-guard.mjs +24 -41
  141. package/server/services/upgrade-socket-guard.mjs +16 -0
  142. package/server/services/windows-autostart-service.mjs +6 -1
  143. package/server/services/windows-boot-autostart-service.mjs +207 -23
  144. package/shared/preview-tool-core.mjs +15 -5
  145. package/shared/relay-events.mjs +371 -0
  146. package/shared/remote-relay-contract.mjs +160 -6
  147. package/shared/share-path-privacy.mjs +30 -0
  148. package/shared/subagent-record-retention.mjs +31 -0
  149. package/shared/subagent-timeline.mjs +79 -0
  150. package/shared/windows-system-tools.mjs +106 -0
  151. package/shared/worker-runtime/background-task-digest.mjs +30 -0
  152. package/shared/worker-runtime/heartbeat.mjs +24 -0
  153. package/shared/worker-runtime/kept-posts.mjs +83 -0
  154. package/shared/worker-runtime/worker-websocket-link.mjs +47 -15
@@ -95,7 +95,15 @@ export function registerPreviewRoutes(app, deps) {
95
95
  // Probe immediately so the card does not sit badge-less until the next
96
96
  // scheduled sweep — and so a typo'd port shows as offline right away.
97
97
  previewHealthProbe?.probeNow?.(created.preview.token);
98
- res.json({ ok: true, preview: created.preview, url: created.preview.url, basePath: created.preview.basePath });
98
+ // `existing`: this conversation already had a preview of the same target,
99
+ // and this is it (same token, same link, label unchanged).
100
+ res.json({
101
+ ok: true,
102
+ existing: created.existing === true,
103
+ preview: created.preview,
104
+ url: created.preview.url,
105
+ basePath: created.preview.basePath,
106
+ });
99
107
  });
100
108
 
101
109
  // DELETE /api/previews/:token — the link stops resolving immediately. The dev
@@ -0,0 +1,157 @@
1
+ 'use strict';
2
+
3
+ // Relay event subscriptions, for the browser: the active subscriptions (the
4
+ // "watching" chip and the "watched" badges) and Unsubscribe. Agents use the
5
+ // remote_relay tool instead; a request from another relay's agent is
6
+ // refused here, so only this relay's user ends a subscription from outside
7
+ // its conversation.
8
+ //
9
+ // And for paired relays: their agents subscribe to this relay's sessions
10
+ // (POST /api/relay-events/remote/subscribe) and their relay fetches the
11
+ // events (POST /api/relay-events/remote/pull, relay-event-link-service.mjs on
12
+ // the other side). Only another relay's request is taken there, under the
13
+ // inbound switch like a prompt; its subscriptions are filed under the relay
14
+ // id it states, which never names a conversation here.
15
+
16
+ import { REMOTE_RELAY_ERROR_CODES } from '../../shared/remote-relay-contract.mjs';
17
+ import { admitRemoteRelayRequest, readRemoteRelayRequest } from '../services/remote-relay-inbound.mjs';
18
+ import { RelayEventError, cleanSubscriberRelayId } from '../services/relay-event-service.mjs';
19
+
20
+ const RELAY_EVENT_ERROR_STATUS = Object.freeze({
21
+ invalid: [400, REMOTE_RELAY_ERROR_CODES.invalidInput],
22
+ notFound: [404, REMOTE_RELAY_ERROR_CODES.notFound],
23
+ ownSession: [400, REMOTE_RELAY_ERROR_CODES.ownSession],
24
+ limit: [409, REMOTE_RELAY_ERROR_CODES.subscriptionLimit],
25
+ unavailable: [503, REMOTE_RELAY_ERROR_CODES.unsupported],
26
+ });
27
+
28
+ function sendRelayEventError(res, error, what) {
29
+ if (error instanceof RelayEventError) {
30
+ const [status, code] = RELAY_EVENT_ERROR_STATUS[error.code] || [400, REMOTE_RELAY_ERROR_CODES.invalidInput];
31
+ return res.status(status).json({ error: error.message, code, ...(error.extra && typeof error.extra === 'object' ? error.extra : {}) });
32
+ }
33
+ return res.status(500).json({ error: `${what} failed: ${String(error?.message || error)}`, code: REMOTE_RELAY_ERROR_CODES.internal });
34
+ }
35
+
36
+ function asObject(value) {
37
+ return value && typeof value === 'object' && !Array.isArray(value) ? value : {};
38
+ }
39
+
40
+ export function registerRelayEventRoutes(app, deps) {
41
+ const {
42
+ auth,
43
+ relayEventService = null,
44
+ remoteRelayInbound = null,
45
+ } = deps;
46
+
47
+ function refuseRemote(req, res) {
48
+ const request = readRemoteRelayRequest(req, remoteRelayInbound || {});
49
+ if (!request.remote) return false;
50
+ res.status(403).json({ error: 'Relay event subscriptions are managed by this relay\'s user and its own agents only.' });
51
+ return true;
52
+ }
53
+
54
+ // GET /api/relay-events/subscriptions — every active subscription.
55
+ app.get('/api/relay-events/subscriptions', auth, (req, res) => {
56
+ if (refuseRemote(req, res)) return;
57
+ if (!relayEventService?.isAvailable?.()) return res.json({ available: false, subscriptions: [] });
58
+ res.json({ available: true, subscriptions: relayEventService.listActive() });
59
+ });
60
+
61
+ /**
62
+ * The paired relay a request comes from: its stated relay id, or null
63
+ * after answering. Refused: this relay's own agents and the browser (they
64
+ * have the tool and the routes above), any request while the inbound
65
+ * switch is off, and one that names no relay.
66
+ */
67
+ function admitPairedRelay(req, res) {
68
+ const request = admitRemoteRelayRequest(req, res, remoteRelayInbound);
69
+ if (!request) return null;
70
+ if (!request.remote || request.local) {
71
+ res.status(403).json({
72
+ error: 'Only another relay subscribes to sessions here this way; this relay\'s own agents use the remote_relay tool.',
73
+ code: REMOTE_RELAY_ERROR_CODES.forbidden,
74
+ });
75
+ return null;
76
+ }
77
+ const subscriber = cleanSubscriberRelayId(request.origin?.relayId);
78
+ if (!subscriber) {
79
+ res.status(400).json({ error: 'The calling relay sent no relay id.', code: REMOTE_RELAY_ERROR_CODES.invalidInput });
80
+ return null;
81
+ }
82
+ if (!relayEventService?.isCrossRelayAvailable?.()) {
83
+ res.status(503).json({
84
+ error: 'Event subscriptions across relays are not available on this relay right now (see its server log).',
85
+ code: REMOTE_RELAY_ERROR_CODES.unsupported,
86
+ });
87
+ return null;
88
+ }
89
+ return subscriber;
90
+ }
91
+
92
+ // POST /api/relay-events/remote/subscribe — a paired relay's agent
93
+ // subscribes: `{ watcher: { conversationId, title }, subscriber: { name },
94
+ // scope, sessions, events, once, match, stalled_minutes, heartbeat_minutes,
95
+ // dry_run }`. A dry run checks everything and stores nothing.
96
+ app.post('/api/relay-events/remote/subscribe', auth, (req, res) => {
97
+ const subscriber = admitPairedRelay(req, res);
98
+ if (!subscriber) return;
99
+ const body = asObject(req.body);
100
+ const watcher = asObject(body.watcher);
101
+ try {
102
+ const subscription = relayEventService.subscribeServed({
103
+ subscriberRelayId: subscriber,
104
+ subscriberRelayName: String(asObject(body.subscriber).name || ''),
105
+ watcherId: String(watcher.conversationId || ''),
106
+ watcherTitle: String(watcher.title || ''),
107
+ scope: body.scope === 'all' ? 'all' : 'sessions',
108
+ sessionIds: Array.isArray(body.sessions) ? body.sessions.map((id) => String(id ?? '')) : [],
109
+ events: Array.isArray(body.events) ? body.events.map((type) => String(type ?? '')) : null,
110
+ once: body.once === true,
111
+ match: typeof body.match === 'string' ? body.match : '',
112
+ stalledMinutes: body.stalled_minutes ?? null,
113
+ heartbeatMinutes: body.heartbeat_minutes ?? null,
114
+ dryRun: body.dry_run === true,
115
+ });
116
+ res.json({ ok: true, ...(body.dry_run === true ? { dryRun: true } : {}), subscription });
117
+ } catch (error) {
118
+ sendRelayEventError(res, error, 'Subscribe');
119
+ }
120
+ });
121
+
122
+ // POST /api/relay-events/remote/pull — a paired relay fetches its events:
123
+ // `{ after, subscriptions: [{ id, watcher }], ended: [ids] }`.
124
+ app.post('/api/relay-events/remote/pull', auth, (req, res) => {
125
+ const subscriber = admitPairedRelay(req, res);
126
+ if (!subscriber) return;
127
+ const body = asObject(req.body);
128
+ try {
129
+ res.json({
130
+ ok: true,
131
+ ...relayEventService.pullServed({
132
+ subscriberRelayId: subscriber,
133
+ after: body.after,
134
+ subscriptions: Array.isArray(body.subscriptions) ? body.subscriptions : [],
135
+ ended: Array.isArray(body.ended) ? body.ended : [],
136
+ }),
137
+ });
138
+ } catch (error) {
139
+ sendRelayEventError(res, error, 'The event fetch');
140
+ }
141
+ });
142
+
143
+ // DELETE /api/relay-events/subscriptions/:id — the user's Unsubscribe.
144
+ app.delete('/api/relay-events/subscriptions/:id', auth, (req, res) => {
145
+ if (refuseRemote(req, res)) return;
146
+ if (!relayEventService?.isAvailable?.()) return res.status(503).json({ error: 'Relay event subscriptions are not available' });
147
+ try {
148
+ const ended = relayEventService.unsubscribe({ subscriptionId: String(req.params.id || '').trim(), reason: 'unsubscribed-by-user' });
149
+ res.json({ ok: true, ended });
150
+ } catch (error) {
151
+ if (error instanceof RelayEventError) {
152
+ return res.status(error.code === 'notFound' ? 404 : 400).json({ error: error.message, code: error.code });
153
+ }
154
+ res.status(500).json({ error: `Unsubscribe failed: ${String(error?.message || error)}` });
155
+ }
156
+ });
157
+ }
@@ -8,6 +8,9 @@
8
8
  // No response carries a stored token: the registry's public views drop it, and
9
9
  // the pairing service's answers never contain one.
10
10
 
11
+ import { REMOTE_RELAY_ERROR_CODES } from '../../shared/remote-relay-contract.mjs';
12
+ import { readRemoteRelayRequest } from '../services/remote-relay-inbound.mjs';
13
+
11
14
  function sendResult(res, result, fallbackStatus = 400) {
12
15
  const { status, ...payload } = result || {};
13
16
  const code = Number.isInteger(status) ? status : (payload.ok === false ? fallbackStatus : 200);
@@ -26,6 +29,9 @@ export function registerRemoteRelayRoutes(app, deps) {
26
29
  // WP2's dispatcher: `{ dispatch({ conversationId, action, args, req }) →
27
30
  // Promise<{ status, body }>, inflight(conversationId) → number }`.
28
31
  dispatcher = null,
32
+ // The inbound facts (remote-relay-inbound.mjs): which requests come from
33
+ // another relay.
34
+ remoteRelayInbound = null,
29
35
  } = deps;
30
36
 
31
37
  // Static paths first: Express would otherwise hand them to /:id.
@@ -85,6 +91,15 @@ export function registerRemoteRelayRoutes(app, deps) {
85
91
  // POST /api/remote-relays/tool — every provider adapter's remote_relay call.
86
92
  app.post('/api/remote-relays/tool', auth, async (req, res) => {
87
93
  if (!dispatcher) return res.status(503).json({ error: 'Remote relay dispatcher unavailable' });
94
+ // The tool acts for one of this relay's conversations: another relay
95
+ // never calls it (it would act, or subscribe, for a conversation it does
96
+ // not host). Its agents reach this relay through the routes it calls.
97
+ if (readRemoteRelayRequest(req, remoteRelayInbound).remote) {
98
+ return res.status(403).json({
99
+ error: 'The remote_relay tool of this relay is for its own sessions; another relay cannot call it.',
100
+ code: REMOTE_RELAY_ERROR_CODES.forbidden,
101
+ });
102
+ }
88
103
  // A worker that gives up on the call (turn stopped, worker gone) closes
89
104
  // the request: withdraw its approval card and stop waiting for the reply.
90
105
  const controller = new AbortController();