@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.
- package/CHANGELOG.md +561 -1
- package/README.md +162 -909
- package/bin/bat/oar.bat +46 -46
- package/package.json +14 -14
- package/server/README.md +369 -13
- package/server/claude-worker/claude-remote-relay-tool.mjs +10 -0
- package/server/claude-worker/claude-session-process.mjs +121 -9
- package/server/claude-worker/claude-session-worker.mjs +9 -1
- package/server/claude-worker/claude-turn-publisher.mjs +3 -0
- package/server/claude-worker/sdk-message-normalizer.mjs +31 -4
- package/server/claude-worker/subagent-timeline-recorder.mjs +572 -0
- package/server/copilot-worker/copilot-sdk-session-process.mjs +79 -12
- package/server/copilot-worker/copilot-sdk-session-worker.mjs +6 -0
- package/server/cursor-worker/cursor-sdk-adapter.mjs +56 -2
- package/server/db-schema.mjs +95 -2
- package/server/grok-worker/acp-client.mjs +2 -1
- package/server/grok-worker/acp-host-services.mjs +4 -3
- package/server/migrations/0009-subagent-timeline.mjs +135 -0
- package/server/migrations/0010-subagent-timeline-pruning.mjs +89 -0
- package/server/migrations/0011-relay-event-subscriptions.mjs +125 -0
- package/server/migrations/0012-cross-relay-event-subscriptions.mjs +116 -0
- package/server/migrations/0013-previews.mjs +130 -0
- package/server/migrations/0014-conversation-groups.mjs +89 -0
- package/server/migrations/0015-conversation-group-position.mjs +76 -0
- package/server/migrations/0016-share-access-log.mjs +110 -0
- package/server/public/app/about-ui.js +73 -0
- package/server/public/app/about-view.mjs +234 -0
- package/server/public/app/action-confirmations.js +31 -14
- package/server/public/app/agent-exchange-fold-view.mjs +187 -0
- package/server/public/app/api-client.js +312 -16
- package/server/public/app/archive-confirm.mjs +28 -0
- package/server/public/app/ask-user-view.js +17 -12
- package/server/public/app/attachments-view.js +516 -6
- package/server/public/app/background-tasks-view.mjs +18 -26
- package/server/public/app/bootstrap.js +336 -59
- package/server/public/app/chat-actions-menu-state.mjs +126 -0
- package/server/public/app/claude-cloud-conversation-ui.js +10 -22
- package/server/public/app/composer-control-state.mjs +16 -0
- package/server/public/app/composer-keyboard.mjs +54 -0
- package/server/public/app/context-menu.mjs +90 -6
- package/server/public/app/conversation-group-drag.mjs +191 -0
- package/server/public/app/conversation-group-order.mjs +265 -0
- package/server/public/app/conversation-group-state.mjs +100 -0
- package/server/public/app/conversation-list-drag.mjs +449 -0
- package/server/public/app/conversation-turn-state.mjs +81 -0
- package/server/public/app/conversation-view.js +436 -117
- package/server/public/app/cwd-picker.js +1 -1
- package/server/public/app/explorer-edit-state.mjs +68 -0
- package/server/public/app/hidden-messages-view.mjs +312 -0
- package/server/public/app/host-suspend-ui.js +13 -4
- package/server/public/app/journal-view.js +930 -45
- package/server/public/app/no-conversation-state.mjs +58 -0
- package/server/public/app/preview-cards.mjs +117 -31
- package/server/public/app/preview-command.mjs +10 -1
- package/server/public/app/provider-readiness.mjs +120 -0
- package/server/public/app/relay-board-view.js +19 -3
- package/server/public/app/relay-event-subscriptions-ui.js +184 -0
- package/server/public/app/relay-event-view.mjs +234 -0
- package/server/public/app/relay-restart-error.mjs +16 -0
- package/server/public/app/settings-modal.js +93 -9
- package/server/public/app/settings-tabs.js +1 -1
- package/server/public/app/share-format.mjs +49 -0
- package/server/public/app/share-preview-marks.mjs +167 -0
- package/server/public/app/share-state.js +109 -0
- package/server/public/app/shared-conversations-settings.js +480 -0
- package/server/public/app/socket-handlers.js +137 -40
- package/server/public/app/store.js +115 -32
- package/server/public/app/subagent-row.mjs +138 -0
- package/server/public/app/subagent-timeline-view.mjs +186 -0
- package/server/public/app/subagent-viewer-watch.mjs +50 -0
- package/server/public/app/subagent-viewer.mjs +558 -0
- package/server/public/app/task-row-format.mjs +29 -0
- package/server/public/app/transcript-jump-bottom-view.mjs +464 -0
- package/server/public/app/transcript-jump-bottom.mjs +210 -0
- package/server/public/app/transcript-reload-freshness.mjs +25 -0
- package/server/public/app/transcript-viewport-anchor.mjs +140 -0
- package/server/public/app/update-ui.js +1 -1
- package/server/public/app/viewer-history.mjs +5 -3
- package/server/public/index.html +774 -12
- package/server/relay-tools.md +2 -1
- package/server/relay.mjs +2 -1
- package/server/repositories/conversation-group-repository.mjs +444 -0
- package/server/repositories/message-repository.mjs +30 -3
- package/server/repositories/question-repository.mjs +9 -3
- package/server/repositories/remote-relay-repository.mjs +6 -2
- package/server/repositories/session-repository.mjs +12 -37
- package/server/repositories/share-repository.mjs +322 -0
- package/server/repositories/share-visibility-statements.mjs +10 -0
- package/server/repositories/subagent-timeline-statements.mjs +170 -0
- package/server/routes/about-routes.mjs +15 -0
- package/server/routes/ask-user-routes.mjs +3 -0
- package/server/routes/cache-routes.mjs +4 -0
- package/server/routes/conversation-group-routes.mjs +171 -0
- package/server/routes/messages-routes.mjs +698 -114
- package/server/routes/preview-routes.mjs +9 -1
- package/server/routes/relay-event-routes.mjs +157 -0
- package/server/routes/remote-relay-routes.mjs +15 -0
- package/server/routes/sessions-routes.mjs +843 -193
- package/server/server-runtime.mjs +559 -160
- package/server/services/about-service.mjs +461 -0
- package/server/services/background-task-store.mjs +128 -0
- package/server/services/background-work-view.mjs +228 -0
- package/server/services/cache-rebuild-service.mjs +30 -0
- package/server/services/delete-archive-service.mjs +4 -1
- package/server/services/detached-worker-watch-service.mjs +150 -0
- package/server/services/explorer-entry-name.mjs +26 -0
- package/server/services/explorer-mutations.mjs +122 -0
- package/server/services/hidden-message-list.mjs +21 -0
- package/server/services/host-suspend-command.mjs +2 -1
- package/server/services/message-pin-list.mjs +1 -1
- package/server/services/preview-health-probe.mjs +8 -1
- package/server/services/preview-proxy-server.mjs +49 -27
- package/server/services/preview-registry-service.mjs +676 -26
- package/server/services/preview-static-handler.mjs +40 -7
- package/server/services/preview-store.mjs +274 -0
- package/server/services/push-dispatch-service.mjs +18 -0
- package/server/services/rate-limit-service.mjs +12 -1
- package/server/services/relay-auth.mjs +183 -0
- package/server/services/relay-cli-launcher-service.mjs +3 -1
- package/server/services/relay-event-link-service.mjs +289 -0
- package/server/services/relay-event-service.mjs +1952 -0
- package/server/services/relay-shutdown-gate.mjs +31 -0
- package/server/services/remote-relay-caller-context.mjs +52 -7
- package/server/services/remote-relay-dispatcher.mjs +551 -7
- package/server/services/remote-relay-registry-service.mjs +11 -1
- package/server/services/request-boundary.mjs +183 -0
- package/server/services/session-activity-tracker.mjs +695 -0
- package/server/services/session-history-refresh-service.mjs +7 -0
- package/server/services/session-worker-launch-service.mjs +190 -6
- package/server/services/session-worker-process-service.mjs +10 -6
- package/server/services/share-access-log-service.mjs +123 -0
- package/server/services/share-path-redaction.mjs +424 -0
- package/server/services/stale-attempt-log-limiter.mjs +96 -0
- package/server/services/stale-recovery-clock-guard.mjs +57 -0
- package/server/services/stale-subagent-run-sweep.mjs +109 -0
- package/server/services/status-event-service.mjs +7 -3
- package/server/services/subagent-record-retention.mjs +164 -0
- package/server/services/subagent-timeline-service.mjs +328 -0
- package/server/services/subagent-viewer-rooms.mjs +119 -0
- package/server/services/tunnel-worker-path-guard.mjs +24 -41
- package/server/services/upgrade-socket-guard.mjs +16 -0
- package/server/services/windows-autostart-service.mjs +6 -1
- package/server/services/windows-boot-autostart-service.mjs +207 -23
- package/shared/preview-tool-core.mjs +15 -5
- package/shared/relay-events.mjs +371 -0
- package/shared/remote-relay-contract.mjs +160 -6
- package/shared/share-path-privacy.mjs +30 -0
- package/shared/subagent-record-retention.mjs +31 -0
- package/shared/subagent-timeline.mjs +79 -0
- package/shared/windows-system-tools.mjs +106 -0
- package/shared/worker-runtime/background-task-digest.mjs +30 -0
- package/shared/worker-runtime/heartbeat.mjs +24 -0
- package/shared/worker-runtime/kept-posts.mjs +83 -0
- 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
|
-
|
|
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();
|