@oar-sh/oar 0.9.2 → 0.9.3
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/CHANGELOG.md +269 -0
- package/README.md +407 -424
- package/bin/oar.js +25 -10
- package/package.json +7 -4
- package/server/README.md +1 -1
- package/server/claude-worker/claude-session-process.mjs +981 -44
- 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/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-sdk-event-normalizer.mjs +101 -12
- package/server/copilot-worker/copilot-sdk-session-process.mjs +2269 -492
- package/server/copilot-worker/copilot-sdk-session-worker.mjs +82 -20
- package/server/db-schema.mjs +20 -0
- package/server/migrations/0004-steer-settle-markers.mjs +119 -0
- package/server/public/app/api-client.js +22 -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 +34 -8
- package/server/public/app/composer-control-state.mjs +52 -34
- 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 +919 -194
- package/server/public/app/emoji-view.js +2 -0
- package/server/public/app/font-scaling.js +4 -37
- package/server/public/app/journal-view.js +145 -5
- package/server/public/app/model-selector-options.mjs +1 -1
- package/server/public/app/question-multi-select.mjs +52 -0
- package/server/public/app/relay-prompt-sanitizer.mjs +17 -2
- package/server/public/app/settings-modal.js +8 -6
- package/server/public/app/socket-handlers.js +19 -0
- 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/tool-activity-emoji.mjs +36 -0
- package/server/public/app/transcript-viewport-anchor.mjs +210 -0
- package/server/public/index.html +206 -9
- package/server/public/sw.js +7 -0
- package/server/relay-tools.md +2 -0
- package/server/repositories/message-repository.mjs +61 -18
- package/server/repositories/question-repository.mjs +15 -0
- package/server/repositories/session-repository.mjs +7 -31
- package/server/routes/messages-routes.mjs +488 -46
- package/server/routes/sessions-routes.mjs +298 -55
- package/server/server-runtime.mjs +65 -25
- package/server/services/live-turn-picker.mjs +47 -0
- package/server/services/relay-prompt-sanitizer.mjs +17 -2
- package/server/services/relay-question-context.mjs +46 -0
- package/server/services/sdk-session-import-service.mjs +49 -7
- 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/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/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
|
@@ -228,7 +228,11 @@ function createUsageAccumulator() {
|
|
|
228
228
|
* `{ channel, payload }` actions; the session process dispatches them to the
|
|
229
229
|
* relay and treats the first `result` action as the turn's terminator.
|
|
230
230
|
*/
|
|
231
|
-
export function createCopilotEventNormalizer(
|
|
231
|
+
export function createCopilotEventNormalizer({
|
|
232
|
+
// The runner's answer to "is this agent id a background agent?" — shared
|
|
233
|
+
// across turns, unlike this normalizer's own `backgroundRuns`.
|
|
234
|
+
isBackgroundAgent = () => false,
|
|
235
|
+
} = {}) {
|
|
232
236
|
let sessionModel = '';
|
|
233
237
|
let sessionId = '';
|
|
234
238
|
let lastEmittedStreamText = '';
|
|
@@ -306,6 +310,13 @@ export function createCopilotEventNormalizer() {
|
|
|
306
310
|
// runtime tags only one of them.
|
|
307
311
|
const subagentRuns = new Map(); // runId -> { displayName, status, opened }
|
|
308
312
|
const subagentAliases = new Map(); // agentId|toolCallId -> runId
|
|
313
|
+
// Runs spawned with `executionMode: "background"`: task-panel cards, not
|
|
314
|
+
// lanes. This normalizer lives for ONE turn while a background agent
|
|
315
|
+
// outlives it, so the runner's knowledge (`isBackgroundAgent`) is consulted
|
|
316
|
+
// too — a later turn must not open a lane for an agent spawned earlier.
|
|
317
|
+
const backgroundRuns = new Set();
|
|
318
|
+
const isBackgroundRun = (runId, agentId) => backgroundRuns.has(runId)
|
|
319
|
+
|| (!!agentId && isBackgroundAgent(agentId) === true);
|
|
309
320
|
|
|
310
321
|
/**
|
|
311
322
|
* `useToolCallId` is true only for `subagent.*` lifecycle events, where
|
|
@@ -432,6 +443,17 @@ export function createCopilotEventNormalizer() {
|
|
|
432
443
|
return { channel: 'activity', payload: { text, subagentRunId } };
|
|
433
444
|
}
|
|
434
445
|
|
|
446
|
+
/**
|
|
447
|
+
* A line that only tracks background work: the main agent polling its
|
|
448
|
+
* background agent (`read_agent`), or a permission granted for a tool call
|
|
449
|
+
* (the call's own line, when it is the main agent's, is the real work). It
|
|
450
|
+
* still shows on the row; it just does not make a runtime-opened row worth
|
|
451
|
+
* keeping on its own (the task card covers the background agent).
|
|
452
|
+
*/
|
|
453
|
+
function bookkeepingActivityAction(text) {
|
|
454
|
+
return { ...activityAction(text), bookkeeping: true };
|
|
455
|
+
}
|
|
456
|
+
|
|
435
457
|
/**
|
|
436
458
|
* Build the single terminal `result` action for this turn. Guarded so a
|
|
437
459
|
* `session.error` arriving after a `session.idle` (or a second error) cannot
|
|
@@ -498,23 +520,43 @@ export function createCopilotEventNormalizer() {
|
|
|
498
520
|
case 'subagent.started': {
|
|
499
521
|
const runId = resolveSubagentRun(event, { useToolCallId: true });
|
|
500
522
|
if (!runId) return [];
|
|
523
|
+
// A BACKGROUND agent (`executionMode: "background"`) is not a lane in
|
|
524
|
+
// this reply: it outlives the turn and shows in the task panel as a
|
|
525
|
+
// stoppable card (the runner reads `rpc.tasks`), and its completion
|
|
526
|
+
// arrives as a continuation. Opening a lane for it would leave a
|
|
527
|
+
// bubble spinning after the reply settled — or, if closed at the
|
|
528
|
+
// terminal, would mark a running agent failed. A one-line note keeps
|
|
529
|
+
// the transcript honest; its tagged events are dropped below.
|
|
530
|
+
if (String(data.executionMode || '').trim().toLowerCase() === 'background') {
|
|
531
|
+
backgroundRuns.add(runId);
|
|
532
|
+
const name = subagentDisplayName(data);
|
|
533
|
+
const agentType = String(data.agentType || '').trim();
|
|
534
|
+
return [activityAction(truncate(
|
|
535
|
+
`Started background agent: ${name}${agentType && agentType !== name ? ` (${agentType})` : ''} — see the task panel`,
|
|
536
|
+
))];
|
|
537
|
+
}
|
|
501
538
|
return ensureSubagentRun(runId, subagentDisplayName(data));
|
|
502
539
|
}
|
|
503
540
|
case 'subagent.configured':
|
|
504
541
|
case 'subagent.selected': {
|
|
505
|
-
// `subagent.configured`
|
|
506
|
-
//
|
|
507
|
-
//
|
|
508
|
-
//
|
|
509
|
-
//
|
|
510
|
-
//
|
|
542
|
+
// `subagent.configured` (since documented: `{model, multiTurn, …}`)
|
|
543
|
+
// arrives between started and completed, carrying `agentId` but
|
|
544
|
+
// neither `toolCallId` nor a display name. It is handled rather than
|
|
545
|
+
// ignored so the lane still opens if it ever arrives first, and it
|
|
546
|
+
// must never clobber the roster's display name with its own absent
|
|
547
|
+
// one (hence the `||` in ensureSubagentRun). A background agent's
|
|
548
|
+
// arrives too, and must not open the lane `subagent.started` withheld.
|
|
511
549
|
const runId = resolveSubagentRun(event, { useToolCallId: true });
|
|
512
550
|
if (!runId) return [];
|
|
551
|
+
if (isBackgroundRun(runId, subagentEventKeys(event).agentId)) return [];
|
|
513
552
|
return ensureSubagentRun(runId, subagentDisplayName(data));
|
|
514
553
|
}
|
|
515
554
|
case 'subagent.completed': {
|
|
516
555
|
const runId = resolveSubagentRun(event, { useToolCallId: true });
|
|
517
556
|
if (!runId) return [];
|
|
557
|
+
// A background agent finishing inside a later turn is the task panel's
|
|
558
|
+
// business (the runner watches the same event); no lane to close.
|
|
559
|
+
if (isBackgroundRun(runId, subagentEventKeys(event).agentId)) return [];
|
|
518
560
|
const actions = closeSubagentRun(runId, 'completed', subagentDisplayName(data));
|
|
519
561
|
const stats = formatSubagentStats(data);
|
|
520
562
|
if (stats) actions.push(activityAction(truncate(`Finished · ${stats}`), runId));
|
|
@@ -525,6 +567,11 @@ export function createCopilotEventNormalizer() {
|
|
|
525
567
|
if (!runId) return [];
|
|
526
568
|
const name = subagentDisplayName(data);
|
|
527
569
|
const reason = String(data.error || '').trim() || 'unknown error';
|
|
570
|
+
// A background agent's failure still deserves a main-thread line (the
|
|
571
|
+
// parent agent recovers silently), but there is no lane to close.
|
|
572
|
+
if (isBackgroundRun(runId, subagentEventKeys(event).agentId)) {
|
|
573
|
+
return [activityAction(truncate(`Background agent failed (${name}): ${reason}`))];
|
|
574
|
+
}
|
|
528
575
|
const actions = closeSubagentRun(runId, 'failed', name);
|
|
529
576
|
actions.push(activityAction(truncate(`Subagent failed: ${reason}`), runId));
|
|
530
577
|
// ALSO on the main thread. The parent agent recovers from a failed
|
|
@@ -551,6 +598,14 @@ export function createCopilotEventNormalizer() {
|
|
|
551
598
|
if (isSubagentEvent(event)) {
|
|
552
599
|
const runId = resolveSubagentRun(event);
|
|
553
600
|
if (!runId) return [];
|
|
601
|
+
// A background agent's traffic belongs to its task card (the runner
|
|
602
|
+
// tracks its live tool call and tokens from the same events), not to a
|
|
603
|
+
// lane in this reply — but its model calls still cost real money, so
|
|
604
|
+
// they count toward the spend of whichever turn is live, as before.
|
|
605
|
+
if (isBackgroundRun(runId, subagentEventKeys(event).agentId)) {
|
|
606
|
+
if (type === 'assistant.usage') usage.add(data, { allowModel: false });
|
|
607
|
+
return [];
|
|
608
|
+
}
|
|
554
609
|
if (type === 'assistant.message') {
|
|
555
610
|
const content = String(data.content || '');
|
|
556
611
|
if (!content.trim()) return [];
|
|
@@ -718,7 +773,8 @@ export function createCopilotEventNormalizer() {
|
|
|
718
773
|
lastEmittedStreamText = text;
|
|
719
774
|
return [{ channel: 'stream', payload: { text, done: false, subagentRunId: null } }];
|
|
720
775
|
}
|
|
721
|
-
|
|
776
|
+
const toolLine = formatToolActivityText(toolName, data.arguments);
|
|
777
|
+
return [toolName === 'read_agent' ? bookkeepingActivityAction(toolLine) : activityAction(toolLine)];
|
|
722
778
|
}
|
|
723
779
|
case 'tool.execution_complete': {
|
|
724
780
|
// There is no `tool.*_failed` event type — failure is `success: false`
|
|
@@ -743,9 +799,9 @@ export function createCopilotEventNormalizer() {
|
|
|
743
799
|
const summary = permissionRequests.get(requestId) || '';
|
|
744
800
|
permissionRequests.delete(requestId);
|
|
745
801
|
const kind = String(data.result?.kind || 'unknown').trim();
|
|
746
|
-
|
|
747
|
-
|
|
748
|
-
|
|
802
|
+
const line = truncate(summary ? `Permission ${kind}: ${summary}` : `Permission ${kind}`);
|
|
803
|
+
// A denial is news; a grant is bookkeeping for the call it unblocked.
|
|
804
|
+
return [kind.startsWith('approved') ? bookkeepingActivityAction(line) : activityAction(line)];
|
|
749
805
|
}
|
|
750
806
|
case 'user_input.requested': {
|
|
751
807
|
// The question is forwarded to the relay as a question card; this row
|
|
@@ -791,7 +847,40 @@ export function createCopilotEventNormalizer() {
|
|
|
791
847
|
lastEmittedStreamText = text;
|
|
792
848
|
return [{ channel: 'stream', payload: { text, done: false, subagentRunId: null } }];
|
|
793
849
|
}
|
|
794
|
-
// ----
|
|
850
|
+
// ---- compaction ------------------------------------------------------
|
|
851
|
+
//
|
|
852
|
+
// Surfaced as activity lines so a long pause reads as what it is. The
|
|
853
|
+
// runner ALSO watches these two events directly for its steering hold
|
|
854
|
+
// (a message must not be pushed into a compaction's replay).
|
|
855
|
+
case 'session.compaction_start': {
|
|
856
|
+
return [activityAction('Compacting the conversation context…')];
|
|
857
|
+
}
|
|
858
|
+
case 'session.compaction_complete': {
|
|
859
|
+
if (data.success === false) {
|
|
860
|
+
const reason = String(data.error || '').trim();
|
|
861
|
+
return [activityAction(truncate(reason ? `Context compaction failed: ${reason}` : 'Context compaction failed'))];
|
|
862
|
+
}
|
|
863
|
+
const removed = Number(data.messagesRemoved);
|
|
864
|
+
const tokens = Number(data.tokensRemoved);
|
|
865
|
+
const parts = [];
|
|
866
|
+
if (Number.isFinite(removed) && removed > 0) parts.push(`${removed} messages`);
|
|
867
|
+
if (Number.isFinite(tokens) && tokens > 0) parts.push(`${Math.round(tokens / 1000)}k tokens`);
|
|
868
|
+
return [activityAction(parts.length ? `Context compacted (removed ${parts.join(', ')})` : 'Context compacted')];
|
|
869
|
+
}
|
|
870
|
+
// ---- the terminators ---------------------------------------------------
|
|
871
|
+
//
|
|
872
|
+
// `assistant.idle` is "the main agent's processing loop went idle" and
|
|
873
|
+
// fires even while background agents / attached shells keep the
|
|
874
|
+
// session-level idle deferred (runtime 1.0.88, live-probed 2026-09-25).
|
|
875
|
+
// It is the terminator that lets a row settle while a background agent
|
|
876
|
+
// the turn spawned keeps running under the task panel — exactly the
|
|
877
|
+
// Claude worker's lingering model. `session.idle` stays the hard
|
|
878
|
+
// terminator for runtimes that do not emit the assistant-level one and
|
|
879
|
+
// for the drain's end; `terminalEmitted` makes the second a no-op.
|
|
880
|
+
case 'assistant.idle': {
|
|
881
|
+
if (data.aborted === true) aborted = true;
|
|
882
|
+
return terminalAction({ subtype: aborted ? 'aborted' : 'completed' });
|
|
883
|
+
}
|
|
795
884
|
case 'session.idle': {
|
|
796
885
|
if (data.aborted === true) aborted = true;
|
|
797
886
|
return terminalAction({ subtype: aborted ? 'aborted' : 'completed' });
|