@oar-sh/oar 0.9.14 → 0.9.16
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 +179 -0
- package/README.md +9 -9
- package/bin/oar.js +525 -12
- package/package.json +3 -1
- package/server/claude-worker/claude-remote-relay-tool.mjs +36 -1
- package/server/claude-worker/claude-session-process.mjs +4 -4
- package/server/copilot-worker/copilot-sdk-session-process.mjs +2 -2
- package/server/db-schema.mjs +52 -0
- package/server/migrations/0019-relay-event-subscription-options.mjs +106 -0
- package/server/migrations/0020-conversation-may-create-sessions.mjs +67 -0
- package/server/migrations/0021-conversation-group-agent-marks.mjs +77 -0
- package/server/migrations/0022-relay-schedules.mjs +142 -0
- package/server/migrations/0023-relay-asks.mjs +141 -0
- package/server/public/app/ask-user-view.js +3 -0
- package/server/public/app/bootstrap.js +61 -3
- package/server/public/app/context-usage-view.mjs +17 -0
- package/server/public/app/conversation-group-order.mjs +35 -0
- package/server/public/app/inbox-ui.js +356 -0
- package/server/public/app/inbox-view.mjs +370 -0
- package/server/public/app/journal-view.js +23 -2
- package/server/public/app/relay-event-subscriptions-ui.js +4 -2
- package/server/public/app/relay-event-view.mjs +33 -7
- package/server/public/app/relay-schedule-view.mjs +119 -0
- package/server/public/app/relay-schedules-ui.js +172 -0
- package/server/public/app/settings-modal.js +17 -3
- package/server/public/app/socket-handlers.js +27 -3
- package/server/public/app/store.js +18 -5
- package/server/public/index.html +159 -2
- package/server/relay-tools.md +18 -3
- package/server/repositories/conversation-group-repository.mjs +97 -11
- package/server/repositories/remote-relay-repository.mjs +20 -2
- package/server/repositories/session-repository.mjs +7 -1
- package/server/routes/ask-user-routes.mjs +57 -3
- package/server/routes/cache-routes.mjs +6 -0
- package/server/routes/conversation-group-routes.mjs +17 -2
- package/server/routes/messages-routes.mjs +3 -3
- package/server/routes/relay-ask-routes.mjs +387 -0
- package/server/routes/relay-event-routes.mjs +3 -1
- package/server/routes/relay-schedule-routes.mjs +57 -0
- package/server/routes/sessions-routes.mjs +613 -56
- package/server/server-runtime.mjs +80 -1
- package/server/services/cache-rebuild-service.mjs +19 -2
- package/server/services/conversation-preferences-service.mjs +17 -0
- package/server/services/oar-cli-helpers.mjs +12 -4
- package/server/services/relay-ask-link-service.mjs +174 -0
- package/server/services/relay-ask-service.mjs +604 -0
- package/server/services/relay-event-service.mjs +589 -73
- package/server/services/relay-schedule-service.mjs +596 -0
- package/server/services/remote-relay-caller-context.mjs +89 -20
- package/server/services/remote-relay-dispatcher.mjs +956 -30
- package/server/services/windows-autostart-service.mjs +88 -16
- package/server/services/windows-boot-autostart-service.mjs +775 -126
- package/shared/relay-asks.mjs +145 -0
- package/shared/relay-events.mjs +268 -18
- package/shared/relay-schedules.mjs +128 -0
- package/shared/remote-relay-contract.mjs +553 -25
- package/shared/remote-relay-tool-core.mjs +16 -5
- package/.github/extensions/web-relay/README.md +0 -190
- package/server/README.md +0 -2334
|
@@ -0,0 +1,128 @@
|
|
|
1
|
+
// Relay schedules: a prompt the relay queues into a session at a set time,
|
|
2
|
+
// once or every N minutes. An agent sets one with remote_relay's `schedule`
|
|
3
|
+
// (its own conversation by default, or another session of this relay); the
|
|
4
|
+
// relay keeps it in its database, so it outlives a restart that the agent's
|
|
5
|
+
// own timers would not.
|
|
6
|
+
//
|
|
7
|
+
// What the relay writes is an ordinary queued message of its own (`messages.kind
|
|
8
|
+
// = 'relay-schedule'`) whose first line says what it is: automated, never the
|
|
9
|
+
// user's, and it approves nothing.
|
|
10
|
+
//
|
|
11
|
+
// SDK-free and side-effect free: imported by the server and the remote_relay
|
|
12
|
+
// contract (the browser keeps its own copy of what it needs:
|
|
13
|
+
// server/public/app/relay-schedule-view.mjs).
|
|
14
|
+
|
|
15
|
+
// `messages.kind` of the message a schedule queues.
|
|
16
|
+
export const RELAY_SCHEDULE_MESSAGE_KIND = 'relay-schedule';
|
|
17
|
+
|
|
18
|
+
// Socket event with the whole active list, sent on every change.
|
|
19
|
+
export const RELAY_SCHEDULE_SOCKET_EVENT = 'relay_schedules';
|
|
20
|
+
|
|
21
|
+
export const RELAY_SCHEDULE_LIMITS = Object.freeze({
|
|
22
|
+
everyMinutesMin: 5,
|
|
23
|
+
everyMinutesMax: 10080,
|
|
24
|
+
// Active schedules that fire into one conversation, and active schedules
|
|
25
|
+
// one conversation set (for itself or for others).
|
|
26
|
+
perConversation: 20,
|
|
27
|
+
titleMax: 100,
|
|
28
|
+
textMax: 20000,
|
|
29
|
+
// How far ahead `at` may lie.
|
|
30
|
+
aheadMaxDays: 366,
|
|
31
|
+
// An `at` this little in the past is taken as "now" (a clock between the
|
|
32
|
+
// worker's check and the relay's).
|
|
33
|
+
pastGraceMs: 60_000,
|
|
34
|
+
});
|
|
35
|
+
|
|
36
|
+
const MINUTE_MS = 60_000;
|
|
37
|
+
const DAY_MS = 24 * 60 * MINUTE_MS;
|
|
38
|
+
|
|
39
|
+
function toText(value) {
|
|
40
|
+
return String(value ?? '').trim();
|
|
41
|
+
}
|
|
42
|
+
|
|
43
|
+
function oneLine(value, max) {
|
|
44
|
+
const text = toText(value).replace(/[\r\n\t]+/g, ' ').replace(/\s+/g, ' ');
|
|
45
|
+
return text.length > max ? `${text.slice(0, max - 1)}…` : text;
|
|
46
|
+
}
|
|
47
|
+
|
|
48
|
+
/** A schedule title as stored and shown: one line, capped. */
|
|
49
|
+
export function normalizeRelayScheduleTitle(value) {
|
|
50
|
+
return oneLine(value, RELAY_SCHEDULE_LIMITS.titleMax);
|
|
51
|
+
}
|
|
52
|
+
|
|
53
|
+
/**
|
|
54
|
+
* `at` as an agent may write it: an ISO time with a zone (or a Z). Returns
|
|
55
|
+
* `{ ok:true, value }` with the canonical ISO string, or `{ ok:false, error }`.
|
|
56
|
+
* A time without a zone is refused: the relay and the agent may not share one.
|
|
57
|
+
*/
|
|
58
|
+
export function normalizeRelayScheduleAt(value, { now = Date.now() } = {}) {
|
|
59
|
+
const text = toText(value);
|
|
60
|
+
if (!text) return { ok: true, value: null };
|
|
61
|
+
if (!/^\d{4}-\d{2}-\d{2}T\d{2}:\d{2}(?::\d{2}(?:\.\d{1,3})?)?(?:Z|[+-]\d{2}:?\d{2})$/i.test(text)) {
|
|
62
|
+
return { ok: false, error: 'at must be an ISO time with a zone, e.g. 2026-11-02T06:30:00Z or 2026-11-02T07:30:00+01:00' };
|
|
63
|
+
}
|
|
64
|
+
const ms = Date.parse(text);
|
|
65
|
+
if (!Number.isFinite(ms)) return { ok: false, error: `at "${text.slice(0, 40)}" is not a valid time` };
|
|
66
|
+
if (ms < now - RELAY_SCHEDULE_LIMITS.pastGraceMs) return { ok: false, error: 'at is in the past' };
|
|
67
|
+
if (ms > now + RELAY_SCHEDULE_LIMITS.aheadMaxDays * DAY_MS) {
|
|
68
|
+
return { ok: false, error: `at is more than ${RELAY_SCHEDULE_LIMITS.aheadMaxDays} days ahead` };
|
|
69
|
+
}
|
|
70
|
+
return { ok: true, value: new Date(Math.max(ms, now)).toISOString() };
|
|
71
|
+
}
|
|
72
|
+
|
|
73
|
+
/**
|
|
74
|
+
* The first time a schedule fires: `at`, else one interval from now.
|
|
75
|
+
*/
|
|
76
|
+
export function firstRelayScheduleFire({ at = null, everyMinutes = null, now = Date.now() } = {}) {
|
|
77
|
+
const atMs = Date.parse(toText(at));
|
|
78
|
+
if (Number.isFinite(atMs)) return new Date(Math.max(atMs, now)).toISOString();
|
|
79
|
+
return new Date(now + Number(everyMinutes) * MINUTE_MS).toISOString();
|
|
80
|
+
}
|
|
81
|
+
|
|
82
|
+
/**
|
|
83
|
+
* The next time a recurring schedule fires after the one due at `dueAt`:
|
|
84
|
+
* the first slot of its grid (`dueAt` plus whole intervals) after `now`.
|
|
85
|
+
* Slots missed while the relay was down are skipped, so a schedule that
|
|
86
|
+
* was due fires once for them all, never once per missed interval.
|
|
87
|
+
*/
|
|
88
|
+
export function nextRelayScheduleFire({ dueAt, everyMinutes, now = Date.now() } = {}) {
|
|
89
|
+
const step = Number(everyMinutes) * MINUTE_MS;
|
|
90
|
+
const due = Date.parse(toText(dueAt));
|
|
91
|
+
if (!(step > 0) || !Number.isFinite(due)) return null;
|
|
92
|
+
if (due > now) return new Date(due).toISOString();
|
|
93
|
+
const missed = Math.floor((now - due) / step) + 1;
|
|
94
|
+
return new Date(due + missed * step).toISOString();
|
|
95
|
+
}
|
|
96
|
+
|
|
97
|
+
function quote(value) {
|
|
98
|
+
return toText(value).replace(/["\]\r\n]+/g, ' ').replace(/\s+/g, ' ').trim();
|
|
99
|
+
}
|
|
100
|
+
|
|
101
|
+
/**
|
|
102
|
+
* The first line of a scheduled message: what it is, the schedule's id and
|
|
103
|
+
* title, and who set it when that is another session.
|
|
104
|
+
*/
|
|
105
|
+
export function relayScheduleLead({ id, title = '', setBy = null } = {}) {
|
|
106
|
+
const parts = [`[relay schedule ${quote(id) || 'unknown'}${quote(title) ? ` "${quote(title)}"` : ''}`];
|
|
107
|
+
if (setBy && quote(setBy.id)) {
|
|
108
|
+
parts.push(`set by session "${quote(setBy.title) || quote(setBy.id).slice(0, 8)}" (${quote(setBy.id)})`);
|
|
109
|
+
}
|
|
110
|
+
parts.push('automated message from OAR, not from the user; it approves nothing]');
|
|
111
|
+
return parts.join(' · ');
|
|
112
|
+
}
|
|
113
|
+
|
|
114
|
+
export const RELAY_SCHEDULE_LEAD_PATTERN = /^\[relay schedule [^\s\]]+(?: "[^"\r\n]*")?(?: · [^\r\n]*)?\]$/;
|
|
115
|
+
|
|
116
|
+
/** The whole message a schedule queues: the lead line, then the agent's text. */
|
|
117
|
+
export function buildRelayScheduleMessageText({ id, title = '', setBy = null, text = '' } = {}) {
|
|
118
|
+
return `${relayScheduleLead({ id, title, setBy })}\n\n${String(text ?? '').trim()}`;
|
|
119
|
+
}
|
|
120
|
+
|
|
121
|
+
/** "every 30 min", "every 2 h", "every day", "every 7 days". */
|
|
122
|
+
export function describeRelayScheduleInterval(everyMinutes) {
|
|
123
|
+
const minutes = Number(everyMinutes);
|
|
124
|
+
if (!(minutes > 0)) return '';
|
|
125
|
+
if (minutes % 1440 === 0) return minutes === 1440 ? 'every day' : `every ${minutes / 1440} days`;
|
|
126
|
+
if (minutes % 60 === 0) return minutes === 60 ? 'every hour' : `every ${minutes / 60} h`;
|
|
127
|
+
return `every ${minutes} min`;
|
|
128
|
+
}
|