@oar-sh/oar 0.9.3 → 0.9.4
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 +6 -1
- package/.github/extensions/web-relay/skills/tool-activity.mjs +7 -0
- package/CHANGELOG.md +67 -0
- package/README.md +62 -13
- package/package.json +5 -2
- package/server/README.md +43 -3
- package/server/claude-worker/claude-remote-relay-tool.mjs +99 -0
- package/server/claude-worker/claude-sdk-adapter.mjs +26 -4
- package/server/claude-worker/claude-session-process.mjs +92 -25
- package/server/claude-worker/sdk-message-normalizer.mjs +7 -0
- package/server/copilot-worker/copilot-remote-relay-tool.mjs +59 -0
- package/server/copilot-worker/copilot-sdk-event-normalizer.mjs +12 -2
- package/server/copilot-worker/copilot-sdk-session-process.mjs +83 -9
- package/server/cursor-worker/cursor-remote-relay-tool.mjs +65 -0
- package/server/cursor-worker/cursor-turn-runner.mjs +45 -2
- package/server/cursor-worker/sdk-message-normalizer.mjs +25 -0
- package/server/db-schema.mjs +8 -0
- package/server/grok-worker/acp-client.mjs +92 -6
- package/server/grok-worker/grok-turn-runner.mjs +59 -8
- package/server/grok-worker/sdk-message-normalizer.mjs +13 -0
- package/server/mcp/oar-mcp-server.mjs +473 -0
- package/server/migrations/0005-remote-relays.mjs +90 -0
- package/server/public/app/action-confirmations.js +141 -13
- package/server/public/app/api-client.js +103 -0
- package/server/public/app/bootstrap.js +21 -6
- package/server/public/app/conversation-deep-link.mjs +42 -0
- package/server/public/app/conversation-view.js +98 -7
- package/server/public/app/host-suspend-ui.js +272 -0
- package/server/public/app/journal-view.js +4 -1
- package/server/public/app/mention-autocomplete.mjs +298 -0
- package/server/public/app/push-settings.js +1 -0
- package/server/public/app/pwa-install.js +8 -3
- package/server/public/app/relay-prompt-sanitizer.mjs +48 -5
- package/server/public/app/remote-relay-origin-view.mjs +76 -0
- package/server/public/app/remote-relay-shared.mjs +85 -0
- package/server/public/app/remote-relays-settings.js +618 -0
- package/server/public/app/remote-relays-store.mjs +126 -0
- package/server/public/app/settings-modal.js +4 -2
- package/server/public/app/settings-tabs.js +1 -1
- package/server/public/app/socket-handlers.js +18 -1
- package/server/public/app/thinking-follow.js +260 -0
- package/server/public/app/tool-activity-emoji.mjs +4 -0
- package/server/public/index.html +257 -8
- package/server/relay-tools.md +5 -1
- package/server/repositories/message-repository.mjs +7 -0
- package/server/repositories/question-repository.mjs +3 -0
- package/server/repositories/remote-relay-repository.mjs +75 -0
- package/server/repositories/session-repository.mjs +6 -1
- package/server/routes/ask-user-routes.mjs +42 -2
- package/server/routes/messages-routes.mjs +90 -16
- package/server/routes/remote-relay-routes.mjs +133 -0
- package/server/routes/sessions-routes.mjs +80 -29
- package/server/server-runtime.mjs +238 -0
- package/server/services/github-ci-activity-service.mjs +199 -0
- package/server/services/host-suspend-command.mjs +38 -0
- package/server/services/host-suspend-service.mjs +254 -0
- package/server/services/push-dispatch-service.mjs +37 -4
- package/server/services/remote-relay-caller-context.mjs +341 -0
- package/server/services/remote-relay-client.mjs +332 -0
- package/server/services/remote-relay-dispatcher.mjs +1500 -0
- package/server/services/remote-relay-inbound.mjs +187 -0
- package/server/services/remote-relay-pairing.mjs +189 -0
- package/server/services/remote-relay-registry-service.mjs +581 -0
- package/server/services/session-worker-launch-service.mjs +131 -12
- package/shared/remote-relay-contract.mjs +539 -0
- package/shared/remote-relay-mentions.mjs +129 -0
- package/shared/remote-relay-tool-core.mjs +314 -0
- package/shared/worker-runtime/api-client.mjs +112 -22
|
@@ -1,6 +1,11 @@
|
|
|
1
1
|
import { sanitizeSubagentRunId } from '../../shared/subagent-run-id.mjs';
|
|
2
2
|
import { capThought } from '../../shared/thought-cap.mjs';
|
|
3
3
|
import { shouldEmitStreamUpdate } from '../../shared/stream-emit-gating.mjs';
|
|
4
|
+
import {
|
|
5
|
+
REMOTE_RELAY_TOOL_NAME,
|
|
6
|
+
isRemoteRelayToolName,
|
|
7
|
+
remoteRelayActivitySummary,
|
|
8
|
+
} from '../../shared/remote-relay-tool-core.mjs';
|
|
4
9
|
|
|
5
10
|
const MAX_TOOL_DETAIL_LENGTH = 140;
|
|
6
11
|
const SUBAGENT_TOOL_NAMES = new Set(['task', 'agent']);
|
|
@@ -39,7 +44,27 @@ export function summarizeToolInput(toolName, input) {
|
|
|
39
44
|
}
|
|
40
45
|
}
|
|
41
46
|
|
|
47
|
+
/**
|
|
48
|
+
* The relay's custom tools reach the stream as the SDK's generic MCP call
|
|
49
|
+
* (`name: 'mcp'`, args `{ providerIdentifier: 'custom-user-tools', toolName,
|
|
50
|
+
* args }`). `remote_relay` is shown as itself with the contract's one-line
|
|
51
|
+
* summary, so the transcript reads `Tool (remote_relay): send → …` like every
|
|
52
|
+
* other provider's; null for any other tool.
|
|
53
|
+
*/
|
|
54
|
+
function remoteRelayCallArgs(toolName, input) {
|
|
55
|
+
if (isRemoteRelayToolName(toolName)) return input || {};
|
|
56
|
+
if (String(toolName || '').trim().toLowerCase() !== 'mcp' || !input || typeof input !== 'object') return null;
|
|
57
|
+
return isRemoteRelayToolName(input.toolName) ? (input.args || {}) : null;
|
|
58
|
+
}
|
|
59
|
+
|
|
42
60
|
export function formatToolActivityText(toolName, input) {
|
|
61
|
+
const remoteArgs = remoteRelayCallArgs(toolName, input);
|
|
62
|
+
if (remoteArgs) {
|
|
63
|
+
const remoteSummary = remoteRelayActivitySummary(remoteArgs);
|
|
64
|
+
return truncate(remoteSummary
|
|
65
|
+
? `Tool (${REMOTE_RELAY_TOOL_NAME}): ${remoteSummary}`
|
|
66
|
+
: `Tool (${REMOTE_RELAY_TOOL_NAME})`);
|
|
67
|
+
}
|
|
43
68
|
const summary = summarizeToolInput(toolName, input);
|
|
44
69
|
return truncate(summary ? `Tool (${toolName}): ${summary}` : `Tool (${toolName})`);
|
|
45
70
|
}
|
package/server/db-schema.mjs
CHANGED
|
@@ -10,6 +10,7 @@
|
|
|
10
10
|
import { rebuildRecentWorkspaceRootsTable } from './migrations/0002-recent-workspace-roots-path-key.mjs';
|
|
11
11
|
import { ensurePushSubscriptionsTable } from './migrations/0003-push-subscriptions.mjs';
|
|
12
12
|
import { migrateSteerSettleMarkers } from './migrations/0004-steer-settle-markers.mjs';
|
|
13
|
+
import { migrateRemoteRelays } from './migrations/0005-remote-relays.mjs';
|
|
13
14
|
import { migrateImageConversationSchema } from './repositories/image-conversation-repository.mjs';
|
|
14
15
|
|
|
15
16
|
// Mirrors DEFAULT_RELAY_MODE in server-runtime.mjs; used only by the one-time
|
|
@@ -702,6 +703,13 @@ try {
|
|
|
702
703
|
} catch (error) {
|
|
703
704
|
console.warn(`[steering] migration 0004 failed; retrying on next boot: ${error?.message || error}`);
|
|
704
705
|
}
|
|
706
|
+
// Remote relays: additive columns and the unlock table. Readers tolerate their
|
|
707
|
+
// absence, so a failure is logged and retried next boot like 0004.
|
|
708
|
+
try {
|
|
709
|
+
migrateRemoteRelays(db);
|
|
710
|
+
} catch (error) {
|
|
711
|
+
console.warn(`[remote-relays] migration 0005 failed; retrying on next boot: ${error?.message || error}`);
|
|
712
|
+
}
|
|
705
713
|
|
|
706
714
|
const runtimeSessionColumns = db.prepare(`PRAGMA table_info(runtime_sessions)`).all().map((c) => c.name);
|
|
707
715
|
if (runtimeSessionColumns.length) {
|
|
@@ -17,6 +17,40 @@ function whichCommand(command = 'grok') {
|
|
|
17
17
|
// ceiling bounds a single prompt absolutely. Both are overridable per call.
|
|
18
18
|
export const ACP_PROMPT_INACTIVITY_MS = 120_000;
|
|
19
19
|
export const ACP_PROMPT_MAX_TURN_MS = 1_800_000;
|
|
20
|
+
// How long the watchdog waits for its `holdStall` question before treating
|
|
21
|
+
// the silence as a stall after all.
|
|
22
|
+
export const ACP_HOLD_CHECK_TIMEOUT_MS = 10_000;
|
|
23
|
+
|
|
24
|
+
/**
|
|
25
|
+
* An MCP server launch (`{ command, args, env }`, see
|
|
26
|
+
* server/mcp/oar-mcp-server.mjs `buildOarMcpServerLaunch`) in the ACP stdio
|
|
27
|
+
* shape `session/new` and `session/load` take: no `type` field (that marks the
|
|
28
|
+
* http/sse transports), `args` and `env` always present, `env` as a list of
|
|
29
|
+
* `{ name, value }` pairs.
|
|
30
|
+
*/
|
|
31
|
+
export function toAcpStdioMcpServer(launch, name = 'oar') {
|
|
32
|
+
const env = launch?.env && typeof launch.env === 'object' ? launch.env : {};
|
|
33
|
+
return {
|
|
34
|
+
name: String(name || '').trim() || 'oar',
|
|
35
|
+
command: String(launch?.command || ''),
|
|
36
|
+
args: Array.isArray(launch?.args) ? launch.args.map((arg) => String(arg)) : [],
|
|
37
|
+
env: Object.entries(env).map(([key, value]) => ({ name: key, value: String(value ?? '') })),
|
|
38
|
+
};
|
|
39
|
+
}
|
|
40
|
+
|
|
41
|
+
/**
|
|
42
|
+
* An AcpClient class whose sessions carry `mcpServers` unless the caller
|
|
43
|
+
* passes its own. Lets the turn runner hand the adapter (which builds the
|
|
44
|
+
* client and opens the session in one step) a client that already knows them.
|
|
45
|
+
*/
|
|
46
|
+
export function withAcpMcpServers(BaseClient, mcpServers = []) {
|
|
47
|
+
const servers = Array.isArray(mcpServers) ? mcpServers : [];
|
|
48
|
+
return class AcpClientWithMcpServers extends BaseClient {
|
|
49
|
+
constructor(opts = {}) {
|
|
50
|
+
super({ ...opts, mcpServers: Array.isArray(opts?.mcpServers) ? opts.mcpServers : servers });
|
|
51
|
+
}
|
|
52
|
+
};
|
|
53
|
+
}
|
|
20
54
|
|
|
21
55
|
/**
|
|
22
56
|
* Pick the one-shot allow option from an ACP permission request. A blanket
|
|
@@ -47,10 +81,13 @@ export class AcpClient extends EventEmitter {
|
|
|
47
81
|
* @param {string} [opts.cwd]
|
|
48
82
|
* @param {NodeJS.ProcessEnv} [opts.env]
|
|
49
83
|
* @param {boolean} [opts.alwaysApprove]
|
|
84
|
+
* @param {Array<object>} [opts.mcpServers] ACP McpServer entries every
|
|
85
|
+
* session/new and session/load carries (see toAcpStdioMcpServer)
|
|
50
86
|
* @param {typeof spawn} [opts.spawnImpl]
|
|
51
87
|
*/
|
|
52
88
|
constructor(opts = {}) {
|
|
53
89
|
super();
|
|
90
|
+
this.mcpServers = Array.isArray(opts.mcpServers) ? opts.mcpServers.map((server) => ({ ...server })) : [];
|
|
54
91
|
this.command = whichCommand(opts.command || 'grok');
|
|
55
92
|
this.args = Array.isArray(opts.args) ? [...opts.args] : ['agent', '--no-leader', 'stdio'];
|
|
56
93
|
if (opts.alwaysApprove && !this.args.includes('--always-approve') && !this.args.includes('--yolo')) {
|
|
@@ -276,10 +313,12 @@ export class AcpClient extends EventEmitter {
|
|
|
276
313
|
return this.initializeResult;
|
|
277
314
|
}
|
|
278
315
|
|
|
316
|
+
// MCP servers are per session in ACP, and a loaded session does not keep the
|
|
317
|
+
// ones it was created with — both calls pass them.
|
|
279
318
|
async sessionNew(cwd, extra = {}) {
|
|
280
319
|
return this.request('session/new', {
|
|
281
320
|
cwd,
|
|
282
|
-
mcpServers:
|
|
321
|
+
mcpServers: this.mcpServers,
|
|
283
322
|
...extra,
|
|
284
323
|
});
|
|
285
324
|
}
|
|
@@ -288,7 +327,7 @@ export class AcpClient extends EventEmitter {
|
|
|
288
327
|
return this.request('session/load', {
|
|
289
328
|
sessionId,
|
|
290
329
|
cwd,
|
|
291
|
-
mcpServers:
|
|
330
|
+
mcpServers: this.mcpServers,
|
|
292
331
|
...extra,
|
|
293
332
|
});
|
|
294
333
|
}
|
|
@@ -305,8 +344,15 @@ export class AcpClient extends EventEmitter {
|
|
|
305
344
|
* @param {string} sessionId
|
|
306
345
|
* @param {Array} prompt content blocks
|
|
307
346
|
* @param {(update: object) => void} [onUpdate]
|
|
347
|
+
* `holdStall` (async, optional) is asked once the inactivity window has run
|
|
348
|
+
* out: `true` re-arms the full window instead of failing the prompt. It is
|
|
349
|
+
* for work the agent is blocked on that this client cannot see — a
|
|
350
|
+
* remote_relay call the agent runs through its MCP server, which the relay
|
|
351
|
+
* reports as in flight. A question that fails or takes longer than
|
|
352
|
+
* `holdCheckTimeoutMs` counts as `false`.
|
|
353
|
+
*
|
|
308
354
|
* @param {object} [extra] additional session/prompt params (e.g. `_meta`)
|
|
309
|
-
* @param {object} [watchdog] { inactivityMs, maxTurnMs, hasPendingWork }
|
|
355
|
+
* @param {object} [watchdog] { inactivityMs, maxTurnMs, hasPendingWork, holdStall, holdCheckTimeoutMs }
|
|
310
356
|
*/
|
|
311
357
|
async sessionPrompt(sessionId, prompt, onUpdate, extra = {}, watchdog = {}) {
|
|
312
358
|
const inactivityMs = Number(watchdog?.inactivityMs) > 0
|
|
@@ -322,6 +368,10 @@ export class AcpClient extends EventEmitter {
|
|
|
322
368
|
const hasPendingWork = typeof watchdog?.hasPendingWork === 'function'
|
|
323
369
|
? watchdog.hasPendingWork
|
|
324
370
|
: () => false;
|
|
371
|
+
const holdStall = typeof watchdog?.holdStall === 'function' ? watchdog.holdStall : null;
|
|
372
|
+
const holdCheckTimeoutMs = Number(watchdog?.holdCheckTimeoutMs) > 0
|
|
373
|
+
? Number(watchdog.holdCheckTimeoutMs)
|
|
374
|
+
: ACP_HOLD_CHECK_TIMEOUT_MS;
|
|
325
375
|
const handler = (msg) => {
|
|
326
376
|
if (msg.method !== 'session/update') return;
|
|
327
377
|
if (msg.params?.sessionId && msg.params.sessionId !== sessionId) return;
|
|
@@ -329,8 +379,40 @@ export class AcpClient extends EventEmitter {
|
|
|
329
379
|
};
|
|
330
380
|
this.on('notification', handler);
|
|
331
381
|
let watchdogTimer = null;
|
|
382
|
+
let settled = false;
|
|
383
|
+
// A granted hold restarts the window without faking ACP traffic.
|
|
384
|
+
let heldAt = 0;
|
|
385
|
+
let holdCheck = null;
|
|
386
|
+
const quietSince = () => Math.max(this.lastActivityAt, heldAt);
|
|
332
387
|
const startedAt = Date.now();
|
|
333
388
|
const stallPromise = new Promise((_, reject) => {
|
|
389
|
+
const stall = () => {
|
|
390
|
+
if (settled) return;
|
|
391
|
+
try { this.sessionCancel(sessionId); } catch { /* ignore */ }
|
|
392
|
+
reject(new Error(`grok turn stalled: no ACP activity for ${Math.round(inactivityMs / 1000)}s`));
|
|
393
|
+
};
|
|
394
|
+
const askForHold = () => {
|
|
395
|
+
let timer = null;
|
|
396
|
+
holdCheck = Promise.race([
|
|
397
|
+
Promise.resolve().then(() => holdStall()),
|
|
398
|
+
new Promise((resolve) => {
|
|
399
|
+
timer = setTimeout(() => resolve(false), holdCheckTimeoutMs);
|
|
400
|
+
timer.unref?.();
|
|
401
|
+
}),
|
|
402
|
+
])
|
|
403
|
+
.then((hold) => hold === true, () => false)
|
|
404
|
+
.then((hold) => {
|
|
405
|
+
if (timer) clearTimeout(timer);
|
|
406
|
+
holdCheck = null;
|
|
407
|
+
if (settled) return;
|
|
408
|
+
if (hold) {
|
|
409
|
+
heldAt = Date.now();
|
|
410
|
+
return;
|
|
411
|
+
}
|
|
412
|
+
// Traffic that arrived while the question ran re-armed it anyway.
|
|
413
|
+
if (Date.now() - quietSince() >= inactivityMs) stall();
|
|
414
|
+
});
|
|
415
|
+
};
|
|
334
416
|
const pollMs = Math.max(250, Math.min(5000, Math.floor(inactivityMs / 4)));
|
|
335
417
|
watchdogTimer = setInterval(() => {
|
|
336
418
|
const now = Date.now();
|
|
@@ -340,10 +422,13 @@ export class AcpClient extends EventEmitter {
|
|
|
340
422
|
return;
|
|
341
423
|
}
|
|
342
424
|
if (hasPendingWork()) return;
|
|
343
|
-
if (now -
|
|
344
|
-
|
|
345
|
-
|
|
425
|
+
if (now - quietSince() < inactivityMs) return;
|
|
426
|
+
if (!holdStall) {
|
|
427
|
+
stall();
|
|
428
|
+
return;
|
|
346
429
|
}
|
|
430
|
+
// Before the abort: is the agent blocked on work only the relay sees?
|
|
431
|
+
if (!holdCheck) askForHold();
|
|
347
432
|
}, pollMs);
|
|
348
433
|
if (typeof watchdogTimer.unref === 'function') watchdogTimer.unref();
|
|
349
434
|
});
|
|
@@ -360,6 +445,7 @@ export class AcpClient extends EventEmitter {
|
|
|
360
445
|
stallPromise,
|
|
361
446
|
]);
|
|
362
447
|
} finally {
|
|
448
|
+
settled = true;
|
|
363
449
|
clearInterval(watchdogTimer);
|
|
364
450
|
this.off('notification', handler);
|
|
365
451
|
}
|
|
@@ -3,9 +3,12 @@ import {
|
|
|
3
3
|
startGrokTurn,
|
|
4
4
|
classifyGrokError,
|
|
5
5
|
} from './grok-sdk-adapter.mjs';
|
|
6
|
+
import { AcpClient, toAcpStdioMcpServer, withAcpMcpServers } from './acp-client.mjs';
|
|
6
7
|
import { buildGrokContextUsage, resolveGrokContextWindow } from './grok-context-usage.mjs';
|
|
7
8
|
import { extractGrokUsageFromPromptResult, normalizeGrokTurnUsage } from '../services/plan-usage-grok.mjs';
|
|
9
|
+
import { OAR_MCP_SERVER_NAME, buildOarMcpServerLaunch, isOarMcpServerDisabled } from '../mcp/oar-mcp-server.mjs';
|
|
8
10
|
import { createPreviewInstructionsProvider } from '../../shared/preview-instructions.mjs';
|
|
11
|
+
import { fetchRemoteRelayInflight } from '../../shared/remote-relay-tool-core.mjs';
|
|
9
12
|
import { renderMediaEmbedInstructionBlock } from '../../shared/media-embed-instructions.mjs';
|
|
10
13
|
import { countPlanLikeLines } from '../../shared/plan-lines.mjs';
|
|
11
14
|
import { EMPTY_TURN_COMPLETION_NOTE } from '../../shared/empty-turn-completion.mjs';
|
|
@@ -110,8 +113,30 @@ export function createGrokTurnRunner({
|
|
|
110
113
|
startGrokTurnImpl = startGrokTurn,
|
|
111
114
|
classifyErrorImpl = classifyGrokError,
|
|
112
115
|
getPreviewInstructions = createPreviewInstructionsProvider({ api }),
|
|
116
|
+
env = process.env,
|
|
117
|
+
nodePath = process.execPath,
|
|
118
|
+
// The OAR MCP server launch (`{ command, args, env }`) every Grok session
|
|
119
|
+
// gets through ACP `mcpServers`; undefined builds the default for this
|
|
120
|
+
// worker's conversation, null leaves Grok without it.
|
|
121
|
+
oarMcpServer = undefined,
|
|
113
122
|
dbg = () => {},
|
|
114
123
|
} = {}) {
|
|
124
|
+
// The OAR MCP server gives Grok the relay's real tools — `preview`, and
|
|
125
|
+
// `remote_relay` when remotes are paired — bound to this conversation.
|
|
126
|
+
// `OAR_MCP_SERVER=0` is the operator's way back to the preview instruction
|
|
127
|
+
// block, should an agent build turn out to ignore ACP `mcpServers`.
|
|
128
|
+
const oarMcpLaunch = oarMcpServer !== undefined
|
|
129
|
+
? (oarMcpServer || null)
|
|
130
|
+
: (() => {
|
|
131
|
+
if (isOarMcpServerDisabled(env)) return null;
|
|
132
|
+
try {
|
|
133
|
+
return buildOarMcpServerLaunch({ nodePath, conversationId: sdkSessionId, env });
|
|
134
|
+
} catch (error) {
|
|
135
|
+
dbg('oar mcp server unavailable', error?.message || String(error));
|
|
136
|
+
return null;
|
|
137
|
+
}
|
|
138
|
+
})();
|
|
139
|
+
const acpMcpServers = oarMcpLaunch ? [toAcpStdioMcpServer(oarMcpLaunch, OAR_MCP_SERVER_NAME)] : [];
|
|
115
140
|
let agentHandle = null;
|
|
116
141
|
let grokNativeSessionId = '';
|
|
117
142
|
let activeMessage = null;
|
|
@@ -175,6 +200,12 @@ export function createGrokTurnRunner({
|
|
|
175
200
|
alwaysApprove,
|
|
176
201
|
nativeSessionId: resumeId,
|
|
177
202
|
model,
|
|
203
|
+
// The adapter builds the ACP client and opens the session in one step,
|
|
204
|
+
// so the client it builds must already carry the MCP servers — for
|
|
205
|
+
// session/new and for session/load alike.
|
|
206
|
+
...(acpMcpServers.length
|
|
207
|
+
? { AcpClientImpl: withAcpMcpServers(AcpClient, acpMcpServers), mcpServers: acpMcpServers }
|
|
208
|
+
: {}),
|
|
178
209
|
dbg,
|
|
179
210
|
});
|
|
180
211
|
if (agentHandle.sessionId) {
|
|
@@ -188,6 +219,20 @@ export function createGrokTurnRunner({
|
|
|
188
219
|
return agentHandle;
|
|
189
220
|
}
|
|
190
221
|
|
|
222
|
+
/**
|
|
223
|
+
* The inactivity hold. A remote_relay call runs inside the MCP server, which
|
|
224
|
+
* this worker cannot see: Grok just goes quiet for as long as the relay
|
|
225
|
+
* waits on the remote turn. Before the ACP watchdog calls that a stall, the
|
|
226
|
+
* relay is asked whether a call of this conversation is still in flight.
|
|
227
|
+
* Keyed by the MCP server's own conversation id, which is what its calls
|
|
228
|
+
* carry.
|
|
229
|
+
*/
|
|
230
|
+
async function remoteRelayCallInFlight() {
|
|
231
|
+
const inflight = await fetchRemoteRelayInflight({ api, conversationId: sdkSessionId });
|
|
232
|
+
if (inflight > 0) dbg('remote_relay call in flight; holding the inactivity watchdog', String(inflight));
|
|
233
|
+
return inflight > 0;
|
|
234
|
+
}
|
|
235
|
+
|
|
191
236
|
async function postActivity(message, text, subagentRunId = null) {
|
|
192
237
|
if (!text) return;
|
|
193
238
|
await api('POST', '/api/activity', {
|
|
@@ -369,7 +414,9 @@ export function createGrokTurnRunner({
|
|
|
369
414
|
const modeNudge = grokModeNudge(message.relayMode, lastNudgedRelayMode);
|
|
370
415
|
const userText = buildGrokUserText(message);
|
|
371
416
|
// Advisory: a preview lookup that fails costs the guidance, never the turn.
|
|
372
|
-
|
|
417
|
+
// With the OAR MCP server wired in, `preview` is a real tool and the HTTP
|
|
418
|
+
// instruction block (which says there is none) would contradict it.
|
|
419
|
+
const previewBlock = previewInstructionsSent || acpMcpServers.length
|
|
373
420
|
? ''
|
|
374
421
|
: await Promise.resolve()
|
|
375
422
|
.then(() => getPreviewInstructions?.())
|
|
@@ -378,6 +425,16 @@ export function createGrokTurnRunner({
|
|
|
378
425
|
const promptText = [previewBlock, mediaBlock, modeNudge, userText].filter(Boolean).join('\n\n');
|
|
379
426
|
const pendingNudgedRelayMode = String(message.relayMode || 'agent').trim().toLowerCase();
|
|
380
427
|
|
|
428
|
+
// The user's max-turn-duration setting overrides the worker-local
|
|
429
|
+
// absolute cap: "No limit" (0) must not be silently trimmed to the
|
|
430
|
+
// 30-minute default. The inactivity watchdog stays — it detects a dead
|
|
431
|
+
// transport, not a long turn — but asks the relay before it fires while
|
|
432
|
+
// Grok may be blocked on a remote_relay call.
|
|
433
|
+
const watchdog = {
|
|
434
|
+
...(turnCeilingMs !== null ? { maxTurnMs: turnCeilingMs > 0 ? turnCeilingMs : 0 } : {}),
|
|
435
|
+
...(acpMcpServers.length ? { holdStall: remoteRelayCallInFlight } : {}),
|
|
436
|
+
};
|
|
437
|
+
|
|
381
438
|
let turn = null;
|
|
382
439
|
let planBoardPosted = false;
|
|
383
440
|
const controlState = controlPoller?.start?.({
|
|
@@ -399,13 +456,7 @@ export function createGrokTurnRunner({
|
|
|
399
456
|
text: promptText,
|
|
400
457
|
reasoningEffort: message.reasoningEffort || '',
|
|
401
458
|
abortSignal: abortController.signal,
|
|
402
|
-
|
|
403
|
-
// absolute cap: "No limit" (0) must not be silently trimmed to
|
|
404
|
-
// the 30-minute default. The inactivity watchdog stays — it
|
|
405
|
-
// detects a dead transport, not a long turn.
|
|
406
|
-
watchdog: turnCeilingMs !== null
|
|
407
|
-
? { maxTurnMs: turnCeilingMs > 0 ? turnCeilingMs : 0 }
|
|
408
|
-
: null,
|
|
459
|
+
watchdog: Object.keys(watchdog).length ? watchdog : null,
|
|
409
460
|
dbg,
|
|
410
461
|
});
|
|
411
462
|
for await (const action of turn) {
|
|
@@ -4,6 +4,11 @@
|
|
|
4
4
|
*/
|
|
5
5
|
import { capThought } from '../../shared/thought-cap.mjs';
|
|
6
6
|
import { shouldEmitStreamUpdate } from '../../shared/stream-emit-gating.mjs';
|
|
7
|
+
import {
|
|
8
|
+
REMOTE_RELAY_TOOL_NAME,
|
|
9
|
+
isRemoteRelayToolName,
|
|
10
|
+
remoteRelayActivitySummary,
|
|
11
|
+
} from '../../shared/remote-relay-tool-core.mjs';
|
|
7
12
|
|
|
8
13
|
const MAX_TOOL_DETAIL_LENGTH = 140;
|
|
9
14
|
const SUBAGENT_TOOL_NAMES = new Set(['task', 'agent', 'subagent']);
|
|
@@ -59,6 +64,14 @@ export function summarizeToolInput(input) {
|
|
|
59
64
|
}
|
|
60
65
|
|
|
61
66
|
export function formatToolActivityText(toolName, input, title = '') {
|
|
67
|
+
// The OAR MCP server's `remote_relay`, however the agent labels it (bare,
|
|
68
|
+
// or namespaced by the server), reads as what it does on which relay.
|
|
69
|
+
if (isRemoteRelayToolName(toolName) || isRemoteRelayToolName(title)) {
|
|
70
|
+
const remoteSummary = remoteRelayActivitySummary(input);
|
|
71
|
+
return truncate(remoteSummary
|
|
72
|
+
? `Tool (${REMOTE_RELAY_TOOL_NAME}): ${remoteSummary}`
|
|
73
|
+
: `Tool (${REMOTE_RELAY_TOOL_NAME})`);
|
|
74
|
+
}
|
|
62
75
|
const name = String(toolName || title || 'tool').trim() || 'tool';
|
|
63
76
|
const summary = summarizeToolInput(input) || String(title || '').trim();
|
|
64
77
|
return truncate(summary ? `Tool (${name}): ${summary}` : `Tool (${name})`);
|