@oar-sh/oar 0.9.25 → 0.9.26
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 +142 -0
- package/README.md +9 -9
- package/package.json +3 -1
- package/server/claude-worker/claude-sdk-adapter.mjs +8 -4
- package/server/claude-worker/claude-session-process.mjs +4 -0
- package/server/claude-worker/claude-turn-publisher.mjs +25 -11
- package/server/copilot-worker/copilot-prompt-context.mjs +2 -1
- package/server/cursor-worker/cursor-turn-runner.mjs +3 -1
- package/server/grok-worker/grok-turn-runner.mjs +3 -1
- package/server/public/app/context-usage-view.mjs +2 -2
- package/server/public/app/live-activity-cap.mjs +3 -3
- package/server/public/app/markdown-math.mjs +471 -0
- package/server/public/app/pwa-install.js +25 -4
- package/server/public/app/relay-prompt-sanitizer.mjs +3 -0
- package/server/public/app/relay-restart-ui.js +2 -2
- package/server/public/app/remote-relays-settings.js +33 -6
- package/server/public/app/router.js +49 -237
- package/server/public/app/settings-tabs.js +10 -1
- package/server/public/app/store.js +2 -0
- package/server/public/app/stream-view-preference.mjs +1 -1
- package/server/public/app/thoughts-preference.mjs +1 -1
- package/server/public/app/tmux-inspector-view.js +29 -16
- package/server/public/index.html +134 -116
- package/server/routes/relay-board-routes.mjs +46 -1
- package/server/routes/remote-relay-routes.mjs +1 -1
- package/server/server-runtime.mjs +2 -0
- package/server/services/relay-prompt-sanitizer.mjs +7 -0
- package/server/services/relay-restart-queue.mjs +1 -1
- package/server/services/remote-relay-dispatcher.mjs +23 -1
- package/server/services/remote-relay-registry-service.mjs +7 -1
- package/server/services/session-worker-stop-service.mjs +50 -18
- package/shared/math-instructions.mjs +30 -0
- package/server/public/app/math-dollars.mjs +0 -89
package/server/public/index.html
CHANGED
|
@@ -211,18 +211,17 @@
|
|
|
211
211
|
<title>OAR — Open Agent Relay</title>
|
|
212
212
|
<script>document.write('<scr'+'ipt src="'+__APP_BASE+'/socket.io/socket.io.js"><\/scr'+'ipt>');</script>
|
|
213
213
|
<script src="https://cdn.jsdelivr.net/npm/marked@15.0.12/marked.min.js"></script>
|
|
214
|
-
<link rel="stylesheet" href="https://cdn.jsdelivr.net/npm/katex@0.
|
|
215
|
-
<script src="https://cdn.jsdelivr.net/npm/katex@0.
|
|
216
|
-
<script src="https://cdn.jsdelivr.net/npm/katex@0.
|
|
217
|
-
<script src="https://cdn.jsdelivr.net/npm/katex@0.16.22/dist/contrib/auto-render.min.js"></script>
|
|
214
|
+
<link rel="stylesheet" href="https://cdn.jsdelivr.net/npm/katex@0.19.1/dist/katex.min.css" integrity="sha384-WwGtaC6ywCHxA5cstpj/3+xIZvTez8dq2IcBQaXRkenHZ7FVVq1ZROtURwfbtTlG" crossorigin="anonymous">
|
|
215
|
+
<script src="https://cdn.jsdelivr.net/npm/katex@0.19.1/dist/katex.min.js" integrity="sha384-a1WQdjRj162i159OYq041X6I4K8NenkrI09LlPmiHg0YOb7CR+wJOraXwK9abpnE" crossorigin="anonymous"></script>
|
|
216
|
+
<script src="https://cdn.jsdelivr.net/npm/katex@0.19.1/dist/contrib/mhchem.min.js" integrity="sha384-fB8BH//9nBzROkMUsu/Dr35jWHIbnKesUo9rW0hfEgw8mZGnkAyBAjKX9F98OVuo" crossorigin="anonymous"></script>
|
|
218
217
|
<link rel="stylesheet" href="https://cdnjs.cloudflare.com/ajax/libs/highlight.js/11.9.0/styles/github-dark.min.css">
|
|
219
218
|
<script src="https://cdnjs.cloudflare.com/ajax/libs/highlight.js/11.9.0/highlight.min.js"></script>
|
|
220
219
|
<script src="https://cdnjs.cloudflare.com/ajax/libs/highlight.js/11.9.0/languages/nginx.min.js"></script>
|
|
221
220
|
<script src="https://cdnjs.cloudflare.com/ajax/libs/highlight.js/11.9.0/languages/powershell.min.js"></script>
|
|
222
221
|
<script src="https://cdnjs.cloudflare.com/ajax/libs/highlight.js/11.9.0/languages/x86asm.min.js"></script>
|
|
223
|
-
<link rel="stylesheet" href="https://cdn.jsdelivr.net/npm/xterm@5.5.0/css/xterm.
|
|
224
|
-
<script src="https://cdn.jsdelivr.net/npm/xterm@5.5.0/lib/xterm.
|
|
225
|
-
<script src="https://cdn.jsdelivr.net/npm/
|
|
222
|
+
<link rel="stylesheet" href="https://cdn.jsdelivr.net/npm/@xterm/xterm@5.5.0/css/xterm.css" integrity="sha384-8Xk9wy/gzEDUKrXtrmCFa2bBuK3BpjpDuL/p0SeKQX19Khl/M+lHOgD/CyYf7efP" crossorigin="anonymous">
|
|
223
|
+
<script src="https://cdn.jsdelivr.net/npm/@xterm/xterm@5.5.0/lib/xterm.js" integrity="sha384-M169f14mRZOXm3hD/v2Ti0ThIT/RnAQagXA9nlE15yHAtrW19gdePJh/HaTzUOe/" crossorigin="anonymous"></script>
|
|
224
|
+
<script src="https://cdn.jsdelivr.net/npm/@xterm/addon-fit@0.10.0/lib/addon-fit.js" integrity="sha384-iF+jqbuti4XlB64clWgFWYEscb+UnSRv3VgVikGYZu+otNFnSHr7y7NcKfBnGizn" crossorigin="anonymous"></script>
|
|
226
225
|
<script>
|
|
227
226
|
if (window.hljs && typeof window.hljs.registerAliases === 'function') {
|
|
228
227
|
window.hljs.registerAliases(['pwsh', 'ps1'], { languageName: 'powershell' });
|
|
@@ -1978,6 +1977,8 @@
|
|
|
1978
1977
|
}
|
|
1979
1978
|
.settings-section:first-child .settings-section-title { margin-top: 0; }
|
|
1980
1979
|
.settings-section-title + .settings-row { margin-top: 0; }
|
|
1980
|
+
/* Settings → Appearance: the one line saying where its settings live. */
|
|
1981
|
+
#appearance-browser-note { margin-bottom: calc(8px * var(--ui-scale)); }
|
|
1981
1982
|
/* A row inside a column block: the block's full width; the lead row (the
|
|
1982
1983
|
provider's on/off toggle) keeps a small gap to the fields below. */
|
|
1983
1984
|
.settings-row-full { width: 100%; }
|
|
@@ -2429,6 +2430,9 @@
|
|
|
2429
2430
|
The anchor class comes from JS rather than :has() so positioning works
|
|
2430
2431
|
everywhere the transcript renders. */
|
|
2431
2432
|
pre.has-code-copy { position: relative; }
|
|
2433
|
+
/* The button gets a strip of its own above the first line, so a long line
|
|
2434
|
+
never runs under it. */
|
|
2435
|
+
pre.has-code-copy > code { padding-top: calc(0.72rem + 12px * var(--ui-scale) + 4px); }
|
|
2432
2436
|
.code-copy-btn {
|
|
2433
2437
|
position: absolute;
|
|
2434
2438
|
top: calc(4px * var(--ui-scale));
|
|
@@ -2470,8 +2474,8 @@
|
|
|
2470
2474
|
vertical-align: bottom;
|
|
2471
2475
|
-webkit-overflow-scrolling: touch;
|
|
2472
2476
|
}
|
|
2473
|
-
.
|
|
2474
|
-
|
|
2477
|
+
/* A formula KaTeX could not set is shown as its source, line breaks kept. */
|
|
2478
|
+
.math-source {
|
|
2475
2479
|
white-space: pre-wrap;
|
|
2476
2480
|
}
|
|
2477
2481
|
.msg-bubble code:not(pre code) {
|
|
@@ -5066,8 +5070,7 @@
|
|
|
5066
5070
|
background: var(--bg); color: var(--text); border: 1px solid var(--border);
|
|
5067
5071
|
border-radius: calc(6px * var(--ui-scale)); padding: calc(2px * var(--ui-scale)) calc(4px * var(--ui-scale)); font-size: var(--fs-meta-sm);
|
|
5068
5072
|
}
|
|
5069
|
-
|
|
5070
|
-
.remote-relays-self-name { font-family: var(--mono, monospace); color: var(--text); }
|
|
5073
|
+
#settings-panel-relays #pwa-app-name-input,
|
|
5071
5074
|
#remote-relays-public-url-input,
|
|
5072
5075
|
#remote-relays-add-url-input,
|
|
5073
5076
|
#remote-relays-add-token-input {
|
|
@@ -8844,6 +8847,16 @@
|
|
|
8844
8847
|
tabindex="0"
|
|
8845
8848
|
data-settings-tab="general"
|
|
8846
8849
|
>General</button>
|
|
8850
|
+
<button
|
|
8851
|
+
id="settings-tab-appearance"
|
|
8852
|
+
class="settings-tab"
|
|
8853
|
+
type="button"
|
|
8854
|
+
role="tab"
|
|
8855
|
+
aria-selected="false"
|
|
8856
|
+
aria-controls="settings-panel-appearance"
|
|
8857
|
+
tabindex="-1"
|
|
8858
|
+
data-settings-tab="appearance"
|
|
8859
|
+
>Appearance</button>
|
|
8847
8860
|
<button
|
|
8848
8861
|
id="settings-tab-providers"
|
|
8849
8862
|
class="settings-tab"
|
|
@@ -8914,68 +8927,6 @@
|
|
|
8914
8927
|
aria-labelledby="settings-tab-general"
|
|
8915
8928
|
data-settings-panel="general"
|
|
8916
8929
|
>
|
|
8917
|
-
<section class="settings-section" id="settings-section-appearance" aria-labelledby="settings-section-appearance-title">
|
|
8918
|
-
<h3 class="settings-section-title" id="settings-section-appearance-title">Appearance</h3>
|
|
8919
|
-
<div class="settings-row">
|
|
8920
|
-
<label for="theme-select">Theme</label>
|
|
8921
|
-
<select id="theme-select" onchange="updateTheme(this.value)">
|
|
8922
|
-
<option value="dark">Dark</option>
|
|
8923
|
-
<option value="light">Light</option>
|
|
8924
|
-
</select>
|
|
8925
|
-
</div>
|
|
8926
|
-
<div class="settings-row" style="margin-bottom: 0;">
|
|
8927
|
-
<label for="font-scale-select">Text scaling</label>
|
|
8928
|
-
<select id="font-scale-select" onchange="updateFontScaleFromSelect(this.value)" aria-label="Text scaling"></select>
|
|
8929
|
-
</div>
|
|
8930
|
-
<div class="settings-row settings-gap">
|
|
8931
|
-
<label for="thoughts-expanded-toggle">Expand thoughts while an agent works</label>
|
|
8932
|
-
<input
|
|
8933
|
-
id="thoughts-expanded-toggle"
|
|
8934
|
-
type="checkbox"
|
|
8935
|
-
onchange="updateThoughtsExpandedSetting(this.checked)"
|
|
8936
|
-
aria-label="Expand thoughts while an agent works"
|
|
8937
|
-
checked
|
|
8938
|
-
>
|
|
8939
|
-
</div>
|
|
8940
|
-
<div class="settings-help settings-gap-xs">Open thoughts in the live turn, in sub-agent bubbles and in the sub-agent viewer. Saved in this browser.</div>
|
|
8941
|
-
<div class="settings-row settings-row-column settings-gap">
|
|
8942
|
-
<label for="live-activity-cap-slider">Tool lines shown while an agent works <span id="live-activity-cap-value">50 lines</span></label>
|
|
8943
|
-
<input
|
|
8944
|
-
id="live-activity-cap-slider"
|
|
8945
|
-
type="range"
|
|
8946
|
-
min="10"
|
|
8947
|
-
max="510"
|
|
8948
|
-
step="10"
|
|
8949
|
-
value="50"
|
|
8950
|
-
aria-label="Tool lines shown while an agent works"
|
|
8951
|
-
aria-valuetext="50 lines"
|
|
8952
|
-
oninput="previewLiveActivityCap(this.value)"
|
|
8953
|
-
onchange="updateLiveActivityCap(this.value)"
|
|
8954
|
-
>
|
|
8955
|
-
</div>
|
|
8956
|
-
<div class="settings-help settings-gap-xs">Per agent: the main agent and each sub-agent keep this many of their newest tool lines in the live bubble. The relay keeps up to 500 per agent. Saved in this browser.</div>
|
|
8957
|
-
<div class="settings-row settings-gap">
|
|
8958
|
-
<label for="stream-view-toggle">Stream view: thoughts, tool calls and the reply in order</label>
|
|
8959
|
-
<input
|
|
8960
|
-
id="stream-view-toggle"
|
|
8961
|
-
type="checkbox"
|
|
8962
|
-
onchange="updateStreamViewSetting(this.checked)"
|
|
8963
|
-
aria-label="Stream view: thoughts, tool calls and the reply in order"
|
|
8964
|
-
>
|
|
8965
|
-
</div>
|
|
8966
|
-
<div class="settings-help settings-gap-xs">Turns recorded before this version keep the grouped layout. Saved in this browser.</div>
|
|
8967
|
-
<div class="settings-row settings-gap">
|
|
8968
|
-
<label for="show-suspend-host-toggle">Show Suspend host action</label>
|
|
8969
|
-
<input
|
|
8970
|
-
id="show-suspend-host-toggle"
|
|
8971
|
-
type="checkbox"
|
|
8972
|
-
onchange="updateShowSuspendHostSetting(this.checked)"
|
|
8973
|
-
aria-label="Show Suspend host action"
|
|
8974
|
-
checked
|
|
8975
|
-
>
|
|
8976
|
-
</div>
|
|
8977
|
-
<div class="settings-help settings-gap-xs">Shows or hides the menu action in this browser only. Suspend works only when the relay runs on Windows; a queued suspend still shows its banner here either way.</div>
|
|
8978
|
-
</section>
|
|
8979
8930
|
<section class="settings-section" id="settings-section-reading" aria-labelledby="settings-section-reading-title">
|
|
8980
8931
|
<h3 class="settings-section-title" id="settings-section-reading-title">Reading</h3>
|
|
8981
8932
|
<div class="settings-row settings-gap">
|
|
@@ -9090,44 +9041,8 @@
|
|
|
9090
9041
|
<div id="model-refresh-providers" class="settings-gap" role="group" aria-label="Providers checked for new models"></div>
|
|
9091
9042
|
<div class="settings-help settings-gap-xs">The relay asks each switched-on provider for its model list this often. A provider whose list was refreshed in the meantime (Refresh in Select Models, a settings save, a sign-in) waits a full interval again, and a failed check is tried again after an hour at the earliest. Whether a new model starts on is set per provider in Select Models. A setting of this relay.</div>
|
|
9092
9043
|
</section>
|
|
9093
|
-
<section class="settings-section" id="settings-section-
|
|
9094
|
-
<h3 class="settings-section-title" id="settings-section-
|
|
9095
|
-
<div class="settings-row settings-row-column settings-gap">
|
|
9096
|
-
<label for="pwa-app-name-input">Relay name</label>
|
|
9097
|
-
<input
|
|
9098
|
-
id="pwa-app-name-input"
|
|
9099
|
-
type="text"
|
|
9100
|
-
maxlength="60"
|
|
9101
|
-
placeholder="OAR"
|
|
9102
|
-
aria-label="Relay name"
|
|
9103
|
-
onchange="updatePwaAppName(this.value)"
|
|
9104
|
-
>
|
|
9105
|
-
<div class="settings-help">Shown to other relays and used as the installed app's name. On Android, a rename reaches an already-installed app after a short delay, with one confirmation prompt showing the new name. A setting of this relay.</div>
|
|
9106
|
-
</div>
|
|
9107
|
-
<div class="settings-row settings-gap">
|
|
9108
|
-
<label for="restart-resume-toggle">After a relay restart, tell interrupted sessions to carry on</label>
|
|
9109
|
-
<input
|
|
9110
|
-
id="restart-resume-toggle"
|
|
9111
|
-
type="checkbox"
|
|
9112
|
-
onchange="updateRestartResumeSettingFromToggle(this.checked)"
|
|
9113
|
-
aria-label="After a relay restart, tell interrupted sessions to carry on"
|
|
9114
|
-
>
|
|
9115
|
-
</div>
|
|
9116
|
-
<div class="settings-help settings-gap-xs">Most turns a restart interrupts run again by themselves. A turn that cannot (the agent had already taken the message, or had started on its own after a background task) just stops. With this on, such a session gets one relay note once the relay is back, saying when it restarted and asking the agent to check what it was doing and carry on. Your message is never sent again; idle, archived and usage-limit-paused sessions get nothing. Off by default; a setting of this relay.</div>
|
|
9117
|
-
<div class="settings-row settings-gap">
|
|
9118
|
-
<label for="relay-restart-max-wait-select">Ask after a queued relay restart has waited</label>
|
|
9119
|
-
<select id="relay-restart-max-wait-select" onchange="updateRelayRestartMaxWaitSetting(this.value)" aria-label="Ask after a queued relay restart has waited">
|
|
9120
|
-
<option value="5">5 min</option>
|
|
9121
|
-
<option value="10">10 min</option>
|
|
9122
|
-
<option value="15">15 min</option>
|
|
9123
|
-
<option value="30" selected>30 min</option>
|
|
9124
|
-
<option value="60">1 h</option>
|
|
9125
|
-
<option value="120">2 h</option>
|
|
9126
|
-
<option value="240">4 h</option>
|
|
9127
|
-
<option value="0">No limit</option>
|
|
9128
|
-
</select>
|
|
9129
|
-
</div>
|
|
9130
|
-
<div class="settings-help settings-gap-xs">A queued relay restart waits for the running turns, and new messages wait for the restart. After this long the banner asks whether to restart now, keep waiting or cancel; it never restarts while the turns run, and still restarts as soon as they end. No limit waits until the turns end. A setting of this relay.</div>
|
|
9044
|
+
<section class="settings-section" id="settings-section-computer" aria-labelledby="settings-section-computer-title">
|
|
9045
|
+
<h3 class="settings-section-title" id="settings-section-computer-title">This computer</h3>
|
|
9131
9046
|
<div class="settings-row settings-row-column settings-gap">
|
|
9132
9047
|
<label for="default-session-workspace-root-input">Default CWD for new sessions</label>
|
|
9133
9048
|
<input
|
|
@@ -9204,6 +9119,80 @@
|
|
|
9204
9119
|
</div>
|
|
9205
9120
|
</section>
|
|
9206
9121
|
</div>
|
|
9122
|
+
<div
|
|
9123
|
+
id="settings-panel-appearance"
|
|
9124
|
+
class="settings-panel"
|
|
9125
|
+
role="tabpanel"
|
|
9126
|
+
tabindex="0"
|
|
9127
|
+
aria-labelledby="settings-tab-appearance"
|
|
9128
|
+
data-settings-panel="appearance"
|
|
9129
|
+
hidden
|
|
9130
|
+
>
|
|
9131
|
+
<div id="appearance-browser-note" class="settings-help">These settings are kept in this browser; other devices keep their own.</div>
|
|
9132
|
+
<div class="settings-row">
|
|
9133
|
+
<label for="theme-select">Theme</label>
|
|
9134
|
+
<select id="theme-select" onchange="updateTheme(this.value)" aria-describedby="appearance-browser-note">
|
|
9135
|
+
<option value="dark">Dark</option>
|
|
9136
|
+
<option value="light">Light</option>
|
|
9137
|
+
</select>
|
|
9138
|
+
</div>
|
|
9139
|
+
<div class="settings-row" style="margin-bottom: 0;">
|
|
9140
|
+
<label for="font-scale-select">Text scaling</label>
|
|
9141
|
+
<select id="font-scale-select" onchange="updateFontScaleFromSelect(this.value)" aria-label="Text scaling" aria-describedby="appearance-browser-note"></select>
|
|
9142
|
+
</div>
|
|
9143
|
+
<div class="settings-row settings-gap">
|
|
9144
|
+
<label for="thoughts-expanded-toggle">Expand thoughts while an agent works</label>
|
|
9145
|
+
<input
|
|
9146
|
+
id="thoughts-expanded-toggle"
|
|
9147
|
+
aria-describedby="appearance-browser-note"
|
|
9148
|
+
type="checkbox"
|
|
9149
|
+
onchange="updateThoughtsExpandedSetting(this.checked)"
|
|
9150
|
+
aria-label="Expand thoughts while an agent works"
|
|
9151
|
+
checked
|
|
9152
|
+
>
|
|
9153
|
+
</div>
|
|
9154
|
+
<div class="settings-help settings-gap-xs">Open thoughts in the live turn, in sub-agent bubbles and in the sub-agent viewer.</div>
|
|
9155
|
+
<div class="settings-row settings-row-column settings-gap">
|
|
9156
|
+
<label for="live-activity-cap-slider">Tool lines shown while an agent works <span id="live-activity-cap-value">50 lines</span></label>
|
|
9157
|
+
<input
|
|
9158
|
+
id="live-activity-cap-slider"
|
|
9159
|
+
aria-describedby="appearance-browser-note"
|
|
9160
|
+
type="range"
|
|
9161
|
+
min="10"
|
|
9162
|
+
max="510"
|
|
9163
|
+
step="10"
|
|
9164
|
+
value="50"
|
|
9165
|
+
aria-label="Tool lines shown while an agent works"
|
|
9166
|
+
aria-valuetext="50 lines"
|
|
9167
|
+
oninput="previewLiveActivityCap(this.value)"
|
|
9168
|
+
onchange="updateLiveActivityCap(this.value)"
|
|
9169
|
+
>
|
|
9170
|
+
</div>
|
|
9171
|
+
<div class="settings-help settings-gap-xs">Per agent: the main agent and each sub-agent keep this many of their newest tool lines in the live bubble. The relay keeps up to 500 per agent.</div>
|
|
9172
|
+
<div class="settings-row settings-gap">
|
|
9173
|
+
<label for="stream-view-toggle">Stream view: thoughts, tool calls and the reply in order</label>
|
|
9174
|
+
<input
|
|
9175
|
+
id="stream-view-toggle"
|
|
9176
|
+
aria-describedby="appearance-browser-note"
|
|
9177
|
+
type="checkbox"
|
|
9178
|
+
onchange="updateStreamViewSetting(this.checked)"
|
|
9179
|
+
aria-label="Stream view: thoughts, tool calls and the reply in order"
|
|
9180
|
+
>
|
|
9181
|
+
</div>
|
|
9182
|
+
<div class="settings-help settings-gap-xs">Turns recorded before this version keep the grouped layout.</div>
|
|
9183
|
+
<div class="settings-row settings-gap">
|
|
9184
|
+
<label for="show-suspend-host-toggle">Show Suspend host action</label>
|
|
9185
|
+
<input
|
|
9186
|
+
id="show-suspend-host-toggle"
|
|
9187
|
+
aria-describedby="appearance-browser-note"
|
|
9188
|
+
type="checkbox"
|
|
9189
|
+
onchange="updateShowSuspendHostSetting(this.checked)"
|
|
9190
|
+
aria-label="Show Suspend host action"
|
|
9191
|
+
checked
|
|
9192
|
+
>
|
|
9193
|
+
</div>
|
|
9194
|
+
<div class="settings-help settings-gap-xs">Shows or hides the menu action in this browser only. Suspend works only when the relay runs on Windows; a queued suspend still shows its banner here either way.</div>
|
|
9195
|
+
</div>
|
|
9207
9196
|
<div
|
|
9208
9197
|
id="settings-panel-providers"
|
|
9209
9198
|
class="settings-panel"
|
|
@@ -9936,11 +9925,16 @@
|
|
|
9936
9925
|
hidden
|
|
9937
9926
|
>
|
|
9938
9927
|
<div id="remote-relays-self-section" class="settings-row settings-row-column settings-gap">
|
|
9939
|
-
<label>
|
|
9940
|
-
<
|
|
9941
|
-
|
|
9942
|
-
|
|
9943
|
-
|
|
9928
|
+
<label for="pwa-app-name-input">Relay name</label>
|
|
9929
|
+
<input
|
|
9930
|
+
id="pwa-app-name-input"
|
|
9931
|
+
type="text"
|
|
9932
|
+
maxlength="60"
|
|
9933
|
+
placeholder="OAR"
|
|
9934
|
+
aria-label="Relay name"
|
|
9935
|
+
onchange="updatePwaAppName(this.value)"
|
|
9936
|
+
>
|
|
9937
|
+
<div class="settings-help">Shown to other relays and used as the installed app's name. On Android, a rename reaches an already-installed app after a short delay, with one confirmation prompt showing the new name. A setting of this relay.</div>
|
|
9944
9938
|
</div>
|
|
9945
9939
|
<div class="settings-row settings-row-column settings-gap">
|
|
9946
9940
|
<label for="remote-relays-public-url-input">Public address</label>
|
|
@@ -9958,6 +9952,30 @@
|
|
|
9958
9952
|
<div class="settings-help">How other relays reach this one — the address you open this relay at from another device. Needed for "Also add this relay there".</div>
|
|
9959
9953
|
<div id="remote-relays-self-status" class="settings-help settings-provider-status" role="status" aria-live="polite" hidden></div>
|
|
9960
9954
|
</div>
|
|
9955
|
+
<div class="settings-row settings-gap">
|
|
9956
|
+
<label for="restart-resume-toggle">After a relay restart, tell interrupted sessions to carry on</label>
|
|
9957
|
+
<input
|
|
9958
|
+
id="restart-resume-toggle"
|
|
9959
|
+
type="checkbox"
|
|
9960
|
+
onchange="updateRestartResumeSettingFromToggle(this.checked)"
|
|
9961
|
+
aria-label="After a relay restart, tell interrupted sessions to carry on"
|
|
9962
|
+
>
|
|
9963
|
+
</div>
|
|
9964
|
+
<div class="settings-help settings-gap-xs">Most turns a restart interrupts run again by themselves. A turn that cannot (the agent had already taken the message, or had started on its own after a background task) just stops. With this on, such a session gets one relay note once the relay is back, saying when it restarted and asking the agent to check what it was doing and carry on. Your message is never sent again; idle, archived and usage-limit-paused sessions get nothing. Off by default; a setting of this relay.</div>
|
|
9965
|
+
<div class="settings-row settings-gap">
|
|
9966
|
+
<label for="relay-restart-max-wait-select">Ask after a queued relay restart has waited</label>
|
|
9967
|
+
<select id="relay-restart-max-wait-select" onchange="updateRelayRestartMaxWaitSetting(this.value)" aria-label="Ask after a queued relay restart has waited">
|
|
9968
|
+
<option value="5">5 min</option>
|
|
9969
|
+
<option value="10">10 min</option>
|
|
9970
|
+
<option value="15">15 min</option>
|
|
9971
|
+
<option value="30" selected>30 min</option>
|
|
9972
|
+
<option value="60">1 h</option>
|
|
9973
|
+
<option value="120">2 h</option>
|
|
9974
|
+
<option value="240">4 h</option>
|
|
9975
|
+
<option value="0">No limit</option>
|
|
9976
|
+
</select>
|
|
9977
|
+
</div>
|
|
9978
|
+
<div class="settings-help settings-gap-xs">A queued relay restart waits for the running turns, and new messages wait for the restart. After this long the banner asks whether to restart now, keep waiting or cancel; it never restarts while the turns run, and still restarts as soon as they end. No limit waits until the turns end. A setting of this relay.</div>
|
|
9961
9979
|
<div class="settings-row settings-gap">
|
|
9962
9980
|
<label for="remote-relays-inbound-toggle">Accept prompts from other relays' agents</label>
|
|
9963
9981
|
<input
|
|
@@ -21,6 +21,51 @@ function normalizeBoardText(value, fallback = '') {
|
|
|
21
21
|
return text.slice(0, 12_000);
|
|
22
22
|
}
|
|
23
23
|
|
|
24
|
+
// A plan is shown whole on its card: the old shared limit of 12,000 characters
|
|
25
|
+
// cut a longer one in mid-sentence and left no mark. Past this size the card
|
|
26
|
+
// says how much is missing.
|
|
27
|
+
const BOARD_BODY_MAX_CHARS = 200_000;
|
|
28
|
+
|
|
29
|
+
/**
|
|
30
|
+
* The code fence still open at the end of `text`, as the characters that
|
|
31
|
+
* close it, or ''. By the Markdown rules: a fence is three or more backticks
|
|
32
|
+
* or tildes behind at most three spaces (a backtick fence's info string holds
|
|
33
|
+
* no backtick); it is closed by a line of the same character, at least as
|
|
34
|
+
* long, with nothing but blanks behind it. Lines inside it open nothing.
|
|
35
|
+
*/
|
|
36
|
+
function openCodeFence(text) {
|
|
37
|
+
let open = null;
|
|
38
|
+
// A plan file read on Windows ends its lines with CRLF.
|
|
39
|
+
for (const line of text.split(/\r?\n/)) {
|
|
40
|
+
const fence = /^ {0,3}(`{3,}|~{3,})(.*)$/.exec(line);
|
|
41
|
+
if (!fence) continue;
|
|
42
|
+
const [, run, rest] = fence;
|
|
43
|
+
if (!open) {
|
|
44
|
+
if (run[0] === '`' && rest.includes('`')) continue;
|
|
45
|
+
open = { char: run[0], length: run.length };
|
|
46
|
+
} else if (run[0] === open.char && run.length >= open.length && !rest.trim()) {
|
|
47
|
+
open = null;
|
|
48
|
+
}
|
|
49
|
+
}
|
|
50
|
+
return open ? open.char.repeat(open.length) : '';
|
|
51
|
+
}
|
|
52
|
+
|
|
53
|
+
function normalizeBoardBody(value, context) {
|
|
54
|
+
const text = String(value || '').trim();
|
|
55
|
+
if (text.length <= BOARD_BODY_MAX_CHARS) return text;
|
|
56
|
+
const lineBreak = text.lastIndexOf('\n', BOARD_BODY_MAX_CHARS);
|
|
57
|
+
let kept = text.slice(0, lineBreak > BOARD_BODY_MAX_CHARS / 2 ? lineBreak : BOARD_BODY_MAX_CHARS);
|
|
58
|
+
// Not half a character.
|
|
59
|
+
if (/[\uD800-\uDBFF]$/.test(kept)) kept = kept.slice(0, -1);
|
|
60
|
+
kept = kept.trimEnd();
|
|
61
|
+
// A cut inside a code block would put the note into the code: close the block first.
|
|
62
|
+
const fence = openCodeFence(kept);
|
|
63
|
+
const openFence = fence ? `\n${fence}` : '';
|
|
64
|
+
const planFilePath = String(context?.planFilePath || '').trim();
|
|
65
|
+
const where = planFilePath ? `; the whole plan is in \`${planFilePath}\`` : '';
|
|
66
|
+
return `${kept}${openFence}\n\n… cut here: ${text.length - kept.length} more characters${where}.`;
|
|
67
|
+
}
|
|
68
|
+
|
|
24
69
|
function normalizeBoardType(value) {
|
|
25
70
|
const text = String(value || '').trim().toLowerCase();
|
|
26
71
|
if (!text) return 'generic';
|
|
@@ -197,7 +242,7 @@ export function registerRelayBoardRoutes(app, deps) {
|
|
|
197
242
|
normalizedBoardType,
|
|
198
243
|
relayMode,
|
|
199
244
|
normalizeBoardText(title, 'Plan ready for review'),
|
|
200
|
-
|
|
245
|
+
normalizeBoardBody(body, boardContext),
|
|
201
246
|
normalizedActions.length ? JSON.stringify(normalizedActions) : null,
|
|
202
247
|
normalizedRecommended,
|
|
203
248
|
boardContext ? JSON.stringify(boardContext) : null,
|
|
@@ -44,7 +44,7 @@ export function registerRemoteRelayRoutes(app, deps) {
|
|
|
44
44
|
app.get('/api/remote-relays', auth, (_req, res) => {
|
|
45
45
|
res.json({
|
|
46
46
|
relays: registry.listPublic(),
|
|
47
|
-
self:
|
|
47
|
+
self: registry.selfPayload(),
|
|
48
48
|
});
|
|
49
49
|
});
|
|
50
50
|
|
|
@@ -2401,6 +2401,8 @@ function setPwaAppName(rawValue) {
|
|
|
2401
2401
|
} else {
|
|
2402
2402
|
stmts.deleteAppSetting.run(PWA_APP_NAME_SETTING_KEY);
|
|
2403
2403
|
}
|
|
2404
|
+
// Settings open on another device show the new name (Settings › Relays).
|
|
2405
|
+
try { remoteRelayRegistry.notifySelfChanged(); } catch {}
|
|
2404
2406
|
return { ok: true, appName: normalized.value };
|
|
2405
2407
|
}
|
|
2406
2408
|
|
|
@@ -4,6 +4,7 @@ import fs from 'fs';
|
|
|
4
4
|
import path from 'path';
|
|
5
5
|
import { fileURLToPath } from 'url';
|
|
6
6
|
|
|
7
|
+
import { renderMathInstructionBlock } from '../../shared/math-instructions.mjs';
|
|
7
8
|
import { renderMediaEmbedInstructionBlock } from '../../shared/media-embed-instructions.mjs';
|
|
8
9
|
|
|
9
10
|
const DEFAULT_RELAY_TOOL_GUIDANCE = [
|
|
@@ -148,6 +149,11 @@ const MEDIA_EMBED_BLOCK_PATTERN = new RegExp(
|
|
|
148
149
|
whitespaceFlexiblePattern(renderMediaEmbedInstructionBlock()),
|
|
149
150
|
'gi',
|
|
150
151
|
);
|
|
152
|
+
// The formula guidance (shared/math-instructions.mjs) rides right behind it.
|
|
153
|
+
const MATH_BLOCK_PATTERN = new RegExp(
|
|
154
|
+
whitespaceFlexiblePattern(renderMathInstructionBlock()),
|
|
155
|
+
'gi',
|
|
156
|
+
);
|
|
151
157
|
|
|
152
158
|
// A steered message's hidden note (shared/steer-note.mjs) leads the prompt the
|
|
153
159
|
// runtime stored, right after any mode marker. Matched by its stable opening
|
|
@@ -167,6 +173,7 @@ function stripLeadingRelayNotes(text) {
|
|
|
167
173
|
export function stripRelayPromptContext(text, relayMode = '', attachments = []) {
|
|
168
174
|
const value = stripAttachmentPromptArtifacts(text)
|
|
169
175
|
.replace(MEDIA_EMBED_BLOCK_PATTERN, '')
|
|
176
|
+
.replace(MATH_BLOCK_PATTERN, '')
|
|
170
177
|
.trim();
|
|
171
178
|
if (!value) return '';
|
|
172
179
|
const patterns = buildPromptPrefixPatterns(relayMode);
|
|
@@ -8,7 +8,7 @@
|
|
|
8
8
|
* The request waits until no turn is processing (relay-shutdown-gate.mjs);
|
|
9
9
|
* while it waits, no pending row is handed to a worker. A turn that runs for
|
|
10
10
|
* hours would hold every session's new messages for hours, so the wait has a
|
|
11
|
-
* maximum (Settings ›
|
|
11
|
+
* maximum (Settings › Relays). Past it the relay does not force anything:
|
|
12
12
|
* the request stays queued and turns into a question — restart now, keep
|
|
13
13
|
* waiting another period, or cancel — that the banner shows and a push
|
|
14
14
|
* notification announces once. Only the user, or an API caller that says
|
|
@@ -4127,6 +4127,24 @@ export function createRemoteRelayDispatcher({
|
|
|
4127
4127
|
return null;
|
|
4128
4128
|
}
|
|
4129
4129
|
|
|
4130
|
+
/**
|
|
4131
|
+
* Is `session` on `relay` the session whose agent created this conversation
|
|
4132
|
+
* (its origin, which an agent of this relay never sets for a paired one)?
|
|
4133
|
+
*/
|
|
4134
|
+
function sendsToCreator(conversationId, relay, session) {
|
|
4135
|
+
if (relay.local === true || !toText(relay.relayId) || !toText(session)) return false;
|
|
4136
|
+
let origin = null;
|
|
4137
|
+
try {
|
|
4138
|
+
origin = repository.getConversationOrigin?.(conversationId) || null;
|
|
4139
|
+
} catch {
|
|
4140
|
+
return false;
|
|
4141
|
+
}
|
|
4142
|
+
return !!origin && origin.local !== true
|
|
4143
|
+
&& toText(origin.relayId) === toText(relay.relayId)
|
|
4144
|
+
&& !!toText(origin.conversationId)
|
|
4145
|
+
&& toText(origin.conversationId) === toText(session);
|
|
4146
|
+
}
|
|
4147
|
+
|
|
4130
4148
|
async function runRelayAction({ conversationId, action, args, req, signal, waitNote }) {
|
|
4131
4149
|
const target = await resolveTarget(args.relay);
|
|
4132
4150
|
if (!target.relay) return target.failure;
|
|
@@ -4189,7 +4207,11 @@ export function createRemoteRelayDispatcher({
|
|
|
4189
4207
|
const origins = Array.isArray(caller.originRelayIds) ? caller.originRelayIds.map(toText) : [];
|
|
4190
4208
|
const answering = !!toText(relay.relayId) && origins.length > 0
|
|
4191
4209
|
&& origins.every((id) => id === toText(relay.relayId));
|
|
4192
|
-
|
|
4210
|
+
// A session another relay's agent created may always send to the session
|
|
4211
|
+
// that created it, whatever else its turn acts on (a prompt of a session
|
|
4212
|
+
// on this relay, say): its reports go back the way its work came.
|
|
4213
|
+
if (write && access !== 'user' && !answering && hops > REMOTE_RELAY_LIMITS.hopLimit
|
|
4214
|
+
&& !(action === 'send' && sendsToCreator(conversationId, relay, args.session))) {
|
|
4193
4215
|
return finish(failure(403, REMOTE_RELAY_ERROR_CODES.hopLimit,
|
|
4194
4216
|
`This turn acts on a prompt from another relay's agent (hop ${hops - 1}); passing it on to relay "${relay.name}" would pass the limit of ${REMOTE_RELAY_LIMITS.hopLimit} hops. The user can allow it by mentioning @${relay.name} in this conversation.`,
|
|
4195
4217
|
{ relay: relay.name }));
|
|
@@ -232,8 +232,10 @@ export function createRemoteRelayRegistry({
|
|
|
232
232
|
};
|
|
233
233
|
}
|
|
234
234
|
|
|
235
|
+
// For this relay's own Settings only, never for other relays: `defaultName`
|
|
236
|
+
// is the name it goes by while the relay name is empty.
|
|
235
237
|
function selfPayload() {
|
|
236
|
-
return { ...selfIdentity(), ...getSelfSettings() };
|
|
238
|
+
return { ...selfIdentity(), ...getSelfSettings(), defaultName: toText(hostname) || 'OAR' };
|
|
237
239
|
}
|
|
238
240
|
|
|
239
241
|
function emitChange() {
|
|
@@ -622,8 +624,12 @@ export function createRemoteRelayRegistry({
|
|
|
622
624
|
instanceId,
|
|
623
625
|
selfName,
|
|
624
626
|
selfIdentity,
|
|
627
|
+
selfPayload,
|
|
625
628
|
getSelfSettings,
|
|
626
629
|
setSelfSettings,
|
|
630
|
+
// The relay name lives outside the registry: its setter calls this so
|
|
631
|
+
// every open Settings sees the new name.
|
|
632
|
+
notifySelfChanged: emitChange,
|
|
627
633
|
getAgentSessionsSettings,
|
|
628
634
|
setAgentSessionsSettings,
|
|
629
635
|
list,
|
|
@@ -25,13 +25,24 @@ function toPidList(rows, extraPid) {
|
|
|
25
25
|
// read: the caller must not pretend it stopped what it could not see. The
|
|
26
26
|
// registry's pid counts only while that process is alive: a worker that is
|
|
27
27
|
// gone is stopped already (on Windows its stop call alone could fail).
|
|
28
|
+
//
|
|
29
|
+
// On POSIX the list is every process with its command line, so a live worker
|
|
30
|
+
// of the session is on it as long as its argv carries the session id within
|
|
31
|
+
// ps's width (procps-ng cuts `args` at 128 KiB even with -ww), and the
|
|
32
|
+
// registry's pid counts only when it is: a pid that is alive but not listed
|
|
33
|
+
// is no worker of this session. It is
|
|
34
|
+
// the old worker's pid handed to another process (another user's, which no
|
|
35
|
+
// signal reaches, or one of the relay user's own, which the stop would
|
|
36
|
+
// kill), or the worker ended and its parent has not reaped it yet (a zombie
|
|
37
|
+
// answers signal 0 and has no command line left).
|
|
28
38
|
async function enumeratePids({ sdkSessionId, worker, processInspector, platform, isPidAliveImpl }) {
|
|
29
39
|
const rows = platform === 'win32'
|
|
30
40
|
? await processInspector?.findWindowsProcessTreeForSessionAsync?.(sdkSessionId, { fresh: true })
|
|
31
41
|
: await processInspector?.findProcessesForSessionAsync?.(sdkSessionId, { fresh: true });
|
|
32
42
|
const listed = toPidList(rows || []);
|
|
33
43
|
const workerPid = Number(worker?.pid);
|
|
34
|
-
const workerAlive = Number.isInteger(workerPid) && workerPid > 0
|
|
44
|
+
const workerAlive = Number.isInteger(workerPid) && workerPid > 0
|
|
45
|
+
&& (listed.includes(workerPid) || (platform === 'win32' && isPidAliveImpl(workerPid)));
|
|
35
46
|
return workerAlive ? toPidList(rows || [], workerPid) : listed;
|
|
36
47
|
}
|
|
37
48
|
|
|
@@ -98,6 +109,36 @@ export async function stopSessionWorkerProcesses({
|
|
|
98
109
|
|
|
99
110
|
const survivors = (list) => list.filter((pid) => isPidAliveImpl(pid));
|
|
100
111
|
|
|
112
|
+
// POSIX: a signal no process takes (ESRCH) is ignored; one it is not ours
|
|
113
|
+
// to send (EPERM: another user's process) is remembered and the others
|
|
114
|
+
// still get theirs; the re-check below decides about that pid.
|
|
115
|
+
const refused = new Map();
|
|
116
|
+
const signal = (pid, name) => {
|
|
117
|
+
try {
|
|
118
|
+
killImpl(pid, name);
|
|
119
|
+
} catch (error) {
|
|
120
|
+
const code = String(error?.code || '');
|
|
121
|
+
if (code === 'EPERM') refused.set(pid, error);
|
|
122
|
+
else if (code !== 'ESRCH') throw error;
|
|
123
|
+
}
|
|
124
|
+
};
|
|
125
|
+
|
|
126
|
+
// POSIX: a pid that outlived the stop counts only while a fresh list still
|
|
127
|
+
// shows it as the session's. The list matches command lines, so a match
|
|
128
|
+
// can be a process that was ending anyway (a shell that carried the
|
|
129
|
+
// session's arguments, another user's one included), and a process that
|
|
130
|
+
// ended but is not reaped yet (a zombie) still answers signal 0 with no
|
|
131
|
+
// command line left. A list that cannot be read changes nothing.
|
|
132
|
+
const stillListed = async (list) => {
|
|
133
|
+
if (platform === 'win32' || !list.length) return list;
|
|
134
|
+
try {
|
|
135
|
+
const fresh = await enumeratePids({ sdkSessionId: sid, worker, processInspector, platform, isPidAliveImpl });
|
|
136
|
+
return list.filter((pid) => fresh.includes(pid) && isPidAliveImpl(pid));
|
|
137
|
+
} catch {
|
|
138
|
+
return list;
|
|
139
|
+
}
|
|
140
|
+
};
|
|
141
|
+
|
|
101
142
|
const waitFor = async (list, timeoutMs) => {
|
|
102
143
|
const deadline = nowMs() + Math.max(0, Number(timeoutMs) || 0);
|
|
103
144
|
let remaining = survivors(list);
|
|
@@ -114,13 +155,7 @@ export async function stopSessionWorkerProcesses({
|
|
|
114
155
|
// Stop-Process -Force, so there is no gentler first pass to attempt.
|
|
115
156
|
await stopWindowsPids(processInspector, pids);
|
|
116
157
|
} else {
|
|
117
|
-
for (const pid of pids)
|
|
118
|
-
try {
|
|
119
|
-
killImpl(pid, 'SIGTERM');
|
|
120
|
-
} catch (error) {
|
|
121
|
-
if (String(error?.code || '') !== 'ESRCH') throw error;
|
|
122
|
-
}
|
|
123
|
-
}
|
|
158
|
+
for (const pid of pids) signal(pid, 'SIGTERM');
|
|
124
159
|
// The tmux session can outlive its child after SIGTERM.
|
|
125
160
|
killTmuxSessionImpl?.(sid);
|
|
126
161
|
}
|
|
@@ -140,13 +175,7 @@ export async function stopSessionWorkerProcesses({
|
|
|
140
175
|
escalationTargets = [...new Set([...remainingPids, ...rediscovered.filter((pid) => isPidAliveImpl(pid))])];
|
|
141
176
|
await stopWindowsPids(processInspector, escalationTargets);
|
|
142
177
|
} else {
|
|
143
|
-
for (const pid of escalationTargets)
|
|
144
|
-
try {
|
|
145
|
-
killImpl(pid, 'SIGKILL');
|
|
146
|
-
} catch (error) {
|
|
147
|
-
if (String(error?.code || '') !== 'ESRCH') throw error;
|
|
148
|
-
}
|
|
149
|
-
}
|
|
178
|
+
for (const pid of escalationTargets) signal(pid, 'SIGKILL');
|
|
150
179
|
}
|
|
151
180
|
} catch (error) {
|
|
152
181
|
return done({
|
|
@@ -158,15 +187,18 @@ export async function stopSessionWorkerProcesses({
|
|
|
158
187
|
});
|
|
159
188
|
}
|
|
160
189
|
|
|
161
|
-
remainingPids = await waitFor(escalationTargets, escalationTimeoutMs);
|
|
190
|
+
remainingPids = await stillListed(await waitFor(escalationTargets, escalationTimeoutMs));
|
|
162
191
|
if (remainingPids.length) {
|
|
192
|
+
// A refused signal is the reason only when every survivor refused one;
|
|
193
|
+
// a survivor that took its signals and lives on is a timeout.
|
|
194
|
+
const refusal = remainingPids.every((pid) => refused.has(pid)) ? refused.get(remainingPids[0]) : null;
|
|
163
195
|
return done({
|
|
164
196
|
ok: false,
|
|
165
197
|
pids,
|
|
166
198
|
remainingPids,
|
|
167
199
|
escalated: true,
|
|
168
|
-
timedOut:
|
|
169
|
-
error: `worker-stop-timeout:${remainingPids.join(',')}`,
|
|
200
|
+
timedOut: !refusal,
|
|
201
|
+
error: refusal ? (refusal.message || 'Failed to stop the CLI') : `worker-stop-timeout:${remainingPids.join(',')}`,
|
|
170
202
|
});
|
|
171
203
|
}
|
|
172
204
|
return done({ pids, escalated: true });
|