@oar-sh/oar 0.9.9 → 0.9.11
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 +561 -1
- package/README.md +162 -909
- package/bin/bat/oar.bat +46 -46
- package/package.json +14 -14
- package/server/README.md +369 -13
- package/server/claude-worker/claude-remote-relay-tool.mjs +10 -0
- package/server/claude-worker/claude-session-process.mjs +121 -9
- package/server/claude-worker/claude-session-worker.mjs +9 -1
- package/server/claude-worker/claude-turn-publisher.mjs +3 -0
- package/server/claude-worker/sdk-message-normalizer.mjs +31 -4
- package/server/claude-worker/subagent-timeline-recorder.mjs +572 -0
- package/server/copilot-worker/copilot-sdk-session-process.mjs +79 -12
- package/server/copilot-worker/copilot-sdk-session-worker.mjs +6 -0
- package/server/cursor-worker/cursor-sdk-adapter.mjs +56 -2
- package/server/db-schema.mjs +95 -2
- package/server/grok-worker/acp-client.mjs +2 -1
- package/server/grok-worker/acp-host-services.mjs +4 -3
- package/server/migrations/0009-subagent-timeline.mjs +135 -0
- package/server/migrations/0010-subagent-timeline-pruning.mjs +89 -0
- package/server/migrations/0011-relay-event-subscriptions.mjs +125 -0
- package/server/migrations/0012-cross-relay-event-subscriptions.mjs +116 -0
- package/server/migrations/0013-previews.mjs +130 -0
- package/server/migrations/0014-conversation-groups.mjs +89 -0
- package/server/migrations/0015-conversation-group-position.mjs +76 -0
- package/server/migrations/0016-share-access-log.mjs +110 -0
- package/server/public/app/about-ui.js +73 -0
- package/server/public/app/about-view.mjs +234 -0
- package/server/public/app/action-confirmations.js +31 -14
- package/server/public/app/agent-exchange-fold-view.mjs +187 -0
- package/server/public/app/api-client.js +312 -16
- package/server/public/app/archive-confirm.mjs +28 -0
- package/server/public/app/ask-user-view.js +17 -12
- package/server/public/app/attachments-view.js +516 -6
- package/server/public/app/background-tasks-view.mjs +18 -26
- package/server/public/app/bootstrap.js +336 -59
- package/server/public/app/chat-actions-menu-state.mjs +126 -0
- package/server/public/app/claude-cloud-conversation-ui.js +10 -22
- package/server/public/app/composer-control-state.mjs +16 -0
- package/server/public/app/composer-keyboard.mjs +54 -0
- package/server/public/app/context-menu.mjs +90 -6
- package/server/public/app/conversation-group-drag.mjs +191 -0
- package/server/public/app/conversation-group-order.mjs +265 -0
- package/server/public/app/conversation-group-state.mjs +100 -0
- package/server/public/app/conversation-list-drag.mjs +449 -0
- package/server/public/app/conversation-turn-state.mjs +81 -0
- package/server/public/app/conversation-view.js +436 -117
- package/server/public/app/cwd-picker.js +1 -1
- package/server/public/app/explorer-edit-state.mjs +68 -0
- package/server/public/app/hidden-messages-view.mjs +312 -0
- package/server/public/app/host-suspend-ui.js +13 -4
- package/server/public/app/journal-view.js +930 -45
- package/server/public/app/no-conversation-state.mjs +58 -0
- package/server/public/app/preview-cards.mjs +117 -31
- package/server/public/app/preview-command.mjs +10 -1
- package/server/public/app/provider-readiness.mjs +120 -0
- package/server/public/app/relay-board-view.js +19 -3
- package/server/public/app/relay-event-subscriptions-ui.js +184 -0
- package/server/public/app/relay-event-view.mjs +234 -0
- package/server/public/app/relay-restart-error.mjs +16 -0
- package/server/public/app/settings-modal.js +93 -9
- package/server/public/app/settings-tabs.js +1 -1
- package/server/public/app/share-format.mjs +49 -0
- package/server/public/app/share-preview-marks.mjs +167 -0
- package/server/public/app/share-state.js +109 -0
- package/server/public/app/shared-conversations-settings.js +480 -0
- package/server/public/app/socket-handlers.js +137 -40
- package/server/public/app/store.js +115 -32
- package/server/public/app/subagent-row.mjs +138 -0
- package/server/public/app/subagent-timeline-view.mjs +186 -0
- package/server/public/app/subagent-viewer-watch.mjs +50 -0
- package/server/public/app/subagent-viewer.mjs +558 -0
- package/server/public/app/task-row-format.mjs +29 -0
- package/server/public/app/transcript-jump-bottom-view.mjs +464 -0
- package/server/public/app/transcript-jump-bottom.mjs +210 -0
- package/server/public/app/transcript-reload-freshness.mjs +25 -0
- package/server/public/app/transcript-viewport-anchor.mjs +140 -0
- package/server/public/app/update-ui.js +1 -1
- package/server/public/app/viewer-history.mjs +5 -3
- package/server/public/index.html +774 -12
- package/server/relay-tools.md +2 -1
- package/server/relay.mjs +2 -1
- package/server/repositories/conversation-group-repository.mjs +444 -0
- package/server/repositories/message-repository.mjs +30 -3
- package/server/repositories/question-repository.mjs +9 -3
- package/server/repositories/remote-relay-repository.mjs +6 -2
- package/server/repositories/session-repository.mjs +12 -37
- package/server/repositories/share-repository.mjs +322 -0
- package/server/repositories/share-visibility-statements.mjs +10 -0
- package/server/repositories/subagent-timeline-statements.mjs +170 -0
- package/server/routes/about-routes.mjs +15 -0
- package/server/routes/ask-user-routes.mjs +3 -0
- package/server/routes/cache-routes.mjs +4 -0
- package/server/routes/conversation-group-routes.mjs +171 -0
- package/server/routes/messages-routes.mjs +698 -114
- package/server/routes/preview-routes.mjs +9 -1
- package/server/routes/relay-event-routes.mjs +157 -0
- package/server/routes/remote-relay-routes.mjs +15 -0
- package/server/routes/sessions-routes.mjs +843 -193
- package/server/server-runtime.mjs +559 -160
- package/server/services/about-service.mjs +461 -0
- package/server/services/background-task-store.mjs +128 -0
- package/server/services/background-work-view.mjs +228 -0
- package/server/services/cache-rebuild-service.mjs +30 -0
- package/server/services/delete-archive-service.mjs +4 -1
- package/server/services/detached-worker-watch-service.mjs +150 -0
- package/server/services/explorer-entry-name.mjs +26 -0
- package/server/services/explorer-mutations.mjs +122 -0
- package/server/services/hidden-message-list.mjs +21 -0
- package/server/services/host-suspend-command.mjs +2 -1
- package/server/services/message-pin-list.mjs +1 -1
- package/server/services/preview-health-probe.mjs +8 -1
- package/server/services/preview-proxy-server.mjs +49 -27
- package/server/services/preview-registry-service.mjs +676 -26
- package/server/services/preview-static-handler.mjs +40 -7
- package/server/services/preview-store.mjs +274 -0
- package/server/services/push-dispatch-service.mjs +18 -0
- package/server/services/rate-limit-service.mjs +12 -1
- package/server/services/relay-auth.mjs +183 -0
- package/server/services/relay-cli-launcher-service.mjs +3 -1
- package/server/services/relay-event-link-service.mjs +289 -0
- package/server/services/relay-event-service.mjs +1952 -0
- package/server/services/relay-shutdown-gate.mjs +31 -0
- package/server/services/remote-relay-caller-context.mjs +52 -7
- package/server/services/remote-relay-dispatcher.mjs +551 -7
- package/server/services/remote-relay-registry-service.mjs +11 -1
- package/server/services/request-boundary.mjs +183 -0
- package/server/services/session-activity-tracker.mjs +695 -0
- package/server/services/session-history-refresh-service.mjs +7 -0
- package/server/services/session-worker-launch-service.mjs +190 -6
- package/server/services/session-worker-process-service.mjs +10 -6
- package/server/services/share-access-log-service.mjs +123 -0
- package/server/services/share-path-redaction.mjs +424 -0
- package/server/services/stale-attempt-log-limiter.mjs +96 -0
- package/server/services/stale-recovery-clock-guard.mjs +57 -0
- package/server/services/stale-subagent-run-sweep.mjs +109 -0
- package/server/services/status-event-service.mjs +7 -3
- package/server/services/subagent-record-retention.mjs +164 -0
- package/server/services/subagent-timeline-service.mjs +328 -0
- package/server/services/subagent-viewer-rooms.mjs +119 -0
- package/server/services/tunnel-worker-path-guard.mjs +24 -41
- package/server/services/upgrade-socket-guard.mjs +16 -0
- package/server/services/windows-autostart-service.mjs +6 -1
- package/server/services/windows-boot-autostart-service.mjs +207 -23
- package/shared/preview-tool-core.mjs +15 -5
- package/shared/relay-events.mjs +371 -0
- package/shared/remote-relay-contract.mjs +160 -6
- package/shared/share-path-privacy.mjs +30 -0
- package/shared/subagent-record-retention.mjs +31 -0
- package/shared/subagent-timeline.mjs +79 -0
- package/shared/windows-system-tools.mjs +106 -0
- package/shared/worker-runtime/background-task-digest.mjs +30 -0
- package/shared/worker-runtime/heartbeat.mjs +24 -0
- package/shared/worker-runtime/kept-posts.mjs +83 -0
- package/shared/worker-runtime/worker-websocket-link.mjs +47 -15
|
@@ -38,10 +38,12 @@ import {
|
|
|
38
38
|
createClaudeTurnPublisher,
|
|
39
39
|
isStaleAttemptError,
|
|
40
40
|
} from './claude-turn-publisher.mjs';
|
|
41
|
+
import { createSubagentTimelineRecorder } from './subagent-timeline-recorder.mjs';
|
|
41
42
|
import { createAskUserBridge } from '../../shared/ask-user-bridge.mjs';
|
|
42
43
|
import { createRemoteRelayToolGate } from '../../shared/remote-relay-tool-core.mjs';
|
|
43
44
|
import { buildRelayStopFailure } from '../../shared/relay-stop-failure.mjs';
|
|
44
45
|
import { createTurnLiveness, resolveTurnStallWindows } from '../../shared/worker-runtime/turn-liveness.mjs';
|
|
46
|
+
import { createKeptPosts } from '../../shared/worker-runtime/kept-posts.mjs';
|
|
45
47
|
import {
|
|
46
48
|
ANSWERED_ELSEWHERE_KIND,
|
|
47
49
|
DELIVERY_ANSWERED_ELSEWHERE_TEXT,
|
|
@@ -49,6 +51,7 @@ import {
|
|
|
49
51
|
STEER_STOPPED_TEXT,
|
|
50
52
|
} from '../../shared/steer-settle-markers.mjs';
|
|
51
53
|
import { stripSteerNote, withSteerNoteContent } from '../../shared/steer-note.mjs';
|
|
54
|
+
import { isRelayEventDelivery } from '../../shared/relay-events.mjs';
|
|
52
55
|
import {
|
|
53
56
|
stripSlashCommandGuard,
|
|
54
57
|
stripSlashCommandGuardContent,
|
|
@@ -544,11 +547,19 @@ export function createClaudeSessionRunner({
|
|
|
544
547
|
// the socket link: a hold withdraws the relay's readiness, and its end
|
|
545
548
|
// re-arms it at once so a held message steers into the resumed turn.
|
|
546
549
|
onDeliveryReadinessChange = () => {},
|
|
550
|
+
// The relay turned this worker's newest task set away (`stale`): the
|
|
551
|
+
// worker asks for a heartbeat, on which the relay learns which process
|
|
552
|
+
// holds the session and asks for the set again.
|
|
553
|
+
onBackgroundTaskSetStale = () => {},
|
|
547
554
|
// Whether CLI processes carry the relay's `remote_relay` tool
|
|
548
555
|
// (shared/remote-relay-tool-core.mjs). Default: ask the relay.
|
|
549
556
|
remoteRelayToolGate = null,
|
|
557
|
+
// Records every sub-agent's frames as its timeline, run-scoped (see
|
|
558
|
+
// subagent-timeline-recorder.mjs). Injectable for tests.
|
|
559
|
+
subagentTimelineRecorder = null,
|
|
550
560
|
dbg = () => {},
|
|
551
561
|
} = {}) {
|
|
562
|
+
const timelineRecorder = subagentTimelineRecorder || createSubagentTimelineRecorder({ api, conversationId: sdkSessionId, dbg });
|
|
552
563
|
const publisher = createClaudeTurnPublisher({
|
|
553
564
|
api,
|
|
554
565
|
dbg,
|
|
@@ -598,6 +609,34 @@ export function createClaudeSessionRunner({
|
|
|
598
609
|
// id → terminalError for settling rows the worker gave up posting; reported
|
|
599
610
|
// in the heartbeat's `settleFailed` until the relay confirms it failed them.
|
|
600
611
|
const settleFailedTerminals = new Map();
|
|
612
|
+
// Settle notices of background tasks whose post failed (the relay was
|
|
613
|
+
// away): sent again after the next heartbeat it answers.
|
|
614
|
+
// Every post of the task set carries its place in this worker's order:
|
|
615
|
+
// two posts can overtake each other on the way, and the relay applies the
|
|
616
|
+
// newer one only. `epoch` tells this process from one before it.
|
|
617
|
+
const taskSetEpoch = Date.now();
|
|
618
|
+
let taskSetSeq = 0;
|
|
619
|
+
const nextTaskSetOrder = () => ({ epoch: taskSetEpoch, seq: ++taskSetSeq });
|
|
620
|
+
let taskSetStaleNotedAt = 0;
|
|
621
|
+
/**
|
|
622
|
+
* The relay's answer to a set post. `stale` for the newest one means the
|
|
623
|
+
* relay holds no current set of this worker; an older one was simply
|
|
624
|
+
* overtaken. At most one extra heartbeat per ten seconds: the periodic
|
|
625
|
+
* beat repeats the request anyway.
|
|
626
|
+
*/
|
|
627
|
+
function noteTaskSetAnswer(answer, order) {
|
|
628
|
+
if (answer?.stale !== true || order.seq !== taskSetSeq) return;
|
|
629
|
+
dbg('the relay dropped the newest background task set', answer.reason || '');
|
|
630
|
+
if (Date.now() - taskSetStaleNotedAt < 10_000) return;
|
|
631
|
+
taskSetStaleNotedAt = Date.now();
|
|
632
|
+
try { onBackgroundTaskSetStale(); } catch {}
|
|
633
|
+
}
|
|
634
|
+
const postTaskSet = (body) => {
|
|
635
|
+
const order = nextTaskSetOrder();
|
|
636
|
+
return api('POST', '/api/background-tasks', { conversationId: sdkSessionId, ...body, order })
|
|
637
|
+
.then((answer) => noteTaskSetAnswer(answer, order));
|
|
638
|
+
};
|
|
639
|
+
const keptTaskSettles = createKeptPosts({ api, dbg: (...args) => dbg('background task settle:', ...args) });
|
|
601
640
|
// Row id → the latest in-flight /api/queue-consumed POST touching it.
|
|
602
641
|
const consumedMarkChains = new Map();
|
|
603
642
|
|
|
@@ -1688,10 +1727,19 @@ export function createClaudeSessionRunner({
|
|
|
1688
1727
|
* which renders it as the composer's background-tasks panel. Membership
|
|
1689
1728
|
* changes post immediately; chatty progress updates are trailing-edge
|
|
1690
1729
|
* throttled. Advisory: failures must never disturb the stream consumer.
|
|
1730
|
+
*
|
|
1731
|
+
* `resync`: the relay asked for the set again on a heartbeat, because its
|
|
1732
|
+
* own set for this worker differs (it restarted, or a post was lost). The
|
|
1733
|
+
* same set, flagged, and "no tasks" is an answer too. Returns the post.
|
|
1691
1734
|
*/
|
|
1692
|
-
function publishBackgroundTasks({ throttled = false } = {}) {
|
|
1735
|
+
function publishBackgroundTasks({ throttled = false, resync = false } = {}) {
|
|
1693
1736
|
const processRef = proc;
|
|
1694
|
-
if (!processRef)
|
|
1737
|
+
if (!processRef) {
|
|
1738
|
+
if (!resync) return Promise.resolve();
|
|
1739
|
+
return postTaskSet({ tasks: [], resync: true }).catch((error) => {
|
|
1740
|
+
dbg('background task resync failed', error?.message || String(error));
|
|
1741
|
+
});
|
|
1742
|
+
}
|
|
1695
1743
|
const send = () => {
|
|
1696
1744
|
processRef.taskPublishTimer = null;
|
|
1697
1745
|
const tasks = [...processRef.liveTasks.entries()].map(([taskId, task]) => {
|
|
@@ -1723,7 +1771,7 @@ export function createClaudeSessionRunner({
|
|
|
1723
1771
|
...(task.workflowProgress ? { workflowProgress: task.workflowProgress } : {}),
|
|
1724
1772
|
};
|
|
1725
1773
|
});
|
|
1726
|
-
|
|
1774
|
+
return postTaskSet({ tasks, ...(resync ? { resync: true } : {}) }).catch((error) => {
|
|
1727
1775
|
dbg('background task publish failed', error?.message || String(error));
|
|
1728
1776
|
});
|
|
1729
1777
|
};
|
|
@@ -1732,12 +1780,12 @@ export function createClaudeSessionRunner({
|
|
|
1732
1780
|
clearTimeout(processRef.taskPublishTimer);
|
|
1733
1781
|
processRef.taskPublishTimer = null;
|
|
1734
1782
|
}
|
|
1735
|
-
send();
|
|
1736
|
-
return;
|
|
1783
|
+
return send();
|
|
1737
1784
|
}
|
|
1738
|
-
if (processRef.taskPublishTimer) return;
|
|
1785
|
+
if (processRef.taskPublishTimer) return Promise.resolve();
|
|
1739
1786
|
processRef.taskPublishTimer = setTimeout(send, 2_000);
|
|
1740
1787
|
processRef.taskPublishTimer.unref?.();
|
|
1788
|
+
return Promise.resolve();
|
|
1741
1789
|
}
|
|
1742
1790
|
|
|
1743
1791
|
// ---------------------------------------------------------------------------
|
|
@@ -2435,7 +2483,12 @@ export function createClaudeSessionRunner({
|
|
|
2435
2483
|
// capture a settling workflow's final digest for the transcript card
|
|
2436
2484
|
// (the notification then refines the status when it lands).
|
|
2437
2485
|
for (const [taskId, task] of previous) {
|
|
2438
|
-
if (
|
|
2486
|
+
if (proc.liveTasks.has(taskId)) continue;
|
|
2487
|
+
bufferSettledWorkflowRun(taskId, task);
|
|
2488
|
+
// What the task was, for its settle notice: the row is gone by the
|
|
2489
|
+
// time the notification says how it ended.
|
|
2490
|
+
proc.departedTasks.set(taskId, task);
|
|
2491
|
+
if (proc.departedTasks.size > 50) proc.departedTasks.delete(proc.departedTasks.keys().next().value);
|
|
2439
2492
|
}
|
|
2440
2493
|
syncWorkflowPoller(proc);
|
|
2441
2494
|
publishBackgroundTasks();
|
|
@@ -2481,6 +2534,29 @@ export function createClaudeSessionRunner({
|
|
|
2481
2534
|
// tree publishes before the row clears (best-effort: the CLI may have
|
|
2482
2535
|
// already removed the task via background_tasks_changed).
|
|
2483
2536
|
if (taskId) publishFinalWorkflowDigest(taskId, String(sdkMessage.status || '').trim());
|
|
2537
|
+
// How the task ended, for the relay's task status and the watchers of
|
|
2538
|
+
// this session (the live set only says it is gone). Ambient tasks are
|
|
2539
|
+
// not activity. Advisory: a failed post never disturbs the stream.
|
|
2540
|
+
if (taskId && sdkMessage.ambient !== true) {
|
|
2541
|
+
// A task that was in the published set is described too, so that a
|
|
2542
|
+
// relay which no longer knows it (it restarted meanwhile) can still
|
|
2543
|
+
// tell its watchers the end. A failed post is kept and sent again.
|
|
2544
|
+
const published = proc.liveTasks.get(taskId) || proc.departedTasks.get(taskId) || null;
|
|
2545
|
+
proc.departedTasks.delete(taskId);
|
|
2546
|
+
void keptTaskSettles.post('/api/background-task-settled', {
|
|
2547
|
+
conversationId: sdkSessionId,
|
|
2548
|
+
taskId,
|
|
2549
|
+
toolUseId: String(sdkMessage.tool_use_id || published?.toolUseId || '').trim() || null,
|
|
2550
|
+
status: String(sdkMessage.status || '').trim() || 'completed',
|
|
2551
|
+
summary: String(sdkMessage.summary || '').trim().slice(0, 500) || null,
|
|
2552
|
+
...(sdkMessage.reason ? { reason: String(sdkMessage.reason).slice(0, 40) } : {}),
|
|
2553
|
+
...(published?.description ? {
|
|
2554
|
+
description: String(published.description).slice(0, 500),
|
|
2555
|
+
taskType: String(published.taskType || ''),
|
|
2556
|
+
startedAt: published.startedAt || null,
|
|
2557
|
+
} : {}),
|
|
2558
|
+
});
|
|
2559
|
+
}
|
|
2484
2560
|
// A settled session-level task means the CLI is about to dequeue a
|
|
2485
2561
|
// continuation turn — the process must not idle out under it. The SDK
|
|
2486
2562
|
// flags silent notifications explicitly: skip_transcript means no
|
|
@@ -2681,6 +2757,9 @@ export function createClaudeSessionRunner({
|
|
|
2681
2757
|
}
|
|
2682
2758
|
|
|
2683
2759
|
async function handleSdkMessage(sdkMessage) {
|
|
2760
|
+
// Before any turn takes the frame: a background agent's frames arrive
|
|
2761
|
+
// between turns too, and belong to the run, not to whichever turn is open.
|
|
2762
|
+
timelineRecorder.observe(sdkMessage);
|
|
2684
2763
|
const usageLimitReport = usageLimit.observe(sdkMessage);
|
|
2685
2764
|
if (usageLimitReport) publisher.publishUsageLimitReport({ conversationId: sdkSessionId, sdkSessionId, report: usageLimitReport });
|
|
2686
2765
|
const suppressed = maybeSuppressStopFollowUp(sdkMessage);
|
|
@@ -2702,6 +2781,12 @@ export function createClaudeSessionRunner({
|
|
|
2702
2781
|
await settling;
|
|
2703
2782
|
}
|
|
2704
2783
|
if (!ctx) return;
|
|
2784
|
+
timelineRecorder.claimSpawns(sdkMessage, ctx);
|
|
2785
|
+
// A background agent spawned by an earlier turn: its frames are not this
|
|
2786
|
+
// turn's, and published here they would be filed under it. Its record
|
|
2787
|
+
// goes to its own run through the recorder above.
|
|
2788
|
+
const subagentParent = String(sdkMessage?.parent_tool_use_id || '').trim();
|
|
2789
|
+
if (subagentParent && timelineRecorder.isForeignRun(subagentParent, ctx)) return;
|
|
2705
2790
|
const actions = ctx.normalizer.normalize(sdkMessage);
|
|
2706
2791
|
for (const action of actions) {
|
|
2707
2792
|
// Decided here, while the reports the turn ran under are the latest.
|
|
@@ -3201,6 +3286,10 @@ export function createClaudeSessionRunner({
|
|
|
3201
3286
|
const wasCurrent = proc === processRef;
|
|
3202
3287
|
// The next spawn reads the registration decision; refresh it now.
|
|
3203
3288
|
if (wasCurrent) void remoteRelayGate.refresh();
|
|
3289
|
+
// Agents die with their process: their runs end, what streamed closes.
|
|
3290
|
+
if (wasCurrent) {
|
|
3291
|
+
void timelineRecorder.processEnded().catch((error) => dbg('subagent timeline flush failed', error?.message || String(error)));
|
|
3292
|
+
}
|
|
3204
3293
|
stopLifecycleTimer(processRef);
|
|
3205
3294
|
if (processRef.taskPublishTimer) {
|
|
3206
3295
|
clearTimeout(processRef.taskPublishTimer);
|
|
@@ -3217,7 +3306,7 @@ export function createClaudeSessionRunner({
|
|
|
3217
3306
|
if (processRef.liveTasks.size) {
|
|
3218
3307
|
processRef.liveTasks = new Map();
|
|
3219
3308
|
if (wasCurrent) {
|
|
3220
|
-
|
|
3309
|
+
postTaskSet({ tasks: [] }).catch(() => {});
|
|
3221
3310
|
}
|
|
3222
3311
|
}
|
|
3223
3312
|
// Steers pushed into a turn the user stopped: if the Stop fell back to
|
|
@@ -3299,6 +3388,9 @@ export function createClaudeSessionRunner({
|
|
|
3299
3388
|
permissionMode: permissionModeForRelayMode(relayMode),
|
|
3300
3389
|
liveTasks: new Map(),
|
|
3301
3390
|
knownTasks: new Set(),
|
|
3391
|
+
// taskId → the row of a task that left the live set, until its
|
|
3392
|
+
// notification arrives.
|
|
3393
|
+
departedTasks: new Map(),
|
|
3302
3394
|
// taskId → { runId, labels, digestJson } for live local_workflow tasks;
|
|
3303
3395
|
// pruned by syncWorkflowPoller when a task leaves the live set.
|
|
3304
3396
|
workflowStates: new Map(),
|
|
@@ -3913,7 +4005,9 @@ export function createClaudeSessionRunner({
|
|
|
3913
4005
|
// noted steer no longer starts with "/" and is pushed as noted. The
|
|
3914
4006
|
// replay signatures strip the guard too, so expectedText stays the
|
|
3915
4007
|
// user's own text here as well.
|
|
3916
|
-
|
|
4008
|
+
// Nor into an event message the relay wrote: the note speaks as the
|
|
4009
|
+
// user, the event must not (shared/relay-events.mjs).
|
|
4010
|
+
const pushedContent = withSlashCommandGuardContent(steered && procRef.activeCtx?.kind !== 'continuation' && !isRelayEventDelivery(message)
|
|
3917
4011
|
? withSteerNoteContent(content)
|
|
3918
4012
|
: content);
|
|
3919
4013
|
procRef.pendingDelivered.push({ ctx, expectedText: contentText(content), pushedAt: Date.now(), steered });
|
|
@@ -4000,6 +4094,21 @@ export function createClaudeSessionRunner({
|
|
|
4000
4094
|
return false;
|
|
4001
4095
|
}
|
|
4002
4096
|
|
|
4097
|
+
/** The ids of the live background tasks, for the heartbeat's digest. */
|
|
4098
|
+
function backgroundTaskIds() {
|
|
4099
|
+
return proc ? [...proc.liveTasks.keys()] : [];
|
|
4100
|
+
}
|
|
4101
|
+
|
|
4102
|
+
/** The relay asked for the task set again (heartbeat answer). */
|
|
4103
|
+
function resyncBackgroundTasks() {
|
|
4104
|
+
return publishBackgroundTasks({ resync: true });
|
|
4105
|
+
}
|
|
4106
|
+
|
|
4107
|
+
/** The relay answered a heartbeat: settle notices kept for it go out. */
|
|
4108
|
+
function flushKeptTaskSettles() {
|
|
4109
|
+
return keptTaskSettles.flush();
|
|
4110
|
+
}
|
|
4111
|
+
|
|
4003
4112
|
return {
|
|
4004
4113
|
handlePendingPayload,
|
|
4005
4114
|
canAcceptSteering,
|
|
@@ -4012,6 +4121,9 @@ export function createClaudeSessionRunner({
|
|
|
4012
4121
|
isTurnActive,
|
|
4013
4122
|
stopBackgroundTask,
|
|
4014
4123
|
stopBackgroundTaskByToolUseId,
|
|
4124
|
+
backgroundTaskIds,
|
|
4125
|
+
resyncBackgroundTasks,
|
|
4126
|
+
flushKeptTaskSettles,
|
|
4015
4127
|
shutdown,
|
|
4016
4128
|
// Test seams / observability.
|
|
4017
4129
|
_getProcess: () => proc,
|
|
@@ -119,6 +119,7 @@ async function main() {
|
|
|
119
119
|
// A readiness flip is a hold starting or ending (question card,
|
|
120
120
|
// compaction, a turn-opening delivery): the steering snapshot changed
|
|
121
121
|
// with it, so it goes out now rather than on the next 10 s heartbeat.
|
|
122
|
+
onBackgroundTaskSetStale: () => heartbeat?.requestPulse(),
|
|
122
123
|
onDeliveryReadinessChange: (ready) => {
|
|
123
124
|
linkBridge.onDeliveryReadinessChange(ready);
|
|
124
125
|
heartbeat?.requestPulse();
|
|
@@ -147,6 +148,12 @@ async function main() {
|
|
|
147
148
|
// Consumed rows the runner could not fail itself; the relay fails them.
|
|
148
149
|
getSettleFailed: () => turnRunner.getSettleFailed(),
|
|
149
150
|
onSettleFailedHandled: (ids) => turnRunner.acknowledgeSettleFailed(ids),
|
|
151
|
+
// The relay holds the task set in memory only: the digest of the live
|
|
152
|
+
// task ids rides every beat, and a relay that is out of step (it
|
|
153
|
+
// restarted, a post was lost) asks for the set again.
|
|
154
|
+
getBackgroundTaskIds: () => turnRunner.backgroundTaskIds(),
|
|
155
|
+
onResyncBackgroundTasks: () => turnRunner.resyncBackgroundTasks(),
|
|
156
|
+
onAnswered: () => turnRunner.flushKeptTaskSettles(),
|
|
150
157
|
});
|
|
151
158
|
|
|
152
159
|
const wsLink = createWorkerWebSocketLink({
|
|
@@ -164,7 +171,8 @@ async function main() {
|
|
|
164
171
|
// even between deliveries — a delivery drawn into it is only handed back.
|
|
165
172
|
getDeliveryHeld: () => turnRunner.isDeliveryHeld(),
|
|
166
173
|
// A relay that just (re)started knows nothing of this worker until it
|
|
167
|
-
// heartbeats; the composer's steering snapshot rides that beat
|
|
174
|
+
// heartbeats; the composer's steering snapshot rides that beat, and so
|
|
175
|
+
// does the digest that makes the relay ask for the background tasks.
|
|
168
176
|
onServerHello: () => heartbeat.requestPulse(),
|
|
169
177
|
getSessionId: () => sdkSessionId,
|
|
170
178
|
getPid: () => process.pid,
|
|
@@ -127,6 +127,9 @@ export function createClaudeTurnPublisher({ api, dbg = () => {}, takeWorkflowRun
|
|
|
127
127
|
parentSubagentId: payload.parentSubagentId || undefined,
|
|
128
128
|
displayName: payload.displayName || undefined,
|
|
129
129
|
status: payload.status,
|
|
130
|
+
...(payload.subagentType ? { subagentType: payload.subagentType } : {}),
|
|
131
|
+
...(payload.background ? { background: true } : {}),
|
|
132
|
+
...(payload.timelineOwner ? { timelineOwner: payload.timelineOwner } : {}),
|
|
130
133
|
...attemptFields(message),
|
|
131
134
|
}).catch(() => {});
|
|
132
135
|
return;
|
|
@@ -116,6 +116,23 @@ export function isSubagentToolName(name) {
|
|
|
116
116
|
return SUBAGENT_TOOL_NAMES.has(String(name || '').trim().toLowerCase());
|
|
117
117
|
}
|
|
118
118
|
|
|
119
|
+
const BACKGROUND_LAUNCH_STATUSES = new Set(['async_launched', 'remote_launched']);
|
|
120
|
+
|
|
121
|
+
/**
|
|
122
|
+
* The tool result of an Agent/Task spawn that went to the background: the
|
|
123
|
+
* structured result says so (`tool_use_result.status`), and the receipt text
|
|
124
|
+
* the model reads is the fallback for producers without it.
|
|
125
|
+
*/
|
|
126
|
+
export function isBackgroundLaunch(block, toolUseResult = null) {
|
|
127
|
+
const status = String(toolUseResult?.status || '').trim();
|
|
128
|
+
if (BACKGROUND_LAUNCH_STATUSES.has(status)) return true;
|
|
129
|
+
const content = block?.content;
|
|
130
|
+
const text = typeof content === 'string'
|
|
131
|
+
? content
|
|
132
|
+
: (Array.isArray(content) ? content.map((part) => String(part?.text || '')).join(' ') : '');
|
|
133
|
+
return /^\s*Async agent launched/i.test(text);
|
|
134
|
+
}
|
|
135
|
+
|
|
119
136
|
export function summarizeToolInput(toolName, input) {
|
|
120
137
|
if (!input || typeof input !== 'object') return '';
|
|
121
138
|
// `mcp__relay__remote_relay` reads as what it does on which relay
|
|
@@ -287,7 +304,8 @@ export function createSdkMessageNormalizer() {
|
|
|
287
304
|
|| 'Subagent',
|
|
288
305
|
).trim() || 'Subagent';
|
|
289
306
|
const parentSubagentId = subagentRunIdFor(parentToolUseId);
|
|
290
|
-
|
|
307
|
+
const subagentType = String(input.subagent_type || '').trim();
|
|
308
|
+
knownSubagentRuns.set(toolUseId, { displayName, parentSubagentId, subagentType });
|
|
291
309
|
actions.push({
|
|
292
310
|
channel: 'subagent',
|
|
293
311
|
payload: {
|
|
@@ -295,6 +313,10 @@ export function createSdkMessageNormalizer() {
|
|
|
295
313
|
parentSubagentId,
|
|
296
314
|
displayName,
|
|
297
315
|
status: 'running',
|
|
316
|
+
...(subagentType ? { subagentType } : {}),
|
|
317
|
+
// The worker's timeline recorder writes this run's record itself;
|
|
318
|
+
// the relay must not copy the lane frames into it as well.
|
|
319
|
+
timelineOwner: 'worker',
|
|
298
320
|
},
|
|
299
321
|
});
|
|
300
322
|
}
|
|
@@ -308,19 +330,24 @@ export function createSdkMessageNormalizer() {
|
|
|
308
330
|
return actions;
|
|
309
331
|
}
|
|
310
332
|
|
|
311
|
-
function actionsForToolResultBlock(block, parentToolUseId) {
|
|
333
|
+
function actionsForToolResultBlock(block, parentToolUseId, toolUseResult = null) {
|
|
312
334
|
const actions = [];
|
|
313
335
|
const toolUseId = String(block?.tool_use_id || '').trim();
|
|
314
336
|
const isError = block?.is_error === true;
|
|
315
337
|
if (toolUseId && knownSubagentRuns.has(toolUseId)) {
|
|
316
338
|
const run = knownSubagentRuns.get(toolUseId);
|
|
339
|
+
// A background launch answers its spawn at once with a receipt; the
|
|
340
|
+
// agent is only starting. Its end arrives later as a task
|
|
341
|
+
// notification, which the worker's timeline recorder reports.
|
|
342
|
+
const launchedInBackground = !isError && isBackgroundLaunch(block, toolUseResult);
|
|
317
343
|
actions.push({
|
|
318
344
|
channel: 'subagent',
|
|
319
345
|
payload: {
|
|
320
346
|
subagentRunId: toolUseId,
|
|
321
347
|
parentSubagentId: run.parentSubagentId,
|
|
322
348
|
displayName: run.displayName,
|
|
323
|
-
status: isError ? 'failed' : 'completed',
|
|
349
|
+
status: launchedInBackground ? 'running' : (isError ? 'failed' : 'completed'),
|
|
350
|
+
...(launchedInBackground ? { background: true } : {}),
|
|
324
351
|
},
|
|
325
352
|
});
|
|
326
353
|
}
|
|
@@ -574,7 +601,7 @@ export function createSdkMessageNormalizer() {
|
|
|
574
601
|
const content = Array.isArray(sdkMessage?.message?.content) ? sdkMessage.message.content : [];
|
|
575
602
|
for (const block of content) {
|
|
576
603
|
if (block?.type === 'tool_result') {
|
|
577
|
-
actions.push(...actionsForToolResultBlock(block, parentToolUseId));
|
|
604
|
+
actions.push(...actionsForToolResultBlock(block, parentToolUseId, sdkMessage?.tool_use_result || null));
|
|
578
605
|
}
|
|
579
606
|
}
|
|
580
607
|
return actions;
|