@oar-sh/oar 0.9.12 → 0.9.14
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/.github/extensions/web-relay/extension.mjs +9 -0
- package/.github/extensions/web-relay/server-lifecycle/managed-server.mjs +15 -1
- package/.github/extensions/web-relay/skills/question-bridge.mjs +4 -1
- package/CHANGELOG.md +508 -0
- package/README.md +12 -12
- package/bin/oar.js +155 -94
- package/package.json +5 -5
- package/server/README.md +479 -157
- package/server/claude-cloud-worker/claude-cloud-session-process.mjs +10 -4
- package/server/claude-worker/claude-session-process.mjs +92 -15
- package/server/claude-worker/claude-turn-publisher.mjs +20 -1
- package/server/claude-worker/subagent-timeline-recorder.mjs +75 -8
- package/server/claude-worker/task-output-discovery.mjs +28 -2
- package/server/copilot-worker/copilot-ask-user-tool.mjs +1 -1
- package/server/copilot-worker/copilot-continuation-signals.mjs +11 -5
- package/server/copilot-worker/copilot-prompt-context.mjs +3 -1
- package/server/copilot-worker/copilot-question-bridge.mjs +11 -6
- package/server/copilot-worker/copilot-sdk-adapter.mjs +11 -0
- package/server/copilot-worker/copilot-sdk-session-process.mjs +24 -1
- package/server/cursor-worker/cursor-ask-user-tool.mjs +1 -1
- package/server/cursor-worker/cursor-sdk-adapter.mjs +22 -7
- package/server/cursor-worker/cursor-session-worker.mjs +9 -2
- package/server/cursor-worker/cursor-turn-runner.mjs +18 -3
- package/server/db-schema.mjs +10 -0
- package/server/grok-worker/acp-host-services.mjs +6 -3
- package/server/grok-worker/grok-background-tasks.mjs +7 -0
- package/server/grok-worker/grok-sdk-adapter.mjs +14 -0
- package/server/grok-worker/grok-turn-runner.mjs +13 -3
- package/server/migrations/0018-background-task-rows.mjs +101 -0
- package/server/public/app/action-confirmations.js +12 -15
- package/server/public/app/api-client.js +60 -4
- package/server/public/app/ask-user-view.js +29 -4
- package/server/public/app/background-tasks-view.mjs +56 -7
- package/server/public/app/bootstrap.js +7 -0
- package/server/public/app/conversation-preferences.mjs +10 -4
- package/server/public/app/conversation-provider-indicator.mjs +18 -8
- package/server/public/app/conversation-view.js +33 -9
- package/server/public/app/host-suspend-ui.js +45 -44
- package/server/public/app/journal-view.js +9 -3
- package/server/public/app/push-settings.js +1 -0
- package/server/public/app/question-answer-rule.mjs +15 -0
- package/server/public/app/question-multi-select.mjs +9 -2
- package/server/public/app/relay-event-view.mjs +5 -1
- package/server/public/app/relay-prompt-sanitizer.mjs +9 -2
- package/server/public/app/relay-restart-note-view.mjs +36 -0
- package/server/public/app/relay-restart-ui.js +568 -0
- package/server/public/app/settings-modal.js +52 -0
- package/server/public/app/socket-handlers.js +1 -1
- package/server/public/app/task-console-render.mjs +171 -7
- package/server/public/app/task-console.mjs +111 -11
- package/server/public/app/task-output-text.mjs +15 -6
- package/server/public/app/transcript-jump-bottom-view.mjs +5 -1
- package/server/public/app/transcript-jump-bottom.mjs +20 -0
- package/server/public/index.html +76 -5
- package/server/relay-exit-codes.mjs +6 -0
- package/server/relay-self-restart.mjs +6 -1
- package/server/relay-tools.md +17 -9
- package/server/repositories/message-repository.mjs +32 -28
- package/server/repositories/remote-relay-repository.mjs +4 -2
- package/server/repositories/subagent-timeline-statements.mjs +2 -0
- package/server/routes/ask-user-routes.mjs +41 -7
- package/server/routes/messages-routes.mjs +220 -37
- package/server/routes/preview-routes.mjs +73 -7
- package/server/routes/relay-board-routes.mjs +19 -2
- package/server/routes/relay-restart-routes.mjs +73 -0
- package/server/routes/sessions-routes.mjs +79 -5
- package/server/server-runtime.mjs +335 -218
- package/server/services/about-service.mjs +13 -12
- package/server/services/background-task-persistence.mjs +133 -0
- package/server/services/background-task-store.mjs +240 -13
- package/server/services/conversation-model-fallback.mjs +57 -0
- package/server/services/cursor-session-root-service.mjs +70 -6
- package/server/services/data-dir-resolution.mjs +208 -19
- package/server/services/earlier-database-service.mjs +631 -0
- package/server/services/host-suspend-command.mjs +2 -1
- package/server/services/oar-cli-helpers.mjs +6 -1
- package/server/services/oar-state-layout.mjs +107 -0
- package/server/services/oar-state-migration-service.mjs +387 -96
- package/server/services/package-data-rescue-preinstall.mjs +22 -0
- package/server/services/package-data-rescue.mjs +137 -0
- package/server/services/preview-proxy-server.mjs +14 -1
- package/server/services/preview-registry-service.mjs +3 -2
- package/server/services/process-list-service.mjs +2 -1
- package/server/services/provider-model-selection.mjs +195 -0
- package/server/services/push-dispatch-service.mjs +42 -8
- package/server/services/relay-event-service.mjs +210 -14
- package/server/services/relay-prompt-sanitizer.mjs +7 -0
- package/server/services/relay-restart-picture.mjs +209 -0
- package/server/services/relay-restart-queue.mjs +387 -0
- package/server/services/relay-shutdown-gate.mjs +4 -1
- package/server/services/relay-singleton-guard.mjs +300 -36
- package/server/services/remote-relay-caller-context.mjs +7 -2
- package/server/services/remote-relay-dispatcher.mjs +217 -28
- package/server/services/restart-resume-service.mjs +303 -0
- package/server/services/session-activity-tracker.mjs +52 -15
- package/server/services/session-worker-launch-service.mjs +31 -1
- package/server/services/session-worker-process-service.mjs +6 -4
- package/server/services/stale-turn-recovery.mjs +99 -0
- package/server/services/subagent-timeline-service.mjs +4 -1
- package/server/services/task-output-service.mjs +217 -18
- package/server/services/windows-autostart-service.mjs +329 -31
- package/server/services/windows-boot-autostart-service.mjs +188 -60
- package/shared/preview-instructions.mjs +17 -12
- package/shared/preview-tool-core.mjs +32 -14
- package/shared/question-answer-rule.mjs +63 -0
- package/shared/relay-events.mjs +106 -3
- package/shared/remote-relay-contract.mjs +32 -15
- package/shared/subagent-timeline.mjs +4 -0
- package/shared/task-output-text.mjs +196 -83
- package/shared/windows-system-tools.mjs +25 -0
- package/shared/worker-runtime/config-loader.mjs +54 -8
- package/shared/worker-runtime/prompt-context.mjs +3 -1
- package/shared/worker-runtime/worker-websocket-link.mjs +96 -20
- package/shared/worker-runtime/working-directory.mjs +65 -0
- package/server/uploads/image_input.md +0 -540
|
@@ -25,9 +25,12 @@ import {
|
|
|
25
25
|
withRemotePromptHeader,
|
|
26
26
|
} from '../../shared/remote-relay-contract.mjs';
|
|
27
27
|
import { schemaFields } from '../../shared/question-schema.mjs';
|
|
28
|
+
import { matchQuestionChoice } from '../../shared/question-answer-rule.mjs';
|
|
28
29
|
import { ANSWERED_ELSEWHERE_KIND } from '../../shared/steer-settle-markers.mjs';
|
|
29
|
-
import { RELAY_EVENT_DEFAULT_TYPES } from '../../shared/relay-events.mjs';
|
|
30
|
+
import { RELAY_EVENT_DEFAULT_TYPES, RELAY_EVENT_RELAY_LEVEL_TYPES } from '../../shared/relay-events.mjs';
|
|
31
|
+
import { checkRequestedProviderModel, isAutoModelId, providerModelLabel, unknownProviderModelText } from './provider-model-selection.mjs';
|
|
30
32
|
import { cleanTaskOutput, parseRunnerSummary, stallMinutes, stripSgr } from '../../shared/task-output-text.mjs';
|
|
33
|
+
import { TASK_OUTPUT_NONE_MESSAGES } from '../public/app/task-console-render.mjs';
|
|
31
34
|
import {
|
|
32
35
|
RELAY_EVENT_REMOTE_PULL_PATH,
|
|
33
36
|
RELAY_EVENT_REMOTE_SUBSCRIBE_PATH,
|
|
@@ -130,6 +133,8 @@ const FAILURE_TEXT_PATTERNS = Object.freeze([
|
|
|
130
133
|
/^relay timeout after \d+ attempts/i,
|
|
131
134
|
]);
|
|
132
135
|
const CLAUDE_LONG_CONTEXT_SUFFIX = /\[1m\]$/i;
|
|
136
|
+
// How many of a provider's model ids an unknown-model refusal carries.
|
|
137
|
+
const UNKNOWN_MODEL_LIST_MAX = 50;
|
|
133
138
|
// POST /api/conversation/bootstrap refuses an effort its model does not take
|
|
134
139
|
// with this code (400); POST /api/message sends the same 400 without a code,
|
|
135
140
|
// but with the list below.
|
|
@@ -269,6 +274,45 @@ function matchOfferedModel(candidate, offered = []) {
|
|
|
269
274
|
return offered.find((model) => model.toLowerCase().replace(CLAUDE_LONG_CONTEXT_SUFFIX, '') === wantedBase) || '';
|
|
270
275
|
}
|
|
271
276
|
|
|
277
|
+
/** The models /api/models lists for a provider label ("openai-byok"). */
|
|
278
|
+
function catalogModelsLabelled(catalog, label) {
|
|
279
|
+
const providersByModel = isPlainObject(catalog?.models?.providersByModel) ? catalog.models.providersByModel : {};
|
|
280
|
+
return Object.entries(providersByModel)
|
|
281
|
+
.filter(([, providers]) => Array.isArray(providers) && providers.some((entry) => toText(entry).toLowerCase() === label))
|
|
282
|
+
.map(([model]) => model);
|
|
283
|
+
}
|
|
284
|
+
|
|
285
|
+
/** Copilot's catalogue: every model of /api/models no other provider claims alone. */
|
|
286
|
+
function catalogGithubModels(catalog) {
|
|
287
|
+
const models = catalog?.models || {};
|
|
288
|
+
const providersByModel = isPlainObject(models.providersByModel) ? models.providersByModel : {};
|
|
289
|
+
const catalogModels = Array.isArray(models.models) ? models.models.map(toText).filter(Boolean) : [];
|
|
290
|
+
return catalogModels.filter((model) => {
|
|
291
|
+
const providers = catalogProvidersFor(providersByModel, model);
|
|
292
|
+
return providers.length === 0 || providers.includes(CATALOG_PROVIDER_LABELS.github);
|
|
293
|
+
});
|
|
294
|
+
}
|
|
295
|
+
|
|
296
|
+
/**
|
|
297
|
+
* The models a provider's New Chat list offers on the target, which its
|
|
298
|
+
* bootstrap and message routes accept, or null when the target has no list
|
|
299
|
+
* for it yet (discovery has not run or failed, the route did not answer):
|
|
300
|
+
* then a requested model cannot be checked here.
|
|
301
|
+
*/
|
|
302
|
+
function offeredModelList(catalog, provider) {
|
|
303
|
+
if (provider === 'github') {
|
|
304
|
+
const listed = catalogGithubModels(catalog);
|
|
305
|
+
return listed.length ? listed : null;
|
|
306
|
+
}
|
|
307
|
+
if (provider === 'openai') {
|
|
308
|
+
const listed = catalogModelsLabelled(catalog, CATALOG_PROVIDER_LABELS.openai);
|
|
309
|
+
return listed.length ? listed : null;
|
|
310
|
+
}
|
|
311
|
+
const settings = catalog?.settings?.[provider];
|
|
312
|
+
const listed = Array.isArray(settings?.models) ? settings.models.map(toText).filter(Boolean) : [];
|
|
313
|
+
return listed.length ? listed : null;
|
|
314
|
+
}
|
|
315
|
+
|
|
272
316
|
/** A reasoning effort as the relays write it, or '' when the value is not one. */
|
|
273
317
|
function normalizeEffort(value) {
|
|
274
318
|
const effort = toText(value).toLowerCase();
|
|
@@ -1016,19 +1060,9 @@ export function createRemoteRelayDispatcher({
|
|
|
1016
1060
|
|
|
1017
1061
|
function listProviders(catalog) {
|
|
1018
1062
|
const models = catalog.models || {};
|
|
1019
|
-
const providersByModel = isPlainObject(models.providersByModel) ? models.providersByModel : {};
|
|
1020
|
-
const catalogModels = Array.isArray(models.models) ? models.models.map(toText).filter(Boolean) : [];
|
|
1021
|
-
const modelsLabelled = (label) => Object.entries(providersByModel)
|
|
1022
|
-
.filter(([, providers]) => Array.isArray(providers) && providers.some((entry) => toText(entry).toLowerCase() === label))
|
|
1023
|
-
.map(([model]) => model);
|
|
1024
|
-
|
|
1025
1063
|
return REMOTE_RELAY_PROVIDERS.map((provider) => {
|
|
1026
1064
|
if (provider === 'github') {
|
|
1027
|
-
|
|
1028
|
-
const githubModels = catalogModels.filter((model) => {
|
|
1029
|
-
const providers = catalogProvidersFor(providersByModel, model);
|
|
1030
|
-
return providers.length === 0 || providers.includes(CATALOG_PROVIDER_LABELS.github);
|
|
1031
|
-
});
|
|
1065
|
+
const githubModels = catalogGithubModels(catalog);
|
|
1032
1066
|
const defaultModel = toText(catalog.status.defaultModel) || toText(models.defaultModel) || toText(models.currentModel) || null;
|
|
1033
1067
|
return {
|
|
1034
1068
|
provider,
|
|
@@ -1060,7 +1094,7 @@ export function createRemoteRelayDispatcher({
|
|
|
1060
1094
|
&& (provider === 'claude' || provider === 'grok' || settings.configured === true);
|
|
1061
1095
|
const defaultModel = toText(settings.model) || null;
|
|
1062
1096
|
const listed = provider === 'openai'
|
|
1063
|
-
?
|
|
1097
|
+
? catalogModelsLabelled(catalog, CATALOG_PROVIDER_LABELS.openai)
|
|
1064
1098
|
: (Array.isArray(settings.models) ? settings.models : []);
|
|
1065
1099
|
return {
|
|
1066
1100
|
provider,
|
|
@@ -1299,9 +1333,12 @@ export function createRemoteRelayDispatcher({
|
|
|
1299
1333
|
const health = isPlainObject(task.health) ? task.health : null;
|
|
1300
1334
|
const quietAfter = Number(row.taskStallMinutes) || null;
|
|
1301
1335
|
const minutes = live && health ? stallMinutes(health.lastOutputAt, now()) : null;
|
|
1336
|
+
// The command line comes scrubbed: the relay cleans it when the worker posts it.
|
|
1337
|
+
const command = toText(task.command);
|
|
1302
1338
|
return {
|
|
1303
1339
|
taskId,
|
|
1304
1340
|
output: kind !== 'none',
|
|
1341
|
+
...(command ? { command: clipLine(command, 500) } : {}),
|
|
1305
1342
|
...(health ? {
|
|
1306
1343
|
health: {
|
|
1307
1344
|
...(minutes !== null ? { minutesSinceOutput: minutes } : {}),
|
|
@@ -1595,7 +1632,7 @@ export function createRemoteRelayDispatcher({
|
|
|
1595
1632
|
}
|
|
1596
1633
|
const kind = toText(answer?.kind) || 'file';
|
|
1597
1634
|
let note = null;
|
|
1598
|
-
if (kind === 'none') note = toText(answer?.message) ||
|
|
1635
|
+
if (kind === 'none') note = toText(answer?.message) || TASK_OUTPUT_NONE_MESSAGES.provider;
|
|
1599
1636
|
else if (answer?.pending) note = 'The session\'s worker was asked for the output; call again in a few seconds.';
|
|
1600
1637
|
else if (answer?.unavailable) note = 'The output file is not there any more (the provider cleaned it up).';
|
|
1601
1638
|
else if (kind === 'snapshot') note = 'Only the end of this task\'s output is available (its provider keeps no file).';
|
|
@@ -1785,16 +1822,24 @@ export function createRemoteRelayDispatcher({
|
|
|
1785
1822
|
}
|
|
1786
1823
|
const runtime = isPlainObject(conversation?.runtimeSession) ? conversation.runtimeSession : {};
|
|
1787
1824
|
const provider = toText(runtime.providerType).toLowerCase() || 'github';
|
|
1825
|
+
// Checked before any approval card (runRelayAction) when the session
|
|
1826
|
+
// could be read there; here otherwise.
|
|
1827
|
+
const settled = args.model
|
|
1828
|
+
? (ctx.settledModel || await settleSendModel(ctx, conversation, args.model))
|
|
1829
|
+
: null;
|
|
1830
|
+
const requestedModel = settled?.model || '';
|
|
1831
|
+
const modelNote = settled?.note || null;
|
|
1788
1832
|
// Without a model the remote falls back to its catalogue's current one and
|
|
1789
1833
|
// silently switches the session; keep the model the session already uses.
|
|
1790
1834
|
// Grok pins its model at bootstrap and refuses any other, so it gets none.
|
|
1791
|
-
const model =
|
|
1835
|
+
const model = requestedModel
|
|
1792
1836
|
|| (provider === 'grok' ? '' : (toText(conversation?.preferredModel) || toText(runtime.providerModel) || toText(runtime.model)));
|
|
1793
1837
|
const mode = args.mode || ctx.caller.mode || toText(conversation?.preferredRelayMode) || '';
|
|
1794
1838
|
// The remote runs a prompt that names no effort with its provider's
|
|
1795
1839
|
// default, not with the one the session is set to: pass that one on, so
|
|
1796
1840
|
// the session keeps its own. This session's effort is never mirrored here.
|
|
1797
|
-
|
|
1841
|
+
// A Copilot model named with its effort ("gpt-5.4-high") brings that effort.
|
|
1842
|
+
const requested = normalizeEffort(args.effort) || normalizeEffort(settled?.effort);
|
|
1798
1843
|
const own = requested ? '' : normalizeEffort(conversation?.preferredReasoningEffort);
|
|
1799
1844
|
const origin = await buildOrigin(ctx);
|
|
1800
1845
|
const { sent, choice } = await postPromptWithEffort(ctx, { session, text: args.text, origin, model, mode }, {
|
|
@@ -1802,11 +1847,91 @@ export function createRemoteRelayDispatcher({
|
|
|
1802
1847
|
source: requested ? EFFORT_SOURCES.requested : (own ? EFFORT_SOURCES.remote : ''),
|
|
1803
1848
|
note: null,
|
|
1804
1849
|
});
|
|
1805
|
-
return deliverAndWait('send', ctx, {
|
|
1850
|
+
return deliverAndWait('send', ctx, {
|
|
1851
|
+
session,
|
|
1852
|
+
sent,
|
|
1853
|
+
waitSeconds: args.wait_seconds,
|
|
1854
|
+
extra: { ...(modelNote ? { modelNote } : {}), ...effortResult(choice) },
|
|
1855
|
+
});
|
|
1856
|
+
}
|
|
1857
|
+
|
|
1858
|
+
/**
|
|
1859
|
+
* A model the agent named, checked against the list New Chat offers for
|
|
1860
|
+
* `provider` on the target (offeredModelList). Throws a DispatchFailure
|
|
1861
|
+
* (unknownModel) for one it does not offer, before anything is created or
|
|
1862
|
+
* sent. Returns `{ model, note }`: the id to use (the provider's casing, a
|
|
1863
|
+
* Claude alias resolved) and what the agent should know about it, the
|
|
1864
|
+
* model left unchecked for want of a list included.
|
|
1865
|
+
*/
|
|
1866
|
+
function settleRequestedModel(ctx, catalog, provider, requested, { creating, extraModels = [] }) {
|
|
1867
|
+
const entry = catalog ? listProviders(catalog).find((candidate) => candidate.provider === provider) : null;
|
|
1868
|
+
const offered = catalog ? offeredModelList(catalog, provider) : null;
|
|
1869
|
+
const check = checkRequestedProviderModel({
|
|
1870
|
+
requestedModel: requested,
|
|
1871
|
+
provider,
|
|
1872
|
+
// What the message route accepts besides (send): the provider's whole
|
|
1873
|
+
// discovered list and the session's own model. Never a list by itself.
|
|
1874
|
+
models: offered ? [...offered, ...extraModels.map(toText).filter(Boolean)] : null,
|
|
1875
|
+
defaultModel: entry?.defaultModel || '',
|
|
1876
|
+
effortsFor: (base) => catalogEffortsFor(catalog?.models, provider, base),
|
|
1877
|
+
});
|
|
1878
|
+
const where = `on relay "${ctx.relay.name}"`;
|
|
1879
|
+
const label = providerModelLabel(provider);
|
|
1880
|
+
if (check.status === 'unknown') {
|
|
1881
|
+
throw new DispatchFailure(400, REMOTE_RELAY_ERROR_CODES.unknownModel,
|
|
1882
|
+
`${unknownProviderModelText({ model: requested, provider, suggestions: check.suggestions, models: check.models, where })} ${creating ? 'No session was created.' : 'Nothing was sent.'}`,
|
|
1883
|
+
{
|
|
1884
|
+
provider,
|
|
1885
|
+
model: requested,
|
|
1886
|
+
suggestions: check.suggestions,
|
|
1887
|
+
models: check.models.slice(0, UNKNOWN_MODEL_LIST_MAX),
|
|
1888
|
+
note: 'Pass one of these ids (relay_info lists them per provider), or leave model out.',
|
|
1889
|
+
});
|
|
1890
|
+
}
|
|
1891
|
+
if (check.status === 'unchecked') {
|
|
1892
|
+
return {
|
|
1893
|
+
model: requested,
|
|
1894
|
+
note: `Model '${requested}' could not be checked: ${label} ${where} has no model list yet (its model discovery has not run or failed), so ${creating ? 'the session was created' : 'the prompt was sent'} with it as asked.`,
|
|
1895
|
+
};
|
|
1896
|
+
}
|
|
1897
|
+
const tierNote = check.tierDropped ? ` ${label} ${where} lists no "[1m]" id for it, so it runs with the standard context.` : '';
|
|
1898
|
+
if (check.status === 'alias') {
|
|
1899
|
+
return { model: check.model, note: `'${requested}' was taken as ${check.model}, the newest ${label} model of that name ${where}.${tierNote}` };
|
|
1900
|
+
}
|
|
1901
|
+
// A Copilot model with its effort: the bootstrap and the message route
|
|
1902
|
+
// know the listed id, so the effort goes on apart.
|
|
1903
|
+
if (check.base) return { model: check.base, effort: check.effort, note: `'${requested}' was taken as ${check.base} with effort ${check.effort}.` };
|
|
1904
|
+
if (check.tierDropped) return { model: check.model, note: `'${requested}' was taken as ${check.model}.${tierNote}` };
|
|
1905
|
+
// The same id in the provider's casing needs no word; another one does.
|
|
1906
|
+
if (check.model.toLowerCase() !== requested.toLowerCase()) {
|
|
1907
|
+
return { model: check.model, note: `'${requested}' is listed as ${check.model} ${where}; that is the model used.` };
|
|
1908
|
+
}
|
|
1909
|
+
return { model: check.model, note: null };
|
|
1910
|
+
}
|
|
1911
|
+
|
|
1912
|
+
/** settleRequestedModel for a send to `conversation` (its detail payload); an unread catalogue leaves it unchecked. */
|
|
1913
|
+
async function settleSendModel(ctx, conversation, requested) {
|
|
1914
|
+
const runtime = isPlainObject(conversation?.runtimeSession) ? conversation.runtimeSession : {};
|
|
1915
|
+
const provider = toText(runtime.providerType).toLowerCase() || 'github';
|
|
1916
|
+
let catalog = null;
|
|
1917
|
+
try {
|
|
1918
|
+
catalog = await loadCatalog(ctx);
|
|
1919
|
+
} catch {
|
|
1920
|
+
// No catalogue: the model goes unchecked, and the note says so.
|
|
1921
|
+
}
|
|
1922
|
+
const settings = catalog?.settings?.[provider];
|
|
1923
|
+
return settleRequestedModel(ctx, catalog, provider, requested, {
|
|
1924
|
+
creating: false,
|
|
1925
|
+
extraModels: [
|
|
1926
|
+
...(Array.isArray(settings?.availableModels) ? settings.availableModels : []),
|
|
1927
|
+
runtime.providerModel,
|
|
1928
|
+
runtime.model,
|
|
1929
|
+
],
|
|
1930
|
+
});
|
|
1806
1931
|
}
|
|
1807
1932
|
|
|
1808
1933
|
async function createSession(ctx, args) {
|
|
1809
|
-
const catalog = await loadCatalog(ctx);
|
|
1934
|
+
const catalog = ctx.catalog || await loadCatalog(ctx);
|
|
1810
1935
|
const providers = buildProviders(catalog);
|
|
1811
1936
|
const configured = providers.filter((entry) => entry.configured).map((entry) => entry.provider);
|
|
1812
1937
|
const provider = createProvider(ctx.caller, args);
|
|
@@ -1821,6 +1946,16 @@ export function createRemoteRelayDispatcher({
|
|
|
1821
1946
|
let model = args.model || '';
|
|
1822
1947
|
let modelSource = model ? 'requested' : '';
|
|
1823
1948
|
let note = null;
|
|
1949
|
+
let variantEffort = '';
|
|
1950
|
+
if (model) {
|
|
1951
|
+
// Refused here when the provider does not offer it: a session created
|
|
1952
|
+
// with an id its provider never runs stays empty, and every send to it
|
|
1953
|
+
// fails.
|
|
1954
|
+
const settled = settleRequestedModel(ctx, catalog, provider, model, { creating: true });
|
|
1955
|
+
model = settled.model;
|
|
1956
|
+
note = settled.note;
|
|
1957
|
+
variantEffort = normalizeEffort(settled.effort);
|
|
1958
|
+
}
|
|
1824
1959
|
// A cloud session without a requested model gets the Claude Cloud tab's
|
|
1825
1960
|
// default, as New Chat does: the bootstrap picks it, nothing is mirrored.
|
|
1826
1961
|
if (!model && cloud) {
|
|
@@ -1842,7 +1977,7 @@ export function createRemoteRelayDispatcher({
|
|
|
1842
1977
|
|
|
1843
1978
|
// The effort mirrors the caller like the model does: this session's when
|
|
1844
1979
|
// the remote lists it for the model (or lists nothing), else the remote's.
|
|
1845
|
-
let choice = { effort: normalizeEffort(args.effort), source: EFFORT_SOURCES.requested, note: null };
|
|
1980
|
+
let choice = { effort: normalizeEffort(args.effort) || variantEffort, source: EFFORT_SOURCES.requested, note: null };
|
|
1846
1981
|
if (!choice.effort && cloud) {
|
|
1847
1982
|
// A cloud session takes no effort: none is mirrored, none is reported.
|
|
1848
1983
|
choice = { effort: '', source: '', note: null };
|
|
@@ -1898,6 +2033,11 @@ export function createRemoteRelayDispatcher({
|
|
|
1898
2033
|
`Relay "${ctx.relay.name}" did not return the new session's id.`);
|
|
1899
2034
|
}
|
|
1900
2035
|
const boundModel = toText(bootstrap?.selectedModel) || model;
|
|
2036
|
+
// A relay that swapped the asked model for another (one that predates
|
|
2037
|
+
// these checks does for Copilot) is named, never passed over in silence.
|
|
2038
|
+
if (modelSource === 'requested' && model && !isAutoModelId(model) && boundModel.toLowerCase() !== model.toLowerCase()) {
|
|
2039
|
+
note = [note, `Relay "${ctx.relay.name}" started the session on ${boundModel}, not on ${model} as asked.`].filter(Boolean).join(' ');
|
|
2040
|
+
}
|
|
1901
2041
|
let sent;
|
|
1902
2042
|
try {
|
|
1903
2043
|
const posted = await postPromptWithEffort(ctx, { session, text: args.text, origin, model: boundModel, mode }, choice);
|
|
@@ -1993,7 +2133,14 @@ export function createRemoteRelayDispatcher({
|
|
|
1993
2133
|
if (!labels.includes(label)) labels.push(label);
|
|
1994
2134
|
}
|
|
1995
2135
|
// The card's own format: the picked labels, then any typed text, ", "-joined.
|
|
1996
|
-
|
|
2136
|
+
// A card that takes no typed text gets an option or nothing.
|
|
2137
|
+
let extra = toText(args.answer);
|
|
2138
|
+
if (extra && question.allowFreeform === false) {
|
|
2139
|
+
extra = matchQuestionChoice(extra, options);
|
|
2140
|
+
if (!extra) {
|
|
2141
|
+
throw invalidInput(`This question takes only its options (${options.join(', ')}), no typed answer; pass one of them in choices.`);
|
|
2142
|
+
}
|
|
2143
|
+
}
|
|
1997
2144
|
if (extra && !labels.includes(extra)) labels.push(extra);
|
|
1998
2145
|
const text = labels.join(', ');
|
|
1999
2146
|
return { answer: text, text };
|
|
@@ -2193,7 +2340,7 @@ export function createRemoteRelayDispatcher({
|
|
|
2193
2340
|
if (!ctx.relay.local) return subscribeRemoteAction(ctx, args);
|
|
2194
2341
|
const service = eventService(ctx);
|
|
2195
2342
|
const all = args.scope === 'all';
|
|
2196
|
-
const ids = all ? [] : args.sessions;
|
|
2343
|
+
const ids = all ? [] : (args.sessions || []);
|
|
2197
2344
|
// Refused before anyone is asked: the conversation itself, unknown ids.
|
|
2198
2345
|
if (!all && ids.includes(ctx.conversationId)) {
|
|
2199
2346
|
throw new DispatchFailure(400, REMOTE_RELAY_ERROR_CODES.ownSession,
|
|
@@ -2210,11 +2357,15 @@ export function createRemoteRelayDispatcher({
|
|
|
2210
2357
|
titles.push({ id, title: toText(page.title) || null });
|
|
2211
2358
|
}
|
|
2212
2359
|
const events = Array.isArray(args.events) && args.events.length ? args.events : [...RELAY_EVENT_DEFAULT_TYPES];
|
|
2213
|
-
|
|
2214
|
-
|
|
2215
|
-
|
|
2216
|
-
|
|
2217
|
-
|
|
2360
|
+
// Only relay_restarted, no session: nothing of another conversation is
|
|
2361
|
+
// watched, so there is nothing to ask the user about.
|
|
2362
|
+
if (all || ids.length) {
|
|
2363
|
+
const decision = await ensureWatchApproval(ctx, {
|
|
2364
|
+
all,
|
|
2365
|
+
watch: { kind: 'subscribe', all, sessions: titles, events, once: args.once === true, heartbeatMinutes: args.heartbeat_minutes ?? null },
|
|
2366
|
+
});
|
|
2367
|
+
if (!decision.approved) throw new DispatchFailure(403, decision.code, decision.error);
|
|
2368
|
+
}
|
|
2218
2369
|
const subscription = relayEventCall(() => service.subscribe({
|
|
2219
2370
|
watcherConversationId: ctx.conversationId,
|
|
2220
2371
|
scope: all ? 'all' : 'sessions',
|
|
@@ -2225,8 +2376,9 @@ export function createRemoteRelayDispatcher({
|
|
|
2225
2376
|
stalledMinutes: args.stalled_minutes ?? null,
|
|
2226
2377
|
heartbeatMinutes: args.heartbeat_minutes ?? null,
|
|
2227
2378
|
}));
|
|
2379
|
+
const what = all ? 'all sessions' : (ids.length ? `${ids.length} session${ids.length === 1 ? '' : 's'}` : 'relay_restarted');
|
|
2228
2380
|
return {
|
|
2229
|
-
summary: `subscribe → ${ctx.relay.name}: ${
|
|
2381
|
+
summary: `subscribe → ${ctx.relay.name}: ${what} (${subscription.id})`,
|
|
2230
2382
|
subscription,
|
|
2231
2383
|
note: EVENT_NOTE,
|
|
2232
2384
|
};
|
|
@@ -2239,6 +2391,14 @@ export function createRemoteRelayDispatcher({
|
|
|
2239
2391
|
* mirror; the link fetches the events from then on.
|
|
2240
2392
|
*/
|
|
2241
2393
|
async function subscribeRemoteAction(ctx, args) {
|
|
2394
|
+
const relayLevel = (args.events || []).filter((type) => RELAY_EVENT_RELAY_LEVEL_TYPES.includes(type));
|
|
2395
|
+
if (relayLevel.length) {
|
|
2396
|
+
throw new DispatchFailure(400, REMOTE_RELAY_ERROR_CODES.invalidInput,
|
|
2397
|
+
`${relayLevel.join(', ')} is about the relay you run on: subscribe to it on "${REMOTE_RELAY_LOCAL_ALIAS}". An outage of relay "${ctx.relay.name}" shows as link_lost and link_restored.`);
|
|
2398
|
+
}
|
|
2399
|
+
if (!args.sessions?.length && args.scope !== 'all') {
|
|
2400
|
+
throw new DispatchFailure(400, REMOTE_RELAY_ERROR_CODES.invalidInput, 'subscribe on a paired relay needs sessions (ids from list_sessions) or ["all"].');
|
|
2401
|
+
}
|
|
2242
2402
|
const service = eventService(ctx);
|
|
2243
2403
|
const all = args.scope === 'all';
|
|
2244
2404
|
relayEventCall(() => service.checkWatcherCapacity(ctx.conversationId));
|
|
@@ -2679,10 +2839,39 @@ export function createRemoteRelayDispatcher({
|
|
|
2679
2839
|
{ relay: relay.name, retryAfterSeconds }));
|
|
2680
2840
|
}
|
|
2681
2841
|
|
|
2842
|
+
// The target's catalogue, loaded before anyone is asked for approval when
|
|
2843
|
+
// the agent names a model: a card for a model that does not exist there
|
|
2844
|
+
// would be a question about nothing. createSession reuses it.
|
|
2845
|
+
let catalog = null;
|
|
2846
|
+
let settledModel = null;
|
|
2682
2847
|
if (action === 'create_session') {
|
|
2683
2848
|
const problem = createSessionProblem(caller, args);
|
|
2684
2849
|
if (problem) return finish(failure(400, REMOTE_RELAY_ERROR_CODES.invalidInput, problem, { relay: relay.name }));
|
|
2685
2850
|
}
|
|
2851
|
+
if ((action === 'create_session' || action === 'send') && args.model) {
|
|
2852
|
+
const early = { relay, caller, hops, conversationId, signal };
|
|
2853
|
+
try {
|
|
2854
|
+
if (action === 'create_session') {
|
|
2855
|
+
catalog = await loadCatalog(early);
|
|
2856
|
+
const provider = createProvider(caller, args);
|
|
2857
|
+
// A provider not set up there answers as such (createSession), not
|
|
2858
|
+
// with a model it has no list for.
|
|
2859
|
+
if (listProviders(catalog).find((entry) => entry.provider === provider)?.configured) {
|
|
2860
|
+
settleRequestedModel(early, catalog, provider, args.model, { creating: true });
|
|
2861
|
+
}
|
|
2862
|
+
} else {
|
|
2863
|
+
const conversation = await call(early, 'GET', sessionPath(args.session), { query: { limit: 1 } });
|
|
2864
|
+
settledModel = await settleSendModel(early, conversation, args.model);
|
|
2865
|
+
}
|
|
2866
|
+
} catch (error) {
|
|
2867
|
+
if (error instanceof DispatchFailure) {
|
|
2868
|
+
return finish(failure(error.status, error.code, error.message, { relay: relay.name, ...error.extra }));
|
|
2869
|
+
}
|
|
2870
|
+
// The target did not answer: the call itself reports that.
|
|
2871
|
+
catalog = null;
|
|
2872
|
+
settledModel = null;
|
|
2873
|
+
}
|
|
2874
|
+
}
|
|
2686
2875
|
|
|
2687
2876
|
const localCreate = local && action === 'create_session';
|
|
2688
2877
|
let holdsLocalCreate = false;
|
|
@@ -2756,7 +2945,7 @@ export function createRemoteRelayDispatcher({
|
|
|
2756
2945
|
}
|
|
2757
2946
|
}
|
|
2758
2947
|
|
|
2759
|
-
const ctx = { relay, caller, hops, conversationId, signal, onPromptQueued: releaseLocalCreate };
|
|
2948
|
+
const ctx = { relay, caller, hops, conversationId, signal, onPromptQueued: releaseLocalCreate, ...(catalog ? { catalog } : {}), ...(settledModel ? { settledModel } : {}) };
|
|
2760
2949
|
const body = await ACTIONS[action](ctx, args);
|
|
2761
2950
|
if (prompting) agentPrompts(conversationId).count += 1;
|
|
2762
2951
|
const { summary, ...rest } = body;
|