@oar-sh/oar 0.9.2 → 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 +336 -0
- package/README.md +455 -423
- package/bin/oar.js +25 -10
- package/package.json +11 -5
- package/server/README.md +44 -4
- 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 +1073 -69
- package/server/claude-worker/claude-session-worker.mjs +45 -4
- package/server/claude-worker/claude-turn-publisher.mjs +81 -20
- package/server/claude-worker/claude-worker-link-wiring.mjs +9 -0
- package/server/claude-worker/sdk-message-normalizer.mjs +7 -0
- package/server/copilot-worker/copilot-ask-user-tool.mjs +89 -0
- package/server/copilot-worker/copilot-continuation-signals.mjs +55 -0
- package/server/copilot-worker/copilot-question-bridge.mjs +34 -12
- package/server/copilot-worker/copilot-remote-relay-tool.mjs +59 -0
- package/server/copilot-worker/copilot-sdk-event-normalizer.mjs +113 -14
- package/server/copilot-worker/copilot-sdk-session-process.mjs +2349 -498
- package/server/copilot-worker/copilot-sdk-session-worker.mjs +82 -20
- 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 +28 -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/0004-steer-settle-markers.mjs +119 -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 +125 -2
- package/server/public/app/ask-user-view.js +102 -13
- package/server/public/app/attachments-view.js +37 -2
- package/server/public/app/background-task-timeout-options.mjs +9 -0
- package/server/public/app/background-tasks-view.mjs +33 -18
- package/server/public/app/bootstrap.js +55 -14
- package/server/public/app/composer-control-state.mjs +52 -34
- package/server/public/app/conversation-deep-link.mjs +42 -0
- package/server/public/app/conversation-draft-sync.mjs +187 -0
- package/server/public/app/conversation-list-filter.mjs +86 -0
- package/server/public/app/conversation-provider-indicator.mjs +8 -0
- package/server/public/app/conversation-view.js +1016 -200
- package/server/public/app/emoji-view.js +2 -0
- package/server/public/app/font-scaling.js +4 -37
- package/server/public/app/host-suspend-ui.js +272 -0
- package/server/public/app/journal-view.js +149 -6
- package/server/public/app/mention-autocomplete.mjs +298 -0
- package/server/public/app/model-selector-options.mjs +1 -1
- package/server/public/app/push-settings.js +1 -0
- package/server/public/app/pwa-install.js +8 -3
- package/server/public/app/question-multi-select.mjs +52 -0
- package/server/public/app/relay-prompt-sanitizer.mjs +61 -3
- 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 +12 -8
- package/server/public/app/settings-tabs.js +1 -1
- package/server/public/app/socket-handlers.js +37 -1
- package/server/public/app/steered-turn-merge.mjs +100 -0
- package/server/public/app/store.js +126 -15
- package/server/public/app/sync-outbox.mjs +16 -8
- package/server/public/app/thinking-follow.js +260 -0
- package/server/public/app/tool-activity-emoji.mjs +40 -0
- package/server/public/app/transcript-viewport-anchor.mjs +210 -0
- package/server/public/index.html +463 -17
- package/server/public/sw.js +7 -0
- package/server/relay-tools.md +7 -1
- package/server/repositories/message-repository.mjs +68 -18
- package/server/repositories/question-repository.mjs +18 -0
- package/server/repositories/remote-relay-repository.mjs +75 -0
- package/server/repositories/session-repository.mjs +13 -32
- package/server/routes/ask-user-routes.mjs +42 -2
- package/server/routes/messages-routes.mjs +572 -56
- package/server/routes/remote-relay-routes.mjs +133 -0
- package/server/routes/sessions-routes.mjs +378 -84
- package/server/server-runtime.mjs +303 -25
- 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/live-turn-picker.mjs +47 -0
- package/server/services/push-dispatch-service.mjs +37 -4
- package/server/services/relay-prompt-sanitizer.mjs +17 -2
- package/server/services/relay-question-context.mjs +46 -0
- 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/sdk-session-import-service.mjs +49 -7
- package/server/services/session-worker-launch-service.mjs +131 -12
- package/server/services/session-worker-registry-service.mjs +75 -0
- package/server/services/session-worker-supervisor-service.mjs +7 -1
- package/server/services/session-worker-websocket-service.mjs +23 -1
- package/server/services/steer-resend.mjs +45 -0
- package/shared/background-task-timeout.mjs +12 -4
- package/shared/control-poller.mjs +4 -1
- package/shared/question-multi-select.mjs +16 -0
- package/shared/relay-stop-failure.mjs +15 -0
- 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/slash-command-guard.mjs +76 -0
- package/shared/steer-note.mjs +66 -0
- package/shared/steer-settle-failure.mjs +89 -0
- package/shared/steer-settle-markers.mjs +18 -0
- package/shared/worker-crash-guard.mjs +40 -25
- package/shared/worker-runtime/api-client.mjs +112 -22
- package/shared/worker-runtime/heartbeat.mjs +55 -1
- package/shared/worker-runtime/runner-link-wiring.mjs +44 -0
- package/shared/worker-runtime/worker-websocket-link.mjs +119 -8
- package/server/claude-worker/claude-session-test-harness.mjs +0 -250
- package/server/claude-worker/fixtures/workflow-run-record.json +0 -1
- package/server/copilot-worker/copilot-sdk-test-harness.mjs +0 -382
- package/server/copilot-worker/fixtures/abort-turn.json +0 -588
- package/server/copilot-worker/fixtures/ask-user-turn.json +0 -1154
- package/server/copilot-worker/fixtures/background-timer-continuation.json +0 -445
- package/server/copilot-worker/fixtures/background-timer-turn.json +0 -497
- package/server/copilot-worker/fixtures/happy-turn.json +0 -566
- package/server/copilot-worker/fixtures/quota-turn.json +0 -647
- package/server/copilot-worker/fixtures/reasoning-turn.json +0 -625
- package/server/copilot-worker/fixtures/resume-turn.json +0 -700
- package/server/copilot-worker/fixtures/subagent-turn.json +0 -1378
- package/server/copilot-worker/fixtures/tool-permission-turn.json +0 -1293
- package/server/routes/messages-routes-test-harness.mjs +0 -111
- package/server/services/fixtures/claude-auth-stub.cmd +0 -1
- package/server/services/fixtures/claude-auth-stub.mjs +0 -76
- package/server/services/fixtures/claude-auth-stub.sh +0 -4
- package/server/services/fixtures/cli-install-stub.cmd +0 -1
- package/server/services/fixtures/cli-install-stub.mjs +0 -98
- package/server/services/fixtures/cli-install-stub.sh +0 -4
- package/server/services/fixtures/grok-stub.cmd +0 -1
- package/server/services/fixtures/grok-stub.mjs +0 -89
- package/server/services/fixtures/grok-stub.sh +0 -4
- package/shared/fixtures/copilot-catalog-raw-2026-09-07.json +0 -2363
|
@@ -22,6 +22,11 @@
|
|
|
22
22
|
import { sanitizeSubagentRunId } from '../../shared/subagent-run-id.mjs';
|
|
23
23
|
import { capThought } from '../../shared/thought-cap.mjs';
|
|
24
24
|
import { shouldEmitStreamUpdate } from '../../shared/stream-emit-gating.mjs';
|
|
25
|
+
import {
|
|
26
|
+
REMOTE_RELAY_TOOL_NAME,
|
|
27
|
+
isRemoteRelayToolName,
|
|
28
|
+
remoteRelayActivitySummary,
|
|
29
|
+
} from '../../shared/remote-relay-tool-core.mjs';
|
|
25
30
|
|
|
26
31
|
const MAX_TOOL_DETAIL_LENGTH = 140;
|
|
27
32
|
|
|
@@ -78,8 +83,13 @@ export function summarizeToolInput(input) {
|
|
|
78
83
|
}
|
|
79
84
|
|
|
80
85
|
export function formatToolActivityText(toolName, input) {
|
|
81
|
-
const
|
|
82
|
-
|
|
86
|
+
const rawName = String(toolName || '').trim() || 'tool';
|
|
87
|
+
// `remote_relay` (bare, or namespaced by an MCP server) reads as what it
|
|
88
|
+
// does on which relay (`send → linux-test session 01234567: “…”`); its
|
|
89
|
+
// arguments may arrive as a JSON string.
|
|
90
|
+
const isRemoteRelay = isRemoteRelayToolName(rawName);
|
|
91
|
+
const name = isRemoteRelay ? REMOTE_RELAY_TOOL_NAME : rawName;
|
|
92
|
+
const summary = (isRemoteRelay && remoteRelayActivitySummary(input)) || summarizeToolInput(input);
|
|
83
93
|
return truncate(summary ? `Tool (${name}): ${summary}` : `Tool (${name})`);
|
|
84
94
|
}
|
|
85
95
|
|
|
@@ -228,7 +238,11 @@ function createUsageAccumulator() {
|
|
|
228
238
|
* `{ channel, payload }` actions; the session process dispatches them to the
|
|
229
239
|
* relay and treats the first `result` action as the turn's terminator.
|
|
230
240
|
*/
|
|
231
|
-
export function createCopilotEventNormalizer(
|
|
241
|
+
export function createCopilotEventNormalizer({
|
|
242
|
+
// The runner's answer to "is this agent id a background agent?" — shared
|
|
243
|
+
// across turns, unlike this normalizer's own `backgroundRuns`.
|
|
244
|
+
isBackgroundAgent = () => false,
|
|
245
|
+
} = {}) {
|
|
232
246
|
let sessionModel = '';
|
|
233
247
|
let sessionId = '';
|
|
234
248
|
let lastEmittedStreamText = '';
|
|
@@ -306,6 +320,13 @@ export function createCopilotEventNormalizer() {
|
|
|
306
320
|
// runtime tags only one of them.
|
|
307
321
|
const subagentRuns = new Map(); // runId -> { displayName, status, opened }
|
|
308
322
|
const subagentAliases = new Map(); // agentId|toolCallId -> runId
|
|
323
|
+
// Runs spawned with `executionMode: "background"`: task-panel cards, not
|
|
324
|
+
// lanes. This normalizer lives for ONE turn while a background agent
|
|
325
|
+
// outlives it, so the runner's knowledge (`isBackgroundAgent`) is consulted
|
|
326
|
+
// too — a later turn must not open a lane for an agent spawned earlier.
|
|
327
|
+
const backgroundRuns = new Set();
|
|
328
|
+
const isBackgroundRun = (runId, agentId) => backgroundRuns.has(runId)
|
|
329
|
+
|| (!!agentId && isBackgroundAgent(agentId) === true);
|
|
309
330
|
|
|
310
331
|
/**
|
|
311
332
|
* `useToolCallId` is true only for `subagent.*` lifecycle events, where
|
|
@@ -432,6 +453,17 @@ export function createCopilotEventNormalizer() {
|
|
|
432
453
|
return { channel: 'activity', payload: { text, subagentRunId } };
|
|
433
454
|
}
|
|
434
455
|
|
|
456
|
+
/**
|
|
457
|
+
* A line that only tracks background work: the main agent polling its
|
|
458
|
+
* background agent (`read_agent`), or a permission granted for a tool call
|
|
459
|
+
* (the call's own line, when it is the main agent's, is the real work). It
|
|
460
|
+
* still shows on the row; it just does not make a runtime-opened row worth
|
|
461
|
+
* keeping on its own (the task card covers the background agent).
|
|
462
|
+
*/
|
|
463
|
+
function bookkeepingActivityAction(text) {
|
|
464
|
+
return { ...activityAction(text), bookkeeping: true };
|
|
465
|
+
}
|
|
466
|
+
|
|
435
467
|
/**
|
|
436
468
|
* Build the single terminal `result` action for this turn. Guarded so a
|
|
437
469
|
* `session.error` arriving after a `session.idle` (or a second error) cannot
|
|
@@ -498,23 +530,43 @@ export function createCopilotEventNormalizer() {
|
|
|
498
530
|
case 'subagent.started': {
|
|
499
531
|
const runId = resolveSubagentRun(event, { useToolCallId: true });
|
|
500
532
|
if (!runId) return [];
|
|
533
|
+
// A BACKGROUND agent (`executionMode: "background"`) is not a lane in
|
|
534
|
+
// this reply: it outlives the turn and shows in the task panel as a
|
|
535
|
+
// stoppable card (the runner reads `rpc.tasks`), and its completion
|
|
536
|
+
// arrives as a continuation. Opening a lane for it would leave a
|
|
537
|
+
// bubble spinning after the reply settled — or, if closed at the
|
|
538
|
+
// terminal, would mark a running agent failed. A one-line note keeps
|
|
539
|
+
// the transcript honest; its tagged events are dropped below.
|
|
540
|
+
if (String(data.executionMode || '').trim().toLowerCase() === 'background') {
|
|
541
|
+
backgroundRuns.add(runId);
|
|
542
|
+
const name = subagentDisplayName(data);
|
|
543
|
+
const agentType = String(data.agentType || '').trim();
|
|
544
|
+
return [activityAction(truncate(
|
|
545
|
+
`Started background agent: ${name}${agentType && agentType !== name ? ` (${agentType})` : ''} — see the task panel`,
|
|
546
|
+
))];
|
|
547
|
+
}
|
|
501
548
|
return ensureSubagentRun(runId, subagentDisplayName(data));
|
|
502
549
|
}
|
|
503
550
|
case 'subagent.configured':
|
|
504
551
|
case 'subagent.selected': {
|
|
505
|
-
// `subagent.configured`
|
|
506
|
-
//
|
|
507
|
-
//
|
|
508
|
-
//
|
|
509
|
-
//
|
|
510
|
-
//
|
|
552
|
+
// `subagent.configured` (since documented: `{model, multiTurn, …}`)
|
|
553
|
+
// arrives between started and completed, carrying `agentId` but
|
|
554
|
+
// neither `toolCallId` nor a display name. It is handled rather than
|
|
555
|
+
// ignored so the lane still opens if it ever arrives first, and it
|
|
556
|
+
// must never clobber the roster's display name with its own absent
|
|
557
|
+
// one (hence the `||` in ensureSubagentRun). A background agent's
|
|
558
|
+
// arrives too, and must not open the lane `subagent.started` withheld.
|
|
511
559
|
const runId = resolveSubagentRun(event, { useToolCallId: true });
|
|
512
560
|
if (!runId) return [];
|
|
561
|
+
if (isBackgroundRun(runId, subagentEventKeys(event).agentId)) return [];
|
|
513
562
|
return ensureSubagentRun(runId, subagentDisplayName(data));
|
|
514
563
|
}
|
|
515
564
|
case 'subagent.completed': {
|
|
516
565
|
const runId = resolveSubagentRun(event, { useToolCallId: true });
|
|
517
566
|
if (!runId) return [];
|
|
567
|
+
// A background agent finishing inside a later turn is the task panel's
|
|
568
|
+
// business (the runner watches the same event); no lane to close.
|
|
569
|
+
if (isBackgroundRun(runId, subagentEventKeys(event).agentId)) return [];
|
|
518
570
|
const actions = closeSubagentRun(runId, 'completed', subagentDisplayName(data));
|
|
519
571
|
const stats = formatSubagentStats(data);
|
|
520
572
|
if (stats) actions.push(activityAction(truncate(`Finished · ${stats}`), runId));
|
|
@@ -525,6 +577,11 @@ export function createCopilotEventNormalizer() {
|
|
|
525
577
|
if (!runId) return [];
|
|
526
578
|
const name = subagentDisplayName(data);
|
|
527
579
|
const reason = String(data.error || '').trim() || 'unknown error';
|
|
580
|
+
// A background agent's failure still deserves a main-thread line (the
|
|
581
|
+
// parent agent recovers silently), but there is no lane to close.
|
|
582
|
+
if (isBackgroundRun(runId, subagentEventKeys(event).agentId)) {
|
|
583
|
+
return [activityAction(truncate(`Background agent failed (${name}): ${reason}`))];
|
|
584
|
+
}
|
|
528
585
|
const actions = closeSubagentRun(runId, 'failed', name);
|
|
529
586
|
actions.push(activityAction(truncate(`Subagent failed: ${reason}`), runId));
|
|
530
587
|
// ALSO on the main thread. The parent agent recovers from a failed
|
|
@@ -551,6 +608,14 @@ export function createCopilotEventNormalizer() {
|
|
|
551
608
|
if (isSubagentEvent(event)) {
|
|
552
609
|
const runId = resolveSubagentRun(event);
|
|
553
610
|
if (!runId) return [];
|
|
611
|
+
// A background agent's traffic belongs to its task card (the runner
|
|
612
|
+
// tracks its live tool call and tokens from the same events), not to a
|
|
613
|
+
// lane in this reply — but its model calls still cost real money, so
|
|
614
|
+
// they count toward the spend of whichever turn is live, as before.
|
|
615
|
+
if (isBackgroundRun(runId, subagentEventKeys(event).agentId)) {
|
|
616
|
+
if (type === 'assistant.usage') usage.add(data, { allowModel: false });
|
|
617
|
+
return [];
|
|
618
|
+
}
|
|
554
619
|
if (type === 'assistant.message') {
|
|
555
620
|
const content = String(data.content || '');
|
|
556
621
|
if (!content.trim()) return [];
|
|
@@ -718,7 +783,8 @@ export function createCopilotEventNormalizer() {
|
|
|
718
783
|
lastEmittedStreamText = text;
|
|
719
784
|
return [{ channel: 'stream', payload: { text, done: false, subagentRunId: null } }];
|
|
720
785
|
}
|
|
721
|
-
|
|
786
|
+
const toolLine = formatToolActivityText(toolName, data.arguments);
|
|
787
|
+
return [toolName === 'read_agent' ? bookkeepingActivityAction(toolLine) : activityAction(toolLine)];
|
|
722
788
|
}
|
|
723
789
|
case 'tool.execution_complete': {
|
|
724
790
|
// There is no `tool.*_failed` event type — failure is `success: false`
|
|
@@ -743,9 +809,9 @@ export function createCopilotEventNormalizer() {
|
|
|
743
809
|
const summary = permissionRequests.get(requestId) || '';
|
|
744
810
|
permissionRequests.delete(requestId);
|
|
745
811
|
const kind = String(data.result?.kind || 'unknown').trim();
|
|
746
|
-
|
|
747
|
-
|
|
748
|
-
|
|
812
|
+
const line = truncate(summary ? `Permission ${kind}: ${summary}` : `Permission ${kind}`);
|
|
813
|
+
// A denial is news; a grant is bookkeeping for the call it unblocked.
|
|
814
|
+
return [kind.startsWith('approved') ? bookkeepingActivityAction(line) : activityAction(line)];
|
|
749
815
|
}
|
|
750
816
|
case 'user_input.requested': {
|
|
751
817
|
// The question is forwarded to the relay as a question card; this row
|
|
@@ -791,7 +857,40 @@ export function createCopilotEventNormalizer() {
|
|
|
791
857
|
lastEmittedStreamText = text;
|
|
792
858
|
return [{ channel: 'stream', payload: { text, done: false, subagentRunId: null } }];
|
|
793
859
|
}
|
|
794
|
-
// ----
|
|
860
|
+
// ---- compaction ------------------------------------------------------
|
|
861
|
+
//
|
|
862
|
+
// Surfaced as activity lines so a long pause reads as what it is. The
|
|
863
|
+
// runner ALSO watches these two events directly for its steering hold
|
|
864
|
+
// (a message must not be pushed into a compaction's replay).
|
|
865
|
+
case 'session.compaction_start': {
|
|
866
|
+
return [activityAction('Compacting the conversation context…')];
|
|
867
|
+
}
|
|
868
|
+
case 'session.compaction_complete': {
|
|
869
|
+
if (data.success === false) {
|
|
870
|
+
const reason = String(data.error || '').trim();
|
|
871
|
+
return [activityAction(truncate(reason ? `Context compaction failed: ${reason}` : 'Context compaction failed'))];
|
|
872
|
+
}
|
|
873
|
+
const removed = Number(data.messagesRemoved);
|
|
874
|
+
const tokens = Number(data.tokensRemoved);
|
|
875
|
+
const parts = [];
|
|
876
|
+
if (Number.isFinite(removed) && removed > 0) parts.push(`${removed} messages`);
|
|
877
|
+
if (Number.isFinite(tokens) && tokens > 0) parts.push(`${Math.round(tokens / 1000)}k tokens`);
|
|
878
|
+
return [activityAction(parts.length ? `Context compacted (removed ${parts.join(', ')})` : 'Context compacted')];
|
|
879
|
+
}
|
|
880
|
+
// ---- the terminators ---------------------------------------------------
|
|
881
|
+
//
|
|
882
|
+
// `assistant.idle` is "the main agent's processing loop went idle" and
|
|
883
|
+
// fires even while background agents / attached shells keep the
|
|
884
|
+
// session-level idle deferred (runtime 1.0.88, live-probed 2026-09-25).
|
|
885
|
+
// It is the terminator that lets a row settle while a background agent
|
|
886
|
+
// the turn spawned keeps running under the task panel — exactly the
|
|
887
|
+
// Claude worker's lingering model. `session.idle` stays the hard
|
|
888
|
+
// terminator for runtimes that do not emit the assistant-level one and
|
|
889
|
+
// for the drain's end; `terminalEmitted` makes the second a no-op.
|
|
890
|
+
case 'assistant.idle': {
|
|
891
|
+
if (data.aborted === true) aborted = true;
|
|
892
|
+
return terminalAction({ subtype: aborted ? 'aborted' : 'completed' });
|
|
893
|
+
}
|
|
795
894
|
case 'session.idle': {
|
|
796
895
|
if (data.aborted === true) aborted = true;
|
|
797
896
|
return terminalAction({ subtype: aborted ? 'aborted' : 'completed' });
|