@oar-sh/oar 0.9.15 → 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 +85 -0
- package/README.md +9 -9
- package/package.json +1 -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/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 +568 -42
- 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/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/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/CHANGELOG.md
CHANGED
|
@@ -3,6 +3,91 @@
|
|
|
3
3
|
All notable changes to OAR are documented here. The format follows
|
|
4
4
|
[Keep a Changelog](https://keepachangelog.com/en/1.1.0/); versions follow semver.
|
|
5
5
|
|
|
6
|
+
## [0.9.16] — 2026-10-24
|
|
7
|
+
|
|
8
|
+
Highlights: agents can file the sessions they start into your session
|
|
9
|
+
groups, change another session's model, mode or other settings, watch other
|
|
10
|
+
sessions with fewer wake-ups (digests, quiet hours, exclusions), and
|
|
11
|
+
schedule prompts that survive a relay restart. Agents can ask you a question
|
|
12
|
+
without stopping their work, and the new Inbox lists every open question
|
|
13
|
+
from this relay and its paired relays in one place. A new switch lets the
|
|
14
|
+
agents of a conversation start sessions themselves.
|
|
15
|
+
|
|
16
|
+
### Added
|
|
17
|
+
|
|
18
|
+
- **Agents can put a new session into an existing session group.**
|
|
19
|
+
`remote_relay` create_session takes `group` (a group id, or its name in
|
|
20
|
+
any case) on this relay or a paired one, and `relay_info` lists the groups
|
|
21
|
+
there. An unknown group is refused with the groups listed, and nothing is
|
|
22
|
+
created. The approval card names the group, and the result carries it. A
|
|
23
|
+
relay too old for this is refused before anything is created. Renaming
|
|
24
|
+
and rearranging groups stays the owner's. An argument an action does not
|
|
25
|
+
take is no longer dropped without a word: create_session refuses it, and
|
|
26
|
+
the other actions list it in `ignored`.
|
|
27
|
+
- **Agents can change another session's settings.** `remote_relay`
|
|
28
|
+
update_session sets a session's model, reasoning effort, relay mode,
|
|
29
|
+
auto-compact window, title, group, thinking or working folder for its next
|
|
30
|
+
turns, on this relay or a paired one, and answers the settings and what
|
|
31
|
+
changed. An unknown model is refused with the close matches; a group name
|
|
32
|
+
that does not exist makes a new group, and `none` takes the session out (a
|
|
33
|
+
group you made is never removed this way: it stays, shown empty in the
|
|
34
|
+
sidebar for you to rename or dissolve); a new folder
|
|
35
|
+
restarts the session's CLI there and is refused while it works. Only a
|
|
36
|
+
session the agent's own conversation started may be changed freely; any
|
|
37
|
+
other, yours or another agent's, needs your Allow once per conversation and
|
|
38
|
+
relay. A failed change still lists what it wrote. An open page shows the
|
|
39
|
+
change at once.
|
|
40
|
+
- **"Agents in this conversation may start sessions."** A switch in the 🧠
|
|
41
|
+
settings of a session an agent started lets its agents start sessions
|
|
42
|
+
themselves, past the one-level rule; the limit of four at work and your
|
|
43
|
+
first Allow still apply. Only you can set it: agents and paired relays are
|
|
44
|
+
refused.
|
|
45
|
+
- **Fewer wake-ups for watching agents.** A `remote_relay` subscription takes
|
|
46
|
+
`digest_minutes` (its events come as one message every N minutes),
|
|
47
|
+
`quiet_hours` (a daily window in a time zone, crossing midnight and the
|
|
48
|
+
clock changes included, whose events come as one message when it ends) and
|
|
49
|
+
`exclude` (sessions a subscription to all leaves out). Questions and errors
|
|
50
|
+
still come at once, held events survive a restart, and the hourly limit
|
|
51
|
+
stays on top. A turn the watcher prompted itself and whose finished reply it
|
|
52
|
+
waited for no longer wakes it a second time with turn_end.
|
|
53
|
+
`list_subscriptions` shows how many events each subscription holds and when
|
|
54
|
+
they are due, and the watching chip shows a small digest or quiet-hours hint.
|
|
55
|
+
- **Scheduled prompts that survive a restart.** `remote_relay` schedule
|
|
56
|
+
queues a prompt at a time (`at`) or every N minutes (`every_minutes`, 5 to
|
|
57
|
+
10080), into the agent's own conversation or, with the user's approval in
|
|
58
|
+
ask and plan mode, another session on this relay. The relay keeps it in its
|
|
59
|
+
database, so a nightly review or a sweep no longer has to be set up again
|
|
60
|
+
after a deploy. A time missed while the relay was down fires once when it is
|
|
61
|
+
back, and a repeat waits while the one before is still queued. The message
|
|
62
|
+
says it is automated and approves nothing. `list_schedules` and `unschedule`
|
|
63
|
+
manage them, archiving a session ends its schedules, and a conversation
|
|
64
|
+
holds at most 20. The sidebar marks a scheduled session, and its header
|
|
65
|
+
lists the schedules with a Cancel button each.
|
|
66
|
+
- **Questions that don't block, and the Inbox.** `remote_relay` ask puts a
|
|
67
|
+
question for you into the agent's session (up to eight options, a
|
|
68
|
+
recommendation, a folded detail, and urgency `now` for a push) and returns
|
|
69
|
+
at once, so the agent goes on with what does not depend on it. Your answer
|
|
70
|
+
or dismissal reaches it as a relay event, question_answered, without a
|
|
71
|
+
subscription, and does not count as your message for the agents' loop
|
|
72
|
+
guard. A lead can ask on behalf of another session, on this relay or a
|
|
73
|
+
paired one (kept where that session lives); `list_questions` and `withdraw`
|
|
74
|
+
manage them, and `read_session` lists them with their kind. The new
|
|
75
|
+
**Inbox** in the sidebar, with its count, lists every pending question
|
|
76
|
+
across this relay and the paired relays that answer, grouped by relay,
|
|
77
|
+
with a filter for your sessions or the agents', and what you answered in
|
|
78
|
+
the last day under a divider. An ask is answered there or in its session,
|
|
79
|
+
also one on a paired relay; a paired relay's cards open there, and the
|
|
80
|
+
pairing never carries an answer to a card. Agent text shows as plain text,
|
|
81
|
+
and on a phone the Inbox is one column with full-width buttons.
|
|
82
|
+
|
|
83
|
+
### Fixed
|
|
84
|
+
|
|
85
|
+
- **The agents' loop guard no longer counts the relay's own messages as the
|
|
86
|
+
user's.** A relay event, the usage-limit follow-up or the note after a
|
|
87
|
+
restart reset the count of prompts an agent may send to other agents
|
|
88
|
+
before the user is asked, so two agents that watched each other could go
|
|
89
|
+
on without the user. Only a message the user wrote resets it now.
|
|
90
|
+
|
|
6
91
|
## [0.9.15] — 2026-10-22
|
|
7
92
|
|
|
8
93
|
Highlights: `oar setup` asks how the relay starts on Windows, at sign-in or
|
package/README.md
CHANGED
|
@@ -45,29 +45,29 @@ flowchart LR
|
|
|
45
45
|
<table>
|
|
46
46
|
<tr>
|
|
47
47
|
<td width="42%" rowspan="4" valign="top">
|
|
48
|
-
<a href="https://oar.sh/assets/shots/cfmail-mobile.png?v=0.9.
|
|
48
|
+
<a href="https://oar.sh/assets/shots/cfmail-mobile.png?v=0.9.16"><picture><source media="(prefers-color-scheme: light)" srcset="https://oar.sh/assets/shots/cfmail-mobile.light.png?v=0.9.16"><img src="https://oar.sh/assets/shots/cfmail-mobile.png?v=0.9.16" alt="OAR on a phone: a finished agent turn in a conversation, with the composer below"></picture></a>
|
|
49
49
|
<br><sub>On a phone: the same conversations, installable as an app.</sub>
|
|
50
50
|
</td>
|
|
51
51
|
<td width="58%" valign="top">
|
|
52
|
-
<a href="https://oar.sh/assets/shots/cfmail-agents.png?v=0.9.
|
|
52
|
+
<a href="https://oar.sh/assets/shots/cfmail-agents.png?v=0.9.16"><picture><source media="(prefers-color-scheme: light)" srcset="https://oar.sh/assets/shots/cfmail-agents.light.png?v=0.9.16"><img src="https://oar.sh/assets/shots/cfmail-agents.png?v=0.9.16" alt="Background subagents running in parallel in the task panel, each with live token counts, elapsed time, and its own Stop button"></picture></a>
|
|
53
53
|
<br><sub>Agents working in parallel, each with live token counts and its own Stop.</sub>
|
|
54
54
|
</td>
|
|
55
55
|
</tr>
|
|
56
56
|
<tr>
|
|
57
57
|
<td valign="top">
|
|
58
|
-
<a href="https://oar.sh/assets/shots/cfmail-question.png?v=0.9.
|
|
58
|
+
<a href="https://oar.sh/assets/shots/cfmail-question.png?v=0.9.16"><picture><source media="(prefers-color-scheme: light)" srcset="https://oar.sh/assets/shots/cfmail-question.light.png?v=0.9.16"><img src="https://oar.sh/assets/shots/cfmail-question.png?v=0.9.16" alt="A question card in the middle of a turn: the agent offers several choices and a free-text reply"></picture></a>
|
|
59
59
|
<br><sub>When the agent needs a decision, it asks, and waits for your answer.</sub>
|
|
60
60
|
</td>
|
|
61
61
|
</tr>
|
|
62
62
|
<tr>
|
|
63
63
|
<td valign="top">
|
|
64
|
-
<a href="https://oar.sh/assets/shots/pingcf-thoughts.png?v=0.9.
|
|
64
|
+
<a href="https://oar.sh/assets/shots/pingcf-thoughts.png?v=0.9.16"><picture><source media="(prefers-color-scheme: light)" srcset="https://oar.sh/assets/shots/pingcf-thoughts.light.png?v=0.9.16"><img src="https://oar.sh/assets/shots/pingcf-thoughts.png?v=0.9.16" alt="An expanded Thoughts block showing the agent's reasoning above its reply"></picture></a>
|
|
65
65
|
<br><sub>Read the reasoning, not just the answer.</sub>
|
|
66
66
|
</td>
|
|
67
67
|
</tr>
|
|
68
68
|
<tr>
|
|
69
69
|
<td valign="top">
|
|
70
|
-
<a href="https://oar.sh/assets/shots/pagewright-watching.png?v=0.9.
|
|
70
|
+
<a href="https://oar.sh/assets/shots/pagewright-watching.png?v=0.9.16"><picture><source media="(prefers-color-scheme: light)" srcset="https://oar.sh/assets/shots/pagewright-watching.light.png?v=0.9.16"><img src="https://oar.sh/assets/shots/pagewright-watching.png?v=0.9.16" alt="A session watching two other sessions: a finished reply arrives as a compact relay-event row, the watching 2 sessions list is open with Unsubscribe, and a request to also watch a session on a paired relay waits on an Allow / Deny card"></picture></a>
|
|
71
71
|
<br><sub>Agents that watch agents: woken by events, approving nothing.</sub>
|
|
72
72
|
</td>
|
|
73
73
|
</tr>
|
|
@@ -116,11 +116,11 @@ API keys are entered in **⚙️ Settings** and stay on the relay host. The Clau
|
|
|
116
116
|
<table>
|
|
117
117
|
<tr>
|
|
118
118
|
<td width="62%" valign="top">
|
|
119
|
-
<a href="https://oar.sh/assets/shots/cfmail-steering.png?v=0.9.
|
|
119
|
+
<a href="https://oar.sh/assets/shots/cfmail-steering.png?v=0.9.16"><picture><source media="(prefers-color-scheme: light)" srcset="https://oar.sh/assets/shots/cfmail-steering.light.png?v=0.9.16"><img src="https://oar.sh/assets/shots/cfmail-steering.png?v=0.9.16" alt="A running Claude turn with a message steered into it mid-turn, the reply streaming with its tool calls, and the Steer button in the composer"></picture></a>
|
|
120
120
|
<br><sub>Send a message while the agent works, and it steers the running turn.</sub>
|
|
121
121
|
</td>
|
|
122
122
|
<td width="38%" valign="top">
|
|
123
|
-
<a href="https://oar.sh/assets/shots/usage-copilot-compact.png?v=0.9.
|
|
123
|
+
<a href="https://oar.sh/assets/shots/usage-copilot-compact.png?v=0.9.16"><picture><source media="(prefers-color-scheme: light)" srcset="https://oar.sh/assets/shots/usage-copilot-compact.light.png?v=0.9.16"><img src="https://oar.sh/assets/shots/usage-copilot-compact.png?v=0.9.16" alt="The GitHub Copilot card in Check Usage: premium requests and plan quota with reset dates, and the cost of the last turn"></picture></a>
|
|
124
124
|
<br><sub>What each plan has left, in one place.</sub>
|
|
125
125
|
</td>
|
|
126
126
|
</tr>
|
|
@@ -137,7 +137,7 @@ API keys are entered in **⚙️ Settings** and stay on the relay host. The Clau
|
|
|
137
137
|
- **Share** a conversation as a read-only link, hide single messages and the host's paths from it, and see when it was opened ([Sharing](docs/web-ui.md#sharing)).
|
|
138
138
|
- **On your phone**: install OAR as an app and get push notifications ([Notifications and the app](docs/web-ui.md#notifications-and-the-app)).
|
|
139
139
|
|
|
140
|
-
The full tour is in [Using the web UI](docs/web-ui.md); every setting and config key is in [Settings and configuration](docs/configuration.md). What changed in each release is in the [changelog](CHANGELOG.md), with [0.9.
|
|
140
|
+
The full tour is in [Using the web UI](docs/web-ui.md); every setting and config key is in [Settings and configuration](docs/configuration.md). What changed in each release is in the [changelog](CHANGELOG.md), with [0.9.16](CHANGELOG.md#0916--2026-10-24) as the latest.
|
|
141
141
|
|
|
142
142
|
## Remote access
|
|
143
143
|
|
|
@@ -188,7 +188,7 @@ flowchart LR
|
|
|
188
188
|
|
|
189
189
|
You stay in charge: per paired relay you decide whether agents may only read, also prompt, or do everything, and in the `ask` and `plan` modes every write asks you first. A subscription only notifies; it approves nothing.
|
|
190
190
|
|
|
191
|
-
<p><a href="https://oar.sh/assets/shots/pagewright-cross-relay.png?v=0.9.
|
|
191
|
+
<p><a href="https://oar.sh/assets/shots/pagewright-cross-relay.png?v=0.9.16"><picture><source media="(prefers-color-scheme: light)" srcset="https://oar.sh/assets/shots/pagewright-cross-relay.light.png?v=0.9.16"><img src="https://oar.sh/assets/shots/pagewright-cross-relay.png?v=0.9.16" alt="A session watching two sessions on a paired relay: a reply arrives as a relay-event row, and after a short outage the events the other relay kept arrive as one replayed row" width="720"></picture></a></p>
|
|
192
192
|
|
|
193
193
|
How pairing, permissions and limits work is in [Agents working together](docs/agents-together.md).
|
|
194
194
|
|
package/package.json
CHANGED
|
@@ -13,7 +13,9 @@ import {
|
|
|
13
13
|
REMOTE_RELAY_MODES,
|
|
14
14
|
REMOTE_RELAY_PROVIDERS,
|
|
15
15
|
REMOTE_RELAY_SESSION_SCOPES,
|
|
16
|
+
REMOTE_RELAY_THINKING_DISPLAYS,
|
|
16
17
|
} from '../../shared/remote-relay-contract.mjs';
|
|
18
|
+
import { RELAY_ASK_URGENCIES } from '../../shared/relay-asks.mjs';
|
|
17
19
|
import {
|
|
18
20
|
REMOTE_RELAY_CALL_FAILED,
|
|
19
21
|
REMOTE_RELAY_TOOL_DESCRIPTION,
|
|
@@ -67,7 +69,21 @@ export const REMOTE_RELAY_TOOL_ZOD_SHAPE = {
|
|
|
67
69
|
mode: choice('mode', REMOTE_RELAY_MODES),
|
|
68
70
|
effort: text('effort'),
|
|
69
71
|
title: text('title'),
|
|
72
|
+
// The one exception to the parity with the JSON schema: Claude may send
|
|
73
|
+
// null for update_session's group (out of the group), as "none" does on
|
|
74
|
+
// the providers whose strict schemas take a string only.
|
|
75
|
+
group: z.string().nullable().optional().describe(FIELDS.group.description),
|
|
76
|
+
auto_compact_window: number('auto_compact_window'),
|
|
77
|
+
thinking_enabled: z.boolean().optional().describe(FIELDS.thinking_enabled.description),
|
|
78
|
+
thinking_display: choice('thinking_display', REMOTE_RELAY_THINKING_DISPLAYS),
|
|
70
79
|
question_id: text('question_id'),
|
|
80
|
+
// ask: the contract's validator refuses a question or detail that is too
|
|
81
|
+
// long, or more than eight options, with a message the model can act on.
|
|
82
|
+
question: text('question'),
|
|
83
|
+
options: z.array(z.string()).optional().describe(FIELDS.options.description),
|
|
84
|
+
recommendation: text('recommendation'),
|
|
85
|
+
detail: text('detail'),
|
|
86
|
+
urgency: choice('urgency', RELAY_ASK_URGENCIES),
|
|
71
87
|
answer: text('answer'),
|
|
72
88
|
choices: z.array(z.string()).optional().describe(FIELDS.choices.description),
|
|
73
89
|
if_busy: choice('if_busy', REMOTE_RELAY_IF_BUSY),
|
|
@@ -78,18 +94,37 @@ export const REMOTE_RELAY_TOOL_ZOD_SHAPE = {
|
|
|
78
94
|
match: text('match'),
|
|
79
95
|
stalled_minutes: number('stalled_minutes'),
|
|
80
96
|
heartbeat_minutes: number('heartbeat_minutes'),
|
|
97
|
+
digest_minutes: number('digest_minutes'),
|
|
98
|
+
// Loose and every field optional: the contract's validator checks the
|
|
99
|
+
// times and the zone and says what is wrong; an empty one is no option.
|
|
100
|
+
quiet_hours: z.looseObject({
|
|
101
|
+
from: z.string().optional().describe(FIELDS.quiet_hours.properties.from.description),
|
|
102
|
+
to: z.string().optional().describe(FIELDS.quiet_hours.properties.to.description),
|
|
103
|
+
tz: z.string().optional().describe(FIELDS.quiet_hours.properties.tz.description),
|
|
104
|
+
}).optional().describe(FIELDS.quiet_hours.description),
|
|
105
|
+
// A list, or the comma-separated text the contract takes as well.
|
|
106
|
+
exclude: z.union([z.array(z.string()), z.string()]).optional().describe(FIELDS.exclude.description),
|
|
81
107
|
subscription_id: text('subscription_id'),
|
|
82
108
|
watcher: text('watcher'),
|
|
83
109
|
from: text('from'),
|
|
110
|
+
at: text('at'),
|
|
111
|
+
every_minutes: number('every_minutes'),
|
|
112
|
+
schedule_id: text('schedule_id'),
|
|
84
113
|
task_id: text('task_id'),
|
|
85
114
|
lines: number('lines'),
|
|
86
115
|
};
|
|
87
116
|
|
|
117
|
+
// What the tool registers: the shape as a loose object. A raw shape becomes a
|
|
118
|
+
// strict-stripping object in the SDK's MCP layer, which would drop a key the
|
|
119
|
+
// schema lacks before the contract's validator could refuse it (create_session)
|
|
120
|
+
// or name it in `ignored` (the other actions). The loose object lets it through.
|
|
121
|
+
export const REMOTE_RELAY_TOOL_ZOD_SCHEMA = z.looseObject(REMOTE_RELAY_TOOL_ZOD_SHAPE);
|
|
122
|
+
|
|
88
123
|
export function createRemoteRelayToolDefinition({ api, getConversationId = () => '', dbg = () => {} } = {}) {
|
|
89
124
|
return {
|
|
90
125
|
name: REMOTE_RELAY_TOOL_NAME,
|
|
91
126
|
description: REMOTE_RELAY_TOOL_DESCRIPTION,
|
|
92
|
-
inputSchema:
|
|
127
|
+
inputSchema: REMOTE_RELAY_TOOL_ZOD_SCHEMA,
|
|
93
128
|
// `extra.signal` is the MCP request's: the SDK aborts it when the CLI
|
|
94
129
|
// cancels the call (a Stop interrupting the turn) or the process's MCP
|
|
95
130
|
// transport closes, and the pending relay request goes with it.
|
|
@@ -53,7 +53,7 @@ import {
|
|
|
53
53
|
STEER_STOPPED_TEXT,
|
|
54
54
|
} from '../../shared/steer-settle-markers.mjs';
|
|
55
55
|
import { stripSteerNote, withSteerNoteContent } from '../../shared/steer-note.mjs';
|
|
56
|
-
import {
|
|
56
|
+
import { isRelayAutomatedDelivery } from '../../shared/relay-events.mjs';
|
|
57
57
|
import {
|
|
58
58
|
stripSlashCommandGuard,
|
|
59
59
|
stripSlashCommandGuardContent,
|
|
@@ -4209,9 +4209,9 @@ export function createClaudeSessionRunner({
|
|
|
4209
4209
|
// noted steer no longer starts with "/" and is pushed as noted. The
|
|
4210
4210
|
// replay signatures strip the guard too, so expectedText stays the
|
|
4211
4211
|
// user's own text here as well.
|
|
4212
|
-
// Nor into
|
|
4213
|
-
// user,
|
|
4214
|
-
const pushedContent = withSlashCommandGuardContent(steered && procRef.activeCtx?.kind !== 'continuation' && !
|
|
4212
|
+
// Nor into a message the relay wrote (an event, a scheduled prompt):
|
|
4213
|
+
// the note speaks as the user, that message must not (shared/relay-events.mjs).
|
|
4214
|
+
const pushedContent = withSlashCommandGuardContent(steered && procRef.activeCtx?.kind !== 'continuation' && !isRelayAutomatedDelivery(message)
|
|
4215
4215
|
? withSteerNoteContent(content)
|
|
4216
4216
|
: content);
|
|
4217
4217
|
procRef.pendingDelivered.push({ ctx, expectedText: contentText(content), pushedAt: Date.now(), steered });
|
|
@@ -112,7 +112,7 @@ import { STEER_FOLDED_TEXT, STEER_STOPPED_TEXT, STEER_TURN_FAILED_TEXT } from '.
|
|
|
112
112
|
import { buildSteerSettleFailure } from '../../shared/steer-settle-failure.mjs';
|
|
113
113
|
import { shouldEmitStreamUpdate } from '../../shared/stream-emit-gating.mjs';
|
|
114
114
|
import { withSteerNote } from '../../shared/steer-note.mjs';
|
|
115
|
-
import {
|
|
115
|
+
import { isRelayAutomatedDelivery } from '../../shared/relay-events.mjs';
|
|
116
116
|
import { isTransientRelayError } from '../../shared/worker-runtime/relay-errors.mjs';
|
|
117
117
|
import {
|
|
118
118
|
createTurnLiveness,
|
|
@@ -4456,7 +4456,7 @@ export function createCopilotSdkSessionRunner({
|
|
|
4456
4456
|
// Not in a continuation: there the running work is the runtime's own
|
|
4457
4457
|
// follow-up, not a request of the user's to keep going with.
|
|
4458
4458
|
// Nor in an event message the relay wrote: the note speaks as the user.
|
|
4459
|
-
const prompt = withRelayContext(context?.prefix, turn.kind === 'continuation' ||
|
|
4459
|
+
const prompt = withRelayContext(context?.prefix, turn.kind === 'continuation' || isRelayAutomatedDelivery(message) ? body : withSteerNote(body));
|
|
4460
4460
|
// Re-checked AFTER the awaits and before anything is sent or registered.
|
|
4461
4461
|
// Nothing has been sent yet, so handing the row back to the normal path is
|
|
4462
4462
|
// free.
|
package/server/db-schema.mjs
CHANGED
|
@@ -24,6 +24,11 @@ import { migrateConversationGroupPosition } from './migrations/0015-conversation
|
|
|
24
24
|
import { migrateShareAccessLog } from './migrations/0016-share-access-log.mjs';
|
|
25
25
|
import { migrateConversationHistoryIndexes } from './migrations/0017-conversation-history-indexes.mjs';
|
|
26
26
|
import { migrateBackgroundTaskRows } from './migrations/0018-background-task-rows.mjs';
|
|
27
|
+
import { migrateRelayEventSubscriptionOptions } from './migrations/0019-relay-event-subscription-options.mjs';
|
|
28
|
+
import { migrateConversationMayCreateSessions } from './migrations/0020-conversation-may-create-sessions.mjs';
|
|
29
|
+
import { migrateConversationGroupAgentMarks } from './migrations/0021-conversation-group-agent-marks.mjs';
|
|
30
|
+
import { migrateRelaySchedules } from './migrations/0022-relay-schedules.mjs';
|
|
31
|
+
import { migrateRelayAsks } from './migrations/0023-relay-asks.mjs';
|
|
27
32
|
import { migrateImageConversationSchema } from './repositories/image-conversation-repository.mjs';
|
|
28
33
|
|
|
29
34
|
// Mirrors DEFAULT_RELAY_MODE in server-runtime.mjs; used only by the one-time
|
|
@@ -855,6 +860,53 @@ try {
|
|
|
855
860
|
console.warn(`[background-tasks] migration 0018 failed; retrying on next boot: ${error?.message || error}`);
|
|
856
861
|
}
|
|
857
862
|
|
|
863
|
+
// Options of relay event subscriptions (digest, quiet hours, exclude) and the
|
|
864
|
+
// watcher's own prompts: columns on 0011's table and one table. The event
|
|
865
|
+
// service checks for them and refuses the options without them, so a failure
|
|
866
|
+
// is logged and retried next boot like 0018.
|
|
867
|
+
try {
|
|
868
|
+
migrateRelayEventSubscriptionOptions(db);
|
|
869
|
+
} catch (error) {
|
|
870
|
+
console.warn(`[relay-events] migration 0019 failed; retrying on next boot: ${error?.message || error}`);
|
|
871
|
+
}
|
|
872
|
+
|
|
873
|
+
// The owner's "agents in this conversation may start sessions": one column.
|
|
874
|
+
// Without it nobody can turn the switch on (the preferences route refuses
|
|
875
|
+
// it) and the one-level rule holds for every session, so a failure is logged
|
|
876
|
+
// and retried next boot like 0019.
|
|
877
|
+
try {
|
|
878
|
+
migrateConversationMayCreateSessions(db);
|
|
879
|
+
} catch (error) {
|
|
880
|
+
console.warn(`[remote-relays] migration 0020 failed; retrying on next boot: ${error?.message || error}`);
|
|
881
|
+
}
|
|
882
|
+
|
|
883
|
+
// What an agent's update_session may do to a group: two columns. Without
|
|
884
|
+
// them an owner's group an agent emptied is kept until the next boot repair
|
|
885
|
+
// only, so a failure is logged and retried next boot like 0020.
|
|
886
|
+
try {
|
|
887
|
+
migrateConversationGroupAgentMarks(db);
|
|
888
|
+
} catch (error) {
|
|
889
|
+
console.warn(`[session-groups] migration 0021 failed; retrying on next boot: ${error?.message || error}`);
|
|
890
|
+
}
|
|
891
|
+
|
|
892
|
+
// Relay schedules: one new table. The schedule service checks for it and
|
|
893
|
+
// refuses the schedule actions without it, so a failure is logged and
|
|
894
|
+
// retried next boot like 0021.
|
|
895
|
+
try {
|
|
896
|
+
migrateRelaySchedules(db);
|
|
897
|
+
} catch (error) {
|
|
898
|
+
console.warn(`[relay-schedules] migration 0022 failed; retrying on next boot: ${error?.message || error}`);
|
|
899
|
+
}
|
|
900
|
+
|
|
901
|
+
// Questions that don't block (remote_relay ask): one new table. The ask
|
|
902
|
+
// service checks for it and refuses the ask actions without it, so a failure
|
|
903
|
+
// is logged and retried next boot like 0022.
|
|
904
|
+
try {
|
|
905
|
+
migrateRelayAsks(db);
|
|
906
|
+
} catch (error) {
|
|
907
|
+
console.warn(`[relay-asks] migration 0023 failed; retrying on next boot: ${error?.message || error}`);
|
|
908
|
+
}
|
|
909
|
+
|
|
858
910
|
const runtimeSessionColumns = db.prepare(`PRAGMA table_info(runtime_sessions)`).all().map((c) => c.name);
|
|
859
911
|
if (runtimeSessionColumns.length) {
|
|
860
912
|
if (!runtimeSessionColumns.includes('strategy')) {
|
|
@@ -0,0 +1,106 @@
|
|
|
1
|
+
'use strict';
|
|
2
|
+
|
|
3
|
+
/**
|
|
4
|
+
* Migration: options of relay event subscriptions.
|
|
5
|
+
*
|
|
6
|
+
* Columns on `relay_event_subscriptions` (all NULL = off, as before):
|
|
7
|
+
*
|
|
8
|
+
* - `digest_minutes`: the subscription's non-urgent events are held and
|
|
9
|
+
* delivered as one row every N minutes; `digest_last_at` is when its last
|
|
10
|
+
* such row went out (the next one is due N minutes after it).
|
|
11
|
+
* - `quiet_hours_json`: `{ from, to, tz }`; non-urgent events in the window
|
|
12
|
+
* are held until it ends.
|
|
13
|
+
* - `exclude_json`: session ids an "all" subscription leaves out.
|
|
14
|
+
*
|
|
15
|
+
* `relay_event_own_prompts`: the prompts a watcher sent through send or
|
|
16
|
+
* create_session (`message_id` is the prompt's), and when it received the
|
|
17
|
+
* finished reply (`received_at`): the turn_end of such a turn is not
|
|
18
|
+
* delivered to it again. Rows go after a day.
|
|
19
|
+
*
|
|
20
|
+
* Held events are ordinary undelivered `relay_events` rows: they and the
|
|
21
|
+
* timers rebuilt from them survive a restart without anything more here.
|
|
22
|
+
*
|
|
23
|
+
* Additive and idempotent. Safe to run on every boot.
|
|
24
|
+
*
|
|
25
|
+
* Usage: node server/migrations/0019-relay-event-subscription-options.mjs [path/to/copilot.db]
|
|
26
|
+
*/
|
|
27
|
+
|
|
28
|
+
import { fileURLToPath } from 'url';
|
|
29
|
+
import path from 'path';
|
|
30
|
+
import process from 'process';
|
|
31
|
+
import Database from 'better-sqlite3';
|
|
32
|
+
|
|
33
|
+
export const RELAY_EVENT_OPTION_COLUMNS = Object.freeze([
|
|
34
|
+
['digest_minutes', 'INTEGER'],
|
|
35
|
+
['digest_last_at', 'TEXT'],
|
|
36
|
+
['quiet_hours_json', 'TEXT'],
|
|
37
|
+
['exclude_json', 'TEXT'],
|
|
38
|
+
]);
|
|
39
|
+
|
|
40
|
+
function tableExists(db, name) {
|
|
41
|
+
return !!db
|
|
42
|
+
.prepare(`SELECT name FROM sqlite_master WHERE type = 'table' AND name = ?`)
|
|
43
|
+
.get(name);
|
|
44
|
+
}
|
|
45
|
+
|
|
46
|
+
export function migrateRelayEventSubscriptionOptions(db) {
|
|
47
|
+
if (!tableExists(db, 'relay_event_subscriptions')) {
|
|
48
|
+
throw new Error('relay_event_subscriptions is missing (migration 0011 has not run)');
|
|
49
|
+
}
|
|
50
|
+
const present = new Set(db.prepare(`PRAGMA table_info(relay_event_subscriptions)`).all().map((column) => column.name));
|
|
51
|
+
let columnsAdded = 0;
|
|
52
|
+
for (const [name, type] of RELAY_EVENT_OPTION_COLUMNS) {
|
|
53
|
+
if (present.has(name)) continue;
|
|
54
|
+
db.exec(`ALTER TABLE relay_event_subscriptions ADD COLUMN ${name} ${type}`);
|
|
55
|
+
columnsAdded += 1;
|
|
56
|
+
}
|
|
57
|
+
let tablesAdded = 0;
|
|
58
|
+
if (!tableExists(db, 'relay_event_own_prompts')) {
|
|
59
|
+
db.exec(`
|
|
60
|
+
CREATE TABLE relay_event_own_prompts (
|
|
61
|
+
message_id TEXT PRIMARY KEY,
|
|
62
|
+
watcher_conversation_id TEXT NOT NULL,
|
|
63
|
+
relay_id TEXT NOT NULL DEFAULT 'self',
|
|
64
|
+
session_id TEXT NOT NULL,
|
|
65
|
+
created_at TEXT NOT NULL,
|
|
66
|
+
received_at TEXT
|
|
67
|
+
)
|
|
68
|
+
`);
|
|
69
|
+
tablesAdded += 1;
|
|
70
|
+
}
|
|
71
|
+
db.exec(`
|
|
72
|
+
CREATE INDEX IF NOT EXISTS idx_relay_event_own_prompts_created
|
|
73
|
+
ON relay_event_own_prompts(created_at)
|
|
74
|
+
`);
|
|
75
|
+
return { applied: columnsAdded > 0 || tablesAdded > 0, columnsAdded, tablesAdded };
|
|
76
|
+
}
|
|
77
|
+
|
|
78
|
+
function defaultDbPath() {
|
|
79
|
+
const here = path.dirname(fileURLToPath(import.meta.url));
|
|
80
|
+
return path.resolve(here, '..', 'data', 'copilot.db');
|
|
81
|
+
}
|
|
82
|
+
|
|
83
|
+
export function migrate(dbPath, options = {}) {
|
|
84
|
+
const db = new Database(dbPath, options);
|
|
85
|
+
try {
|
|
86
|
+
return migrateRelayEventSubscriptionOptions(db);
|
|
87
|
+
} finally {
|
|
88
|
+
db.close();
|
|
89
|
+
}
|
|
90
|
+
}
|
|
91
|
+
|
|
92
|
+
const isMain = process.argv[1] && path.resolve(process.argv[1]) === fileURLToPath(import.meta.url);
|
|
93
|
+
if (isMain) {
|
|
94
|
+
const dbPath = process.argv[2] ? path.resolve(process.argv[2]) : defaultDbPath();
|
|
95
|
+
try {
|
|
96
|
+
const result = migrate(dbPath);
|
|
97
|
+
console.log(`Migration 0019 on ${dbPath}`);
|
|
98
|
+
console.log(result.applied
|
|
99
|
+
? ` relay event subscription options: columns added ${result.columnsAdded}, tables added ${result.tablesAdded}`
|
|
100
|
+
: ' skipped: nothing to change');
|
|
101
|
+
process.exit(0);
|
|
102
|
+
} catch (err) {
|
|
103
|
+
console.error(`Migration 0019 failed: ${err?.message || err}`);
|
|
104
|
+
process.exit(1);
|
|
105
|
+
}
|
|
106
|
+
}
|
|
@@ -0,0 +1,67 @@
|
|
|
1
|
+
'use strict';
|
|
2
|
+
|
|
3
|
+
/**
|
|
4
|
+
* Migration: the owner's "agents in this conversation may start sessions".
|
|
5
|
+
*
|
|
6
|
+
* `may_create_sessions` is 1 when the owner allowed the agents of a session
|
|
7
|
+
* that an agent started to start sessions on this relay themselves (the
|
|
8
|
+
* remote_relay tool's one-level rule is lifted for it); NULL or 0 otherwise.
|
|
9
|
+
* Only the owner sets it, in the conversation's settings or through the
|
|
10
|
+
* preferences route; no agent can.
|
|
11
|
+
*
|
|
12
|
+
* Additive and idempotent. Safe to run on every boot. Rolling back needs no
|
|
13
|
+
* data change: older code ignores the column and keeps the one-level rule.
|
|
14
|
+
*
|
|
15
|
+
* Usage: node server/migrations/0020-conversation-may-create-sessions.mjs [path/to/copilot.db]
|
|
16
|
+
*/
|
|
17
|
+
|
|
18
|
+
import { fileURLToPath } from 'url';
|
|
19
|
+
import path from 'path';
|
|
20
|
+
import process from 'process';
|
|
21
|
+
import Database from 'better-sqlite3';
|
|
22
|
+
|
|
23
|
+
function tableExists(db, name) {
|
|
24
|
+
return !!db
|
|
25
|
+
.prepare(`SELECT name FROM sqlite_master WHERE type = 'table' AND name = ?`)
|
|
26
|
+
.get(name);
|
|
27
|
+
}
|
|
28
|
+
|
|
29
|
+
export function migrateConversationMayCreateSessions(db) {
|
|
30
|
+
if (!tableExists(db, 'conversations')) {
|
|
31
|
+
throw new Error('conversations is missing (the base schema has not run)');
|
|
32
|
+
}
|
|
33
|
+
const present = new Set(db.prepare(`PRAGMA table_info(conversations)`).all().map((column) => column.name));
|
|
34
|
+
if (present.has('may_create_sessions')) return { applied: false, columnsAdded: 0 };
|
|
35
|
+
db.exec(`ALTER TABLE conversations ADD COLUMN may_create_sessions INTEGER`);
|
|
36
|
+
return { applied: true, columnsAdded: 1 };
|
|
37
|
+
}
|
|
38
|
+
|
|
39
|
+
function defaultDbPath() {
|
|
40
|
+
const here = path.dirname(fileURLToPath(import.meta.url));
|
|
41
|
+
return path.resolve(here, '..', 'data', 'copilot.db');
|
|
42
|
+
}
|
|
43
|
+
|
|
44
|
+
export function migrate(dbPath, options = {}) {
|
|
45
|
+
const db = new Database(dbPath, options);
|
|
46
|
+
try {
|
|
47
|
+
return migrateConversationMayCreateSessions(db);
|
|
48
|
+
} finally {
|
|
49
|
+
db.close();
|
|
50
|
+
}
|
|
51
|
+
}
|
|
52
|
+
|
|
53
|
+
const isMain = process.argv[1] && path.resolve(process.argv[1]) === fileURLToPath(import.meta.url);
|
|
54
|
+
if (isMain) {
|
|
55
|
+
const dbPath = process.argv[2] ? path.resolve(process.argv[2]) : defaultDbPath();
|
|
56
|
+
try {
|
|
57
|
+
const result = migrate(dbPath);
|
|
58
|
+
console.log(`Migration 0020 on ${dbPath}`);
|
|
59
|
+
console.log(result.applied
|
|
60
|
+
? ` may create sessions: columns added ${result.columnsAdded}`
|
|
61
|
+
: ' skipped: nothing to change');
|
|
62
|
+
process.exit(0);
|
|
63
|
+
} catch (err) {
|
|
64
|
+
console.error(`Migration 0020 failed: ${err?.message || err}`);
|
|
65
|
+
process.exit(1);
|
|
66
|
+
}
|
|
67
|
+
}
|
|
@@ -0,0 +1,77 @@
|
|
|
1
|
+
'use strict';
|
|
2
|
+
|
|
3
|
+
/**
|
|
4
|
+
* Migration: what an agent's update_session may do to a session group.
|
|
5
|
+
*
|
|
6
|
+
* - `conversation_groups.agent_made` is 1 for a group update_session made
|
|
7
|
+
* (a name the relay did not have). Only such a group goes once an agent
|
|
8
|
+
* takes its last member out.
|
|
9
|
+
* - `conversation_groups.kept_empty_at` is set when an agent took the last
|
|
10
|
+
* member out of a group the owner made: the group stays (empty), the boot
|
|
11
|
+
* repair and the sweep leave it alone, and agents can still name it. The
|
|
12
|
+
* mark goes as soon as a member joins again.
|
|
13
|
+
*
|
|
14
|
+
* Additive and idempotent. Safe to run on every boot. Rolling back needs no
|
|
15
|
+
* data change: older code ignores both columns (its boot repair removes an
|
|
16
|
+
* empty group, as it always did).
|
|
17
|
+
*
|
|
18
|
+
* Usage: node server/migrations/0021-conversation-group-agent-marks.mjs [path/to/copilot.db]
|
|
19
|
+
*/
|
|
20
|
+
|
|
21
|
+
import { fileURLToPath } from 'url';
|
|
22
|
+
import path from 'path';
|
|
23
|
+
import process from 'process';
|
|
24
|
+
import Database from 'better-sqlite3';
|
|
25
|
+
|
|
26
|
+
function tableExists(db, name) {
|
|
27
|
+
return !!db
|
|
28
|
+
.prepare(`SELECT name FROM sqlite_master WHERE type = 'table' AND name = ?`)
|
|
29
|
+
.get(name);
|
|
30
|
+
}
|
|
31
|
+
|
|
32
|
+
export function migrateConversationGroupAgentMarks(db) {
|
|
33
|
+
if (!tableExists(db, 'conversation_groups')) {
|
|
34
|
+
throw new Error('conversation_groups is missing (migration 0014 has not run)');
|
|
35
|
+
}
|
|
36
|
+
const present = new Set(db.prepare(`PRAGMA table_info(conversation_groups)`).all().map((column) => column.name));
|
|
37
|
+
let columnsAdded = 0;
|
|
38
|
+
if (!present.has('agent_made')) {
|
|
39
|
+
db.exec(`ALTER TABLE conversation_groups ADD COLUMN agent_made INTEGER`);
|
|
40
|
+
columnsAdded += 1;
|
|
41
|
+
}
|
|
42
|
+
if (!present.has('kept_empty_at')) {
|
|
43
|
+
db.exec(`ALTER TABLE conversation_groups ADD COLUMN kept_empty_at TEXT`);
|
|
44
|
+
columnsAdded += 1;
|
|
45
|
+
}
|
|
46
|
+
return { applied: columnsAdded > 0, columnsAdded };
|
|
47
|
+
}
|
|
48
|
+
|
|
49
|
+
function defaultDbPath() {
|
|
50
|
+
const here = path.dirname(fileURLToPath(import.meta.url));
|
|
51
|
+
return path.resolve(here, '..', 'data', 'copilot.db');
|
|
52
|
+
}
|
|
53
|
+
|
|
54
|
+
export function migrate(dbPath, options = {}) {
|
|
55
|
+
const db = new Database(dbPath, options);
|
|
56
|
+
try {
|
|
57
|
+
return migrateConversationGroupAgentMarks(db);
|
|
58
|
+
} finally {
|
|
59
|
+
db.close();
|
|
60
|
+
}
|
|
61
|
+
}
|
|
62
|
+
|
|
63
|
+
const isMain = process.argv[1] && path.resolve(process.argv[1]) === fileURLToPath(import.meta.url);
|
|
64
|
+
if (isMain) {
|
|
65
|
+
const dbPath = process.argv[2] ? path.resolve(process.argv[2]) : defaultDbPath();
|
|
66
|
+
try {
|
|
67
|
+
const result = migrate(dbPath);
|
|
68
|
+
console.log(`Migration 0021 on ${dbPath}`);
|
|
69
|
+
console.log(result.applied
|
|
70
|
+
? ` group agent marks: columns added ${result.columnsAdded}`
|
|
71
|
+
: ' skipped: nothing to change');
|
|
72
|
+
process.exit(0);
|
|
73
|
+
} catch (err) {
|
|
74
|
+
console.error(`Migration 0021 failed: ${err?.message || err}`);
|
|
75
|
+
process.exit(1);
|
|
76
|
+
}
|
|
77
|
+
}
|