@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.
Files changed (138) hide show
  1. package/.github/extensions/web-relay/extension.mjs +6 -1
  2. package/.github/extensions/web-relay/skills/tool-activity.mjs +7 -0
  3. package/CHANGELOG.md +336 -0
  4. package/README.md +455 -423
  5. package/bin/oar.js +25 -10
  6. package/package.json +11 -5
  7. package/server/README.md +44 -4
  8. package/server/claude-worker/claude-remote-relay-tool.mjs +99 -0
  9. package/server/claude-worker/claude-sdk-adapter.mjs +26 -4
  10. package/server/claude-worker/claude-session-process.mjs +1073 -69
  11. package/server/claude-worker/claude-session-worker.mjs +45 -4
  12. package/server/claude-worker/claude-turn-publisher.mjs +81 -20
  13. package/server/claude-worker/claude-worker-link-wiring.mjs +9 -0
  14. package/server/claude-worker/sdk-message-normalizer.mjs +7 -0
  15. package/server/copilot-worker/copilot-ask-user-tool.mjs +89 -0
  16. package/server/copilot-worker/copilot-continuation-signals.mjs +55 -0
  17. package/server/copilot-worker/copilot-question-bridge.mjs +34 -12
  18. package/server/copilot-worker/copilot-remote-relay-tool.mjs +59 -0
  19. package/server/copilot-worker/copilot-sdk-event-normalizer.mjs +113 -14
  20. package/server/copilot-worker/copilot-sdk-session-process.mjs +2349 -498
  21. package/server/copilot-worker/copilot-sdk-session-worker.mjs +82 -20
  22. package/server/cursor-worker/cursor-remote-relay-tool.mjs +65 -0
  23. package/server/cursor-worker/cursor-turn-runner.mjs +45 -2
  24. package/server/cursor-worker/sdk-message-normalizer.mjs +25 -0
  25. package/server/db-schema.mjs +28 -0
  26. package/server/grok-worker/acp-client.mjs +92 -6
  27. package/server/grok-worker/grok-turn-runner.mjs +59 -8
  28. package/server/grok-worker/sdk-message-normalizer.mjs +13 -0
  29. package/server/mcp/oar-mcp-server.mjs +473 -0
  30. package/server/migrations/0004-steer-settle-markers.mjs +119 -0
  31. package/server/migrations/0005-remote-relays.mjs +90 -0
  32. package/server/public/app/action-confirmations.js +141 -13
  33. package/server/public/app/api-client.js +125 -2
  34. package/server/public/app/ask-user-view.js +102 -13
  35. package/server/public/app/attachments-view.js +37 -2
  36. package/server/public/app/background-task-timeout-options.mjs +9 -0
  37. package/server/public/app/background-tasks-view.mjs +33 -18
  38. package/server/public/app/bootstrap.js +55 -14
  39. package/server/public/app/composer-control-state.mjs +52 -34
  40. package/server/public/app/conversation-deep-link.mjs +42 -0
  41. package/server/public/app/conversation-draft-sync.mjs +187 -0
  42. package/server/public/app/conversation-list-filter.mjs +86 -0
  43. package/server/public/app/conversation-provider-indicator.mjs +8 -0
  44. package/server/public/app/conversation-view.js +1016 -200
  45. package/server/public/app/emoji-view.js +2 -0
  46. package/server/public/app/font-scaling.js +4 -37
  47. package/server/public/app/host-suspend-ui.js +272 -0
  48. package/server/public/app/journal-view.js +149 -6
  49. package/server/public/app/mention-autocomplete.mjs +298 -0
  50. package/server/public/app/model-selector-options.mjs +1 -1
  51. package/server/public/app/push-settings.js +1 -0
  52. package/server/public/app/pwa-install.js +8 -3
  53. package/server/public/app/question-multi-select.mjs +52 -0
  54. package/server/public/app/relay-prompt-sanitizer.mjs +61 -3
  55. package/server/public/app/remote-relay-origin-view.mjs +76 -0
  56. package/server/public/app/remote-relay-shared.mjs +85 -0
  57. package/server/public/app/remote-relays-settings.js +618 -0
  58. package/server/public/app/remote-relays-store.mjs +126 -0
  59. package/server/public/app/settings-modal.js +12 -8
  60. package/server/public/app/settings-tabs.js +1 -1
  61. package/server/public/app/socket-handlers.js +37 -1
  62. package/server/public/app/steered-turn-merge.mjs +100 -0
  63. package/server/public/app/store.js +126 -15
  64. package/server/public/app/sync-outbox.mjs +16 -8
  65. package/server/public/app/thinking-follow.js +260 -0
  66. package/server/public/app/tool-activity-emoji.mjs +40 -0
  67. package/server/public/app/transcript-viewport-anchor.mjs +210 -0
  68. package/server/public/index.html +463 -17
  69. package/server/public/sw.js +7 -0
  70. package/server/relay-tools.md +7 -1
  71. package/server/repositories/message-repository.mjs +68 -18
  72. package/server/repositories/question-repository.mjs +18 -0
  73. package/server/repositories/remote-relay-repository.mjs +75 -0
  74. package/server/repositories/session-repository.mjs +13 -32
  75. package/server/routes/ask-user-routes.mjs +42 -2
  76. package/server/routes/messages-routes.mjs +572 -56
  77. package/server/routes/remote-relay-routes.mjs +133 -0
  78. package/server/routes/sessions-routes.mjs +378 -84
  79. package/server/server-runtime.mjs +303 -25
  80. package/server/services/github-ci-activity-service.mjs +199 -0
  81. package/server/services/host-suspend-command.mjs +38 -0
  82. package/server/services/host-suspend-service.mjs +254 -0
  83. package/server/services/live-turn-picker.mjs +47 -0
  84. package/server/services/push-dispatch-service.mjs +37 -4
  85. package/server/services/relay-prompt-sanitizer.mjs +17 -2
  86. package/server/services/relay-question-context.mjs +46 -0
  87. package/server/services/remote-relay-caller-context.mjs +341 -0
  88. package/server/services/remote-relay-client.mjs +332 -0
  89. package/server/services/remote-relay-dispatcher.mjs +1500 -0
  90. package/server/services/remote-relay-inbound.mjs +187 -0
  91. package/server/services/remote-relay-pairing.mjs +189 -0
  92. package/server/services/remote-relay-registry-service.mjs +581 -0
  93. package/server/services/sdk-session-import-service.mjs +49 -7
  94. package/server/services/session-worker-launch-service.mjs +131 -12
  95. package/server/services/session-worker-registry-service.mjs +75 -0
  96. package/server/services/session-worker-supervisor-service.mjs +7 -1
  97. package/server/services/session-worker-websocket-service.mjs +23 -1
  98. package/server/services/steer-resend.mjs +45 -0
  99. package/shared/background-task-timeout.mjs +12 -4
  100. package/shared/control-poller.mjs +4 -1
  101. package/shared/question-multi-select.mjs +16 -0
  102. package/shared/relay-stop-failure.mjs +15 -0
  103. package/shared/remote-relay-contract.mjs +539 -0
  104. package/shared/remote-relay-mentions.mjs +129 -0
  105. package/shared/remote-relay-tool-core.mjs +314 -0
  106. package/shared/slash-command-guard.mjs +76 -0
  107. package/shared/steer-note.mjs +66 -0
  108. package/shared/steer-settle-failure.mjs +89 -0
  109. package/shared/steer-settle-markers.mjs +18 -0
  110. package/shared/worker-crash-guard.mjs +40 -25
  111. package/shared/worker-runtime/api-client.mjs +112 -22
  112. package/shared/worker-runtime/heartbeat.mjs +55 -1
  113. package/shared/worker-runtime/runner-link-wiring.mjs +44 -0
  114. package/shared/worker-runtime/worker-websocket-link.mjs +119 -8
  115. package/server/claude-worker/claude-session-test-harness.mjs +0 -250
  116. package/server/claude-worker/fixtures/workflow-run-record.json +0 -1
  117. package/server/copilot-worker/copilot-sdk-test-harness.mjs +0 -382
  118. package/server/copilot-worker/fixtures/abort-turn.json +0 -588
  119. package/server/copilot-worker/fixtures/ask-user-turn.json +0 -1154
  120. package/server/copilot-worker/fixtures/background-timer-continuation.json +0 -445
  121. package/server/copilot-worker/fixtures/background-timer-turn.json +0 -497
  122. package/server/copilot-worker/fixtures/happy-turn.json +0 -566
  123. package/server/copilot-worker/fixtures/quota-turn.json +0 -647
  124. package/server/copilot-worker/fixtures/reasoning-turn.json +0 -625
  125. package/server/copilot-worker/fixtures/resume-turn.json +0 -700
  126. package/server/copilot-worker/fixtures/subagent-turn.json +0 -1378
  127. package/server/copilot-worker/fixtures/tool-permission-turn.json +0 -1293
  128. package/server/routes/messages-routes-test-harness.mjs +0 -111
  129. package/server/services/fixtures/claude-auth-stub.cmd +0 -1
  130. package/server/services/fixtures/claude-auth-stub.mjs +0 -76
  131. package/server/services/fixtures/claude-auth-stub.sh +0 -4
  132. package/server/services/fixtures/cli-install-stub.cmd +0 -1
  133. package/server/services/fixtures/cli-install-stub.mjs +0 -98
  134. package/server/services/fixtures/cli-install-stub.sh +0 -4
  135. package/server/services/fixtures/grok-stub.cmd +0 -1
  136. package/server/services/fixtures/grok-stub.mjs +0 -89
  137. package/server/services/fixtures/grok-stub.sh +0 -4
  138. 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 name = String(toolName || '').trim() || 'tool';
82
- const summary = summarizeToolInput(input);
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` is UNDOCUMENTED — it appears in no published
506
- // schema and a live probe found it between started and completed,
507
- // carrying `agentId` but neither `toolCallId` nor a display name. It is
508
- // handled rather than ignored so the lane still opens if it ever
509
- // arrives first, and it must never clobber the roster's display name
510
- // with its own absent one (hence the `||` in ensureSubagentRun).
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
- return [activityAction(formatToolActivityText(toolName, data.arguments))];
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
- return [activityAction(truncate(summary
747
- ? `Permission ${kind}: ${summary}`
748
- : `Permission ${kind}`))];
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
- // ---- the one true terminator -----------------------------------------
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' });