@oar-sh/oar 0.9.0
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/README.md +190 -0
- package/.github/extensions/web-relay/extension.mjs +972 -0
- package/.github/extensions/web-relay/model-api/model-switching.mjs +338 -0
- package/.github/extensions/web-relay/polling/heartbeat.mjs +55 -0
- package/.github/extensions/web-relay/polling/polling-loop.mjs +1105 -0
- package/.github/extensions/web-relay/runtime/api-client.mjs +34 -0
- package/.github/extensions/web-relay/runtime/banner-state.mjs +126 -0
- package/.github/extensions/web-relay/runtime/config-loader.mjs +104 -0
- package/.github/extensions/web-relay/runtime/debug-log.mjs +27 -0
- package/.github/extensions/web-relay/runtime/send-and-wait-errors.mjs +170 -0
- package/.github/extensions/web-relay/runtime/session-binding.mjs +78 -0
- package/.github/extensions/web-relay/runtime/session-io.mjs +349 -0
- package/.github/extensions/web-relay/runtime/session-join-retry.mjs +44 -0
- package/.github/extensions/web-relay/runtime/session-registry.mjs +29 -0
- package/.github/extensions/web-relay/runtime/session-runtime-manager.mjs +156 -0
- package/.github/extensions/web-relay/runtime/session-sync-bridge.mjs +74 -0
- package/.github/extensions/web-relay/runtime/worker-websocket-link.mjs +325 -0
- package/.github/extensions/web-relay/runtime/workspace-root.mjs +55 -0
- package/.github/extensions/web-relay/server-lifecycle/managed-server.mjs +386 -0
- package/.github/extensions/web-relay/skills/prompt-context.mjs +160 -0
- package/.github/extensions/web-relay/skills/question-bridge.mjs +141 -0
- package/.github/extensions/web-relay/skills/question-routing-hooks.mjs +463 -0
- package/.github/extensions/web-relay/skills/reasoning-stream.mjs +285 -0
- package/.github/extensions/web-relay/skills/subagent-lifecycle.mjs +345 -0
- package/.github/extensions/web-relay/skills/tool-activity.mjs +243 -0
- package/.github/extensions/web-relay/utils/process-utils.mjs +25 -0
- package/.github/extensions/web-relay/utils/token-generation.mjs +58 -0
- package/CHANGELOG.md +25 -0
- package/LICENSE +21 -0
- package/README.md +717 -0
- package/bin/bat/oar.bat +46 -0
- package/bin/oar.js +665 -0
- package/package.json +68 -0
- package/server/README.md +1379 -0
- package/server/claude-worker/claude-attachments.mjs +134 -0
- package/server/claude-worker/claude-preview-tool.mjs +57 -0
- package/server/claude-worker/claude-sdk-adapter.mjs +420 -0
- package/server/claude-worker/claude-session-process.mjs +2391 -0
- package/server/claude-worker/claude-session-test-harness.mjs +236 -0
- package/server/claude-worker/claude-session-worker.mjs +186 -0
- package/server/claude-worker/claude-transcript-relocator.mjs +189 -0
- package/server/claude-worker/claude-turn-publisher.mjs +291 -0
- package/server/claude-worker/fixtures/workflow-run-record.json +1 -0
- package/server/claude-worker/sdk-message-normalizer.mjs +629 -0
- package/server/claude-worker/workflow-progress-digest.mjs +269 -0
- package/server/copilot-sdk-runtime.mjs +126 -0
- package/server/copilot-worker/copilot-attachments.mjs +97 -0
- package/server/copilot-worker/copilot-byok-provider.mjs +121 -0
- package/server/copilot-worker/copilot-continuation-signals.mjs +349 -0
- package/server/copilot-worker/copilot-plan-board.mjs +110 -0
- package/server/copilot-worker/copilot-prompt-context.mjs +86 -0
- package/server/copilot-worker/copilot-question-bridge.mjs +216 -0
- package/server/copilot-worker/copilot-sdk-adapter.mjs +494 -0
- package/server/copilot-worker/copilot-sdk-event-normalizer.mjs +800 -0
- package/server/copilot-worker/copilot-sdk-session-process.mjs +1729 -0
- package/server/copilot-worker/copilot-sdk-session-worker.mjs +167 -0
- package/server/copilot-worker/copilot-sdk-test-harness.mjs +329 -0
- package/server/copilot-worker/fixtures/abort-turn.json +588 -0
- package/server/copilot-worker/fixtures/ask-user-turn.json +1154 -0
- package/server/copilot-worker/fixtures/background-timer-continuation.json +445 -0
- package/server/copilot-worker/fixtures/background-timer-turn.json +497 -0
- package/server/copilot-worker/fixtures/happy-turn.json +566 -0
- package/server/copilot-worker/fixtures/quota-turn.json +647 -0
- package/server/copilot-worker/fixtures/reasoning-turn.json +625 -0
- package/server/copilot-worker/fixtures/resume-turn.json +700 -0
- package/server/copilot-worker/fixtures/subagent-turn.json +1378 -0
- package/server/copilot-worker/fixtures/tool-permission-turn.json +1293 -0
- package/server/cursor-worker/cursor-ask-user-tool.mjs +83 -0
- package/server/cursor-worker/cursor-attachments.mjs +88 -0
- package/server/cursor-worker/cursor-context-usage.mjs +85 -0
- package/server/cursor-worker/cursor-preview-tool.mjs +44 -0
- package/server/cursor-worker/cursor-sdk-adapter.mjs +554 -0
- package/server/cursor-worker/cursor-session-worker.mjs +145 -0
- package/server/cursor-worker/cursor-subagent-roster.mjs +81 -0
- package/server/cursor-worker/cursor-turn-runner.mjs +717 -0
- package/server/cursor-worker/sdk-message-normalizer.mjs +485 -0
- package/server/db-schema.mjs +932 -0
- package/server/features.mjs +106 -0
- package/server/grok-worker/acp-client.mjs +434 -0
- package/server/grok-worker/acp-host-services.mjs +277 -0
- package/server/grok-worker/grok-context-usage.mjs +77 -0
- package/server/grok-worker/grok-sdk-adapter.mjs +334 -0
- package/server/grok-worker/grok-session-worker.mjs +145 -0
- package/server/grok-worker/grok-turn-runner.mjs +523 -0
- package/server/grok-worker/sdk-message-normalizer.mjs +311 -0
- package/server/migrations/0001-add-structured-answer.mjs +80 -0
- package/server/migrations/0002-recent-workspace-roots-path-key.mjs +121 -0
- package/server/migrations/0003-push-subscriptions.mjs +87 -0
- package/server/public/app/action-confirmations.js +267 -0
- package/server/public/app/activity-replay-state.mjs +149 -0
- package/server/public/app/api-client.js +1082 -0
- package/server/public/app/ask-user-view.js +545 -0
- package/server/public/app/attachments-view.js +2112 -0
- package/server/public/app/auto-compact-window-options.mjs +59 -0
- package/server/public/app/background-tasks-view.mjs +494 -0
- package/server/public/app/bootstrap.js +4347 -0
- package/server/public/app/claude-auth-ui.js +394 -0
- package/server/public/app/claude-thinking-options.mjs +38 -0
- package/server/public/app/cli-install-ui.js +455 -0
- package/server/public/app/code-copy.mjs +85 -0
- package/server/public/app/composer-attachment-cache.mjs +151 -0
- package/server/public/app/composer-control-state.mjs +107 -0
- package/server/public/app/composer-paste.mjs +132 -0
- package/server/public/app/context-tier-options.mjs +68 -0
- package/server/public/app/context-usage-view.mjs +338 -0
- package/server/public/app/conversation-draft-timestamp-utils.mjs +11 -0
- package/server/public/app/conversation-preferences.mjs +91 -0
- package/server/public/app/conversation-provider-indicator.mjs +88 -0
- package/server/public/app/conversation-view.js +2898 -0
- package/server/public/app/cwd-menu-placement.mjs +85 -0
- package/server/public/app/cwd-picker.js +605 -0
- package/server/public/app/emoji-view.js +101 -0
- package/server/public/app/external-link-policy.mjs +80 -0
- package/server/public/app/file-preview-navigation.mjs +51 -0
- package/server/public/app/font-scaling.js +262 -0
- package/server/public/app/git-changes-view.js +339 -0
- package/server/public/app/git-diff-model.mjs +109 -0
- package/server/public/app/grok-auth-ui.js +387 -0
- package/server/public/app/image-downscale.mjs +174 -0
- package/server/public/app/in-flight-snapshot.mjs +40 -0
- package/server/public/app/infinite-loader.js +164 -0
- package/server/public/app/journal-view.js +1141 -0
- package/server/public/app/known-cwd-options.mjs +41 -0
- package/server/public/app/launch-cwd-path.mjs +23 -0
- package/server/public/app/live-message-dedupe.mjs +57 -0
- package/server/public/app/message-search-view.js +242 -0
- package/server/public/app/model-selector-options.mjs +31 -0
- package/server/public/app/model-vision-support.mjs +56 -0
- package/server/public/app/new-conversation-model-choice.mjs +67 -0
- package/server/public/app/plan-usage-view.mjs +307 -0
- package/server/public/app/preview-cards.mjs +273 -0
- package/server/public/app/preview-command.mjs +103 -0
- package/server/public/app/push-settings.js +370 -0
- package/server/public/app/pwa-install.js +456 -0
- package/server/public/app/question-schema-view.mjs +71 -0
- package/server/public/app/reasoning-effort-labels.mjs +42 -0
- package/server/public/app/relay-board-view.js +117 -0
- package/server/public/app/relay-dot-state.mjs +69 -0
- package/server/public/app/relay-error-ctas.mjs +52 -0
- package/server/public/app/relay-prompt-sanitizer.mjs +114 -0
- package/server/public/app/relay-thoughts.mjs +98 -0
- package/server/public/app/relay-watchdog-policy.mjs +29 -0
- package/server/public/app/repo-browser-preferences.mjs +79 -0
- package/server/public/app/repo-browser-tree-state.mjs +97 -0
- package/server/public/app/router.js +657 -0
- package/server/public/app/selection-guard.mjs +93 -0
- package/server/public/app/settings-modal.js +1484 -0
- package/server/public/app/settings-tabs.js +129 -0
- package/server/public/app/slash-autocomplete.mjs +188 -0
- package/server/public/app/slash-commands.mjs +183 -0
- package/server/public/app/socket-handlers.js +703 -0
- package/server/public/app/status-store.mjs +242 -0
- package/server/public/app/status-view.mjs +315 -0
- package/server/public/app/store.js +1454 -0
- package/server/public/app/stream-state.mjs +73 -0
- package/server/public/app/streaming-dom-patch.mjs +23 -0
- package/server/public/app/sync-outbox.mjs +208 -0
- package/server/public/app/tap-actions.js +62 -0
- package/server/public/app/thread-order.mjs +54 -0
- package/server/public/app/tmux-inspector-view.js +422 -0
- package/server/public/app/transcript-separators.mjs +282 -0
- package/server/public/app-icon-192.png +0 -0
- package/server/public/app-icon-512.png +0 -0
- package/server/public/app-icon.svg +4 -0
- package/server/public/favicon.ico +0 -0
- package/server/public/index.html +5743 -0
- package/server/public/manifest.webmanifest +32 -0
- package/server/public/sw.js +307 -0
- package/server/relay-exit-codes.mjs +4 -0
- package/server/relay-self-restart.mjs +170 -0
- package/server/relay-tools.md +27 -0
- package/server/relay.mjs +1026 -0
- package/server/repositories/image-conversation-repository.mjs +317 -0
- package/server/repositories/message-repository.mjs +434 -0
- package/server/repositories/question-repository.mjs +161 -0
- package/server/repositories/session-repository.mjs +320 -0
- package/server/repositories/share-visibility-statements.mjs +24 -0
- package/server/routes/ask-user-routes.mjs +292 -0
- package/server/routes/cache-routes.mjs +44 -0
- package/server/routes/git-routes.mjs +92 -0
- package/server/routes/messages-routes-test-harness.mjs +111 -0
- package/server/routes/messages-routes.mjs +7107 -0
- package/server/routes/preview-routes.mjs +108 -0
- package/server/routes/push-routes.mjs +59 -0
- package/server/routes/relay-board-routes.mjs +268 -0
- package/server/routes/sessions-routes.mjs +6150 -0
- package/server/server-runtime.mjs +7463 -0
- package/server/server.js +24 -0
- package/server/services/ask-user-routing-service.mjs +117 -0
- package/server/services/cache-rebuild-service.mjs +236 -0
- package/server/services/claude-auth-service.mjs +578 -0
- package/server/services/claude-context-usage.mjs +240 -0
- package/server/services/claude-session-root-service.mjs +228 -0
- package/server/services/cli-install-service.mjs +1152 -0
- package/server/services/cli-process-runner.mjs +181 -0
- package/server/services/cloudflared-tunnel-service.mjs +358 -0
- package/server/services/context-snapshot-service.mjs +499 -0
- package/server/services/context-usage-view.mjs +160 -0
- package/server/services/conversation-preferences-service.mjs +208 -0
- package/server/services/cursor-dashboard-usage.mjs +217 -0
- package/server/services/cursor-session-root-service.mjs +62 -0
- package/server/services/delete-archive-service.mjs +154 -0
- package/server/services/drives-path-helpers.mjs +66 -0
- package/server/services/fixtures/claude-auth-stub.sh +62 -0
- package/server/services/fixtures/cli-install-stub.sh +75 -0
- package/server/services/fixtures/grok-stub.sh +86 -0
- package/server/services/generated-image-cleanup-service.mjs +114 -0
- package/server/services/git-changes-service.mjs +195 -0
- package/server/services/github-billing-usage.mjs +133 -0
- package/server/services/grok-auth-service.mjs +593 -0
- package/server/services/grok-billing-usage.mjs +120 -0
- package/server/services/image-context-service.mjs +106 -0
- package/server/services/image-operation-service.mjs +135 -0
- package/server/services/keyed-mutex-service.mjs +82 -0
- package/server/services/mime-sniffer.mjs +118 -0
- package/server/services/oar-cli-helpers.mjs +109 -0
- package/server/services/oar-state-migration-service.mjs +243 -0
- package/server/services/plan-usage-claude.mjs +387 -0
- package/server/services/plan-usage-contract.mjs +271 -0
- package/server/services/plan-usage-copilot.mjs +421 -0
- package/server/services/plan-usage-cursor.mjs +566 -0
- package/server/services/plan-usage-grok.mjs +436 -0
- package/server/services/plan-usage-service.mjs +487 -0
- package/server/services/preview-health-probe.mjs +82 -0
- package/server/services/preview-proxy-server.mjs +498 -0
- package/server/services/preview-registry-service.mjs +457 -0
- package/server/services/preview-static-handler.mjs +237 -0
- package/server/services/provider-model-selection.mjs +91 -0
- package/server/services/provider-reasoning-effort.mjs +94 -0
- package/server/services/push-active-device-service.mjs +57 -0
- package/server/services/push-dispatch-service.mjs +459 -0
- package/server/services/rate-limit-service.mjs +63 -0
- package/server/services/relay-bridge-owner-service.mjs +84 -0
- package/server/services/relay-cli-launcher-service.mjs +209 -0
- package/server/services/relay-prompt-sanitizer.mjs +137 -0
- package/server/services/relay-queue-gate-service.mjs +38 -0
- package/server/services/relay-restart-orchestrator-service.mjs +935 -0
- package/server/services/relay-singleton-guard.mjs +124 -0
- package/server/services/safe-served-content.mjs +57 -0
- package/server/services/sdk-session-import-service.mjs +271 -0
- package/server/services/sdk-session-sync-service.mjs +207 -0
- package/server/services/session-history-refresh-service.mjs +497 -0
- package/server/services/session-transcript-service.mjs +419 -0
- package/server/services/session-worker-launch-service.mjs +786 -0
- package/server/services/session-worker-process-service.mjs +319 -0
- package/server/services/session-worker-registry-service.mjs +194 -0
- package/server/services/session-worker-stop-service.mjs +141 -0
- package/server/services/session-worker-supervisor-service.mjs +898 -0
- package/server/services/session-worker-websocket-service.mjs +483 -0
- package/server/services/ssh-tunnel-manager-service.mjs +423 -0
- package/server/services/status-event-service.mjs +158 -0
- package/server/services/tmux-inspector-access-policy.mjs +77 -0
- package/server/services/tmux-inspector-socket-service.mjs +191 -0
- package/server/services/tmux-inspector-stream-service.mjs +552 -0
- package/server/services/tunnel-worker-path-guard.mjs +107 -0
- package/server/services/upload-sweep.mjs +60 -0
- package/server/services/usage-snapshot-helpers.mjs +209 -0
- package/server/services/windows-autostart-service.mjs +115 -0
- package/server/services/workspace-root-defaults-service.mjs +34 -0
- package/server/services/workspace-root-path-policy.mjs +196 -0
- package/server/services/workspace-root-relaunch-service.mjs +132 -0
- package/server/services/workspace-symlink-guard.mjs +54 -0
- package/server/tools/capture-proxy.mjs +173 -0
- package/server/tty-console-bootstrap.mjs +75 -0
- package/server/uploads/README.md +36 -0
- package/server/uploads/image_input.md +540 -0
- package/server/windows-terminal-launcher.mjs +29 -0
- package/server/workspace-root.mjs +87 -0
- package/shared/ask-user-bridge.mjs +131 -0
- package/shared/auto-compact-window.mjs +116 -0
- package/shared/background-task-timeout.mjs +52 -0
- package/shared/claude-thinking.mjs +101 -0
- package/shared/context-window-fallbacks.mjs +148 -0
- package/shared/control-poller.mjs +115 -0
- package/shared/empty-turn-completion.mjs +20 -0
- package/shared/model-catalog-freshness.mjs +11 -0
- package/shared/model-descriptors.mjs +118 -0
- package/shared/model-id.mjs +147 -0
- package/shared/model-refresh.mjs +24 -0
- package/shared/openai-reasoning.mjs +13 -0
- package/shared/plan-lines.mjs +13 -0
- package/shared/preview-instructions.mjs +77 -0
- package/shared/preview-tool-core.mjs +191 -0
- package/shared/provider-routing.mjs +29 -0
- package/shared/question-schema.mjs +315 -0
- package/shared/question-timeout.mjs +18 -0
- package/shared/stream-emit-gating.mjs +28 -0
- package/shared/subagent-run-id.mjs +17 -0
- package/shared/thought-cap.mjs +13 -0
- package/shared/tool-activity.mjs +98 -0
- package/shared/turn-ceiling.mjs +53 -0
- package/shared/worker-bootstrap.mjs +44 -0
- package/shared/worker-crash-guard.mjs +60 -0
|
@@ -0,0 +1,629 @@
|
|
|
1
|
+
import { capThought } from '../../shared/thought-cap.mjs';
|
|
2
|
+
import { shouldEmitStreamUpdate } from '../../shared/stream-emit-gating.mjs';
|
|
3
|
+
|
|
4
|
+
const MAX_TOOL_DETAIL_LENGTH = 140;
|
|
5
|
+
const REDACTED_THINKING_PLACEHOLDER = '[Reasoning redacted by the model provider]';
|
|
6
|
+
const SUBAGENT_TOOL_NAMES = new Set(['task', 'agent']);
|
|
7
|
+
|
|
8
|
+
function truncate(text, maxLength = MAX_TOOL_DETAIL_LENGTH) {
|
|
9
|
+
const value = String(text || '').trim();
|
|
10
|
+
if (value.length <= maxLength) return value;
|
|
11
|
+
return `${value.slice(0, Math.max(0, maxLength - 1))}…`;
|
|
12
|
+
}
|
|
13
|
+
|
|
14
|
+
function formatCompactTokens(value) {
|
|
15
|
+
const n = Number(value);
|
|
16
|
+
if (!Number.isFinite(n)) return '';
|
|
17
|
+
if (Math.abs(n) < 1000) return String(Math.round(n));
|
|
18
|
+
return `${(n / 1000).toFixed(1).replace(/\.0$/, '')}k`;
|
|
19
|
+
}
|
|
20
|
+
|
|
21
|
+
/**
|
|
22
|
+
* One user-readable line for a `system`/`api_retry` stream message. Exported
|
|
23
|
+
* because the session process needs the identical wording for retries that
|
|
24
|
+
* arrive between turns (carried into the next context as a pending activity).
|
|
25
|
+
*/
|
|
26
|
+
export function formatApiRetryNotice(sdkMessage) {
|
|
27
|
+
const status = Number(sdkMessage?.error_status) || 0;
|
|
28
|
+
const attempt = Number(sdkMessage?.attempt) || 0;
|
|
29
|
+
const max = Number(sdkMessage?.max_retries) || 0;
|
|
30
|
+
const delaySec = Math.round((Number(sdkMessage?.retry_delay_ms) || 0) / 1000);
|
|
31
|
+
const cause = status === 529
|
|
32
|
+
? 'Anthropic API overloaded (529)'
|
|
33
|
+
: `Anthropic API error${status ? ` (${status})` : ''}`;
|
|
34
|
+
const counter = attempt && max ? ` ${attempt}/${max}` : '';
|
|
35
|
+
const wait = delaySec > 0 ? ` in ~${delaySec}s` : '';
|
|
36
|
+
return `${cause} — retrying${counter}${wait}…`;
|
|
37
|
+
}
|
|
38
|
+
|
|
39
|
+
/**
|
|
40
|
+
* The single activity action a `compact_boundary` publishes. Exported because
|
|
41
|
+
* a compaction at resume arrives with no turn context — the session process
|
|
42
|
+
* buffers this same action onto the next one rather than losing the boundary
|
|
43
|
+
* (the per-turn normalizer never sees it).
|
|
44
|
+
*
|
|
45
|
+
* `post_tokens` is optional in the SDK type and absent from every auto-compact
|
|
46
|
+
* payload observed live, so the metadata degrades to a pre-only row instead of
|
|
47
|
+
* being dropped. The wire shape is snake_case under `compact_metadata` (the
|
|
48
|
+
* CLI converts its internal camelCase before yielding); the camelCase read is
|
|
49
|
+
* a cheap hedge, since the on-disk transcript uses that spelling.
|
|
50
|
+
*/
|
|
51
|
+
export function compactBoundaryActivityAction(sdkMessage) {
|
|
52
|
+
const metadata = sdkMessage?.compact_metadata || sdkMessage?.compactMetadata || {};
|
|
53
|
+
const preTokens = Number(metadata.pre_tokens ?? metadata.preTokens);
|
|
54
|
+
const postTokens = Number(metadata.post_tokens ?? metadata.postTokens);
|
|
55
|
+
const hasPre = Number.isFinite(preTokens);
|
|
56
|
+
const hasPost = Number.isFinite(postTokens);
|
|
57
|
+
let detail = '';
|
|
58
|
+
if (hasPre && hasPost) detail = ` (${formatCompactTokens(preTokens)} → ${formatCompactTokens(postTokens)} tokens)`;
|
|
59
|
+
else if (hasPre) detail = ` (was ${formatCompactTokens(preTokens)} tokens)`;
|
|
60
|
+
return {
|
|
61
|
+
channel: 'activity',
|
|
62
|
+
payload: {
|
|
63
|
+
text: `Context compacted${detail}`,
|
|
64
|
+
subagentRunId: null,
|
|
65
|
+
// Structured twin of the prose: the transcript promotes this row to a
|
|
66
|
+
// full-width break instead of burying it in the tool-activity details,
|
|
67
|
+
// and needs the token counts to label it.
|
|
68
|
+
metadata: {
|
|
69
|
+
kind: 'compact_boundary',
|
|
70
|
+
preTokens: hasPre ? preTokens : null,
|
|
71
|
+
postTokens: hasPost ? postTokens : null,
|
|
72
|
+
},
|
|
73
|
+
},
|
|
74
|
+
};
|
|
75
|
+
}
|
|
76
|
+
|
|
77
|
+
// MCP tools reach the transcript namespaced as `mcp__<server>__<tool>`; which
|
|
78
|
+
// server carries a tool is relay plumbing, so the chip shows the bare name.
|
|
79
|
+
const MCP_TOOL_NAME = /^mcp__(?:.+?)__(.+)$/;
|
|
80
|
+
|
|
81
|
+
export function displayToolName(name) {
|
|
82
|
+
const value = String(name || '').trim();
|
|
83
|
+
return MCP_TOOL_NAME.exec(value)?.[1] || value;
|
|
84
|
+
}
|
|
85
|
+
|
|
86
|
+
export function isSubagentToolName(name) {
|
|
87
|
+
return SUBAGENT_TOOL_NAMES.has(String(name || '').trim().toLowerCase());
|
|
88
|
+
}
|
|
89
|
+
|
|
90
|
+
export function summarizeToolInput(toolName, input) {
|
|
91
|
+
if (!input || typeof input !== 'object') return '';
|
|
92
|
+
const candidates = [
|
|
93
|
+
input.command,
|
|
94
|
+
input.file_path,
|
|
95
|
+
input.path,
|
|
96
|
+
input.pattern,
|
|
97
|
+
input.description,
|
|
98
|
+
input.url,
|
|
99
|
+
input.query,
|
|
100
|
+
input.prompt,
|
|
101
|
+
];
|
|
102
|
+
for (const candidate of candidates) {
|
|
103
|
+
const value = String(candidate ?? '').trim();
|
|
104
|
+
if (value) return value;
|
|
105
|
+
}
|
|
106
|
+
try {
|
|
107
|
+
const serialized = JSON.stringify(input);
|
|
108
|
+
return serialized === '{}' ? '' : serialized;
|
|
109
|
+
} catch {
|
|
110
|
+
return '';
|
|
111
|
+
}
|
|
112
|
+
}
|
|
113
|
+
|
|
114
|
+
export function formatToolActivityText(toolName, input) {
|
|
115
|
+
const summary = summarizeToolInput(toolName, input);
|
|
116
|
+
return truncate(summary ? `Tool (${toolName}): ${summary}` : `Tool (${toolName})`);
|
|
117
|
+
}
|
|
118
|
+
|
|
119
|
+
// Re-exported (rather than defined here) so existing importers of this
|
|
120
|
+
// normalizer keep working; the rule itself lives in shared/.
|
|
121
|
+
export { shouldEmitStreamUpdate };
|
|
122
|
+
|
|
123
|
+
/**
|
|
124
|
+
* Stateful normalizer mapping Claude Agent SDK messages onto relay channel
|
|
125
|
+
* actions. One instance per turn.
|
|
126
|
+
*
|
|
127
|
+
* `normalize(sdkMessage)` returns an array of `{ channel, payload }` actions:
|
|
128
|
+
* - `init` → `{ sessionId, model }`
|
|
129
|
+
* - `stream` → `{ text, done, subagentRunId? }` (cumulative snapshots per thread)
|
|
130
|
+
* - `thought` → `{ reasoningId, text, done, subagentRunId? }` (thinking blocks
|
|
131
|
+
* and interim narration; ids are stable across partial/complete frames so the
|
|
132
|
+
* server upserts rather than duplicating)
|
|
133
|
+
* - `activity` → `{ text, subagentRunId? }`
|
|
134
|
+
* - `subagent` → `{ subagentRunId, parentSubagentId, displayName, status }`
|
|
135
|
+
* - `background_tasks` → `{ tasks: [{ taskId, taskType, description }] }`
|
|
136
|
+
* (REPLACE semantics: the full live set after each membership change)
|
|
137
|
+
* - `background_task_settled` → `{ taskId, status }`
|
|
138
|
+
* - `result` → `{ text, isError, subtype, sessionId, model, usage }`
|
|
139
|
+
*/
|
|
140
|
+
export function createSdkMessageNormalizer() {
|
|
141
|
+
const threads = new Map(); // threadKey -> per-thread stream/thinking bookkeeping
|
|
142
|
+
const knownSubagentRuns = new Map(); // toolUseId -> { displayName, parentSubagentId }
|
|
143
|
+
let initSessionId = '';
|
|
144
|
+
let initModel = '';
|
|
145
|
+
|
|
146
|
+
function threadKeyFor(parentToolUseId) {
|
|
147
|
+
return String(parentToolUseId || '').trim() || 'main';
|
|
148
|
+
}
|
|
149
|
+
|
|
150
|
+
function threadState(parentToolUseId) {
|
|
151
|
+
const key = threadKeyFor(parentToolUseId);
|
|
152
|
+
if (!threads.has(key)) {
|
|
153
|
+
threads.set(key, {
|
|
154
|
+
key,
|
|
155
|
+
streamText: '',
|
|
156
|
+
lastEmittedStreamText: '',
|
|
157
|
+
thinking: new Map(), // block index -> { reasoningId, text }
|
|
158
|
+
// The SDK delivers one complete `assistant` event PER CONTENT BLOCK
|
|
159
|
+
// (all sharing the message's API id, each arriving before that block's
|
|
160
|
+
// content_block_stop), so positional bookkeeping alone cannot pair a
|
|
161
|
+
// complete thinking block with its streamed frames. The streamed
|
|
162
|
+
// reasoning ids are recorded here per API message id and consumed in
|
|
163
|
+
// block order by the complete events — the server upserts thoughts by
|
|
164
|
+
// reasoningId, so the complete republish updates the streamed thought
|
|
165
|
+
// in place instead of duplicating it.
|
|
166
|
+
currentMessageId: '',
|
|
167
|
+
streamedThinkingIds: new Map(), // API message id -> [reasoningId, ...] in stream order
|
|
168
|
+
consumedThinkingIds: new Map(), // API message id -> count consumed by complete events
|
|
169
|
+
// Assistant-message counter for the thread; the fallback id key when
|
|
170
|
+
// partial frames (and thus recorded streamed ids) are unavailable.
|
|
171
|
+
messageIndex: 0,
|
|
172
|
+
// Set when `message_start` opens a message, so the complete `assistant`
|
|
173
|
+
// message reuses that index instead of allocating a new one. Without
|
|
174
|
+
// partial messages there is no `message_start`, and each complete
|
|
175
|
+
// message allocates its own index.
|
|
176
|
+
messageStarted: false,
|
|
177
|
+
// Which complete message the thread is currently inside, for the
|
|
178
|
+
// one-event-per-block delivery model.
|
|
179
|
+
lastCompleteMessageId: '',
|
|
180
|
+
currentComplete: null,
|
|
181
|
+
});
|
|
182
|
+
}
|
|
183
|
+
return threads.get(key);
|
|
184
|
+
}
|
|
185
|
+
|
|
186
|
+
function thoughtIdFor(state, kind, blockIndex) {
|
|
187
|
+
return `claude-${kind}-${state.key}-${state.messageIndex}-${blockIndex}`;
|
|
188
|
+
}
|
|
189
|
+
|
|
190
|
+
// Reasoning id for a thinking block on a complete assistant event: reuse the
|
|
191
|
+
// id its streamed frames already published (matched by API message id, in
|
|
192
|
+
// block order), falling back to a fresh positional id when no partial frames
|
|
193
|
+
// were seen for the message.
|
|
194
|
+
function completeThinkingIdFor(state, messageId, blockIndex) {
|
|
195
|
+
const streamedIds = messageId ? state.streamedThinkingIds.get(messageId) : null;
|
|
196
|
+
if (streamedIds?.length) {
|
|
197
|
+
const consumed = state.consumedThinkingIds.get(messageId) || 0;
|
|
198
|
+
if (consumed < streamedIds.length) {
|
|
199
|
+
state.consumedThinkingIds.set(messageId, consumed + 1);
|
|
200
|
+
return streamedIds[consumed];
|
|
201
|
+
}
|
|
202
|
+
}
|
|
203
|
+
return thoughtIdFor(state, 'thought', blockIndex);
|
|
204
|
+
}
|
|
205
|
+
|
|
206
|
+
// Called when a new assistant message begins on a thread, from whichever
|
|
207
|
+
// signal arrives first (`message_start`, or the complete message itself).
|
|
208
|
+
// Complete events arrive one per content block, all sharing message.id, so
|
|
209
|
+
// later blocks of the same message must not open a new message (that would
|
|
210
|
+
// drift the positional thought ids and re-trigger the boundary logic).
|
|
211
|
+
function beginAssistantMessage(state, { fromStreamEvent, messageId = '' }) {
|
|
212
|
+
if (fromStreamEvent) {
|
|
213
|
+
state.messageIndex += 1;
|
|
214
|
+
state.messageStarted = true;
|
|
215
|
+
state.thinking.clear();
|
|
216
|
+
// New API message: the pending bubble must show this message's prose,
|
|
217
|
+
// not the previous message's narration concatenated onto it. The
|
|
218
|
+
// stream channel publishes cumulative text, so the accumulator resets
|
|
219
|
+
// at the message boundary.
|
|
220
|
+
state.streamText = '';
|
|
221
|
+
state.lastEmittedStreamText = '';
|
|
222
|
+
return;
|
|
223
|
+
}
|
|
224
|
+
if (messageId && state.lastCompleteMessageId === messageId) return;
|
|
225
|
+
state.lastCompleteMessageId = messageId;
|
|
226
|
+
if (state.messageStarted) {
|
|
227
|
+
state.messageStarted = false;
|
|
228
|
+
return;
|
|
229
|
+
}
|
|
230
|
+
state.messageIndex += 1;
|
|
231
|
+
}
|
|
232
|
+
|
|
233
|
+
function subagentRunIdFor(parentToolUseId) {
|
|
234
|
+
const value = String(parentToolUseId || '').trim();
|
|
235
|
+
return value || null;
|
|
236
|
+
}
|
|
237
|
+
|
|
238
|
+
function actionsForToolUseBlock(block, parentToolUseId) {
|
|
239
|
+
const actions = [];
|
|
240
|
+
const toolName = displayToolName(block?.name) || 'unknown';
|
|
241
|
+
const toolUseId = String(block?.id || '').trim();
|
|
242
|
+
const input = block?.input && typeof block.input === 'object' ? block.input : {};
|
|
243
|
+
if (isSubagentToolName(toolName) && toolUseId) {
|
|
244
|
+
const displayName = String(
|
|
245
|
+
input.description
|
|
246
|
+
|| input.name
|
|
247
|
+
|| input.subagent_type
|
|
248
|
+
|| 'Subagent',
|
|
249
|
+
).trim() || 'Subagent';
|
|
250
|
+
const parentSubagentId = subagentRunIdFor(parentToolUseId);
|
|
251
|
+
knownSubagentRuns.set(toolUseId, { displayName, parentSubagentId });
|
|
252
|
+
actions.push({
|
|
253
|
+
channel: 'subagent',
|
|
254
|
+
payload: {
|
|
255
|
+
subagentRunId: toolUseId,
|
|
256
|
+
parentSubagentId,
|
|
257
|
+
displayName,
|
|
258
|
+
status: 'running',
|
|
259
|
+
},
|
|
260
|
+
});
|
|
261
|
+
}
|
|
262
|
+
actions.push({
|
|
263
|
+
channel: 'activity',
|
|
264
|
+
payload: {
|
|
265
|
+
text: formatToolActivityText(toolName, input),
|
|
266
|
+
subagentRunId: subagentRunIdFor(parentToolUseId),
|
|
267
|
+
},
|
|
268
|
+
});
|
|
269
|
+
return actions;
|
|
270
|
+
}
|
|
271
|
+
|
|
272
|
+
function actionsForToolResultBlock(block, parentToolUseId) {
|
|
273
|
+
const actions = [];
|
|
274
|
+
const toolUseId = String(block?.tool_use_id || '').trim();
|
|
275
|
+
const isError = block?.is_error === true;
|
|
276
|
+
if (toolUseId && knownSubagentRuns.has(toolUseId)) {
|
|
277
|
+
const run = knownSubagentRuns.get(toolUseId);
|
|
278
|
+
actions.push({
|
|
279
|
+
channel: 'subagent',
|
|
280
|
+
payload: {
|
|
281
|
+
subagentRunId: toolUseId,
|
|
282
|
+
parentSubagentId: run.parentSubagentId,
|
|
283
|
+
displayName: run.displayName,
|
|
284
|
+
status: isError ? 'failed' : 'completed',
|
|
285
|
+
},
|
|
286
|
+
});
|
|
287
|
+
}
|
|
288
|
+
if (isError) {
|
|
289
|
+
const errorText = typeof block?.content === 'string'
|
|
290
|
+
? block.content
|
|
291
|
+
: (Array.isArray(block?.content)
|
|
292
|
+
? block.content.map((entry) => String(entry?.text || '')).join(' ')
|
|
293
|
+
: '');
|
|
294
|
+
actions.push({
|
|
295
|
+
channel: 'activity',
|
|
296
|
+
payload: {
|
|
297
|
+
text: truncate(`Tool failed: ${errorText || 'unknown error'}`),
|
|
298
|
+
subagentRunId: subagentRunIdFor(parentToolUseId),
|
|
299
|
+
},
|
|
300
|
+
});
|
|
301
|
+
}
|
|
302
|
+
return actions;
|
|
303
|
+
}
|
|
304
|
+
|
|
305
|
+
/**
|
|
306
|
+
* Complete assistant messages carry the authoritative copy of every block.
|
|
307
|
+
* Thinking is republished here (same reasoningId as the streamed frames, so
|
|
308
|
+
* it upserts) which is also the only path that surfaces thinking at all when
|
|
309
|
+
* partial messages are unavailable. Assistant text that shares a message with
|
|
310
|
+
* tool calls is interim narration and becomes a thought, mirroring the
|
|
311
|
+
* Copilot extension's reasoning-stream bridge; text on a final, tool-free
|
|
312
|
+
* message is the answer itself and must not be duplicated as a thought.
|
|
313
|
+
*/
|
|
314
|
+
function actionsForAssistantMessage(content, parentToolUseId, messageId) {
|
|
315
|
+
const actions = [];
|
|
316
|
+
const state = threadState(parentToolUseId);
|
|
317
|
+
const subagentRunId = subagentRunIdFor(parentToolUseId);
|
|
318
|
+
beginAssistantMessage(state, { fromStreamEvent: false, messageId });
|
|
319
|
+
|
|
320
|
+
// Whether a text block is interim narration (a thought) or the answer is
|
|
321
|
+
// a property of the whole API message, but the SDK delivers one complete
|
|
322
|
+
// event per content block — so within one event a text block can never
|
|
323
|
+
// share `content` with a tool_use. Track tool-bearing per message.id:
|
|
324
|
+
// text seen before the message's first tool_use buffers until either a
|
|
325
|
+
// tool_use proves it narration or the message ends (answer text is
|
|
326
|
+
// published from the result message and must never double as a thought).
|
|
327
|
+
if (messageId && state.currentComplete?.id !== messageId) {
|
|
328
|
+
state.currentComplete = { id: messageId, sawToolUse: false, pendingTexts: [] };
|
|
329
|
+
}
|
|
330
|
+
const tracker = messageId ? state.currentComplete : null;
|
|
331
|
+
const hasToolUseInEvent = content.some((block) => block?.type === 'tool_use');
|
|
332
|
+
|
|
333
|
+
const emitNarration = (text, index) => {
|
|
334
|
+
const capped = capThought(text || '');
|
|
335
|
+
if (!capped.trim()) return;
|
|
336
|
+
actions.push({
|
|
337
|
+
channel: 'thought',
|
|
338
|
+
payload: {
|
|
339
|
+
reasoningId: thoughtIdFor(state, 'narration', index),
|
|
340
|
+
text: capped,
|
|
341
|
+
done: true,
|
|
342
|
+
subagentRunId,
|
|
343
|
+
},
|
|
344
|
+
});
|
|
345
|
+
};
|
|
346
|
+
|
|
347
|
+
for (const [index, block] of content.entries()) {
|
|
348
|
+
const blockType = String(block?.type || '');
|
|
349
|
+
if (blockType === 'thinking' || blockType === 'redacted_thinking') {
|
|
350
|
+
// Consume the pairing slot BEFORE the empty check: the summarized
|
|
351
|
+
// display can deliver an empty raw block ahead of the summary block,
|
|
352
|
+
// and skipping the empty one would hand its streamed id to the summary.
|
|
353
|
+
const reasoningId = completeThinkingIdFor(state, messageId, index);
|
|
354
|
+
const text = blockType === 'redacted_thinking'
|
|
355
|
+
// Redacted blocks carry encrypted `data`, never `thinking` — a
|
|
356
|
+
// placeholder keeps the reasoning visible instead of vanishing.
|
|
357
|
+
? (String(block?.data || '').trim() ? REDACTED_THINKING_PLACEHOLDER : '')
|
|
358
|
+
: capThought(block?.thinking || '');
|
|
359
|
+
if (!text.trim()) continue;
|
|
360
|
+
actions.push({
|
|
361
|
+
channel: 'thought',
|
|
362
|
+
payload: {
|
|
363
|
+
reasoningId,
|
|
364
|
+
text,
|
|
365
|
+
done: true,
|
|
366
|
+
subagentRunId,
|
|
367
|
+
},
|
|
368
|
+
});
|
|
369
|
+
continue;
|
|
370
|
+
}
|
|
371
|
+
if (blockType === 'text') {
|
|
372
|
+
if (tracker) {
|
|
373
|
+
if (tracker.sawToolUse) emitNarration(block?.text, index);
|
|
374
|
+
else tracker.pendingTexts.push({ index, text: String(block?.text || '') });
|
|
375
|
+
} else if (hasToolUseInEvent) {
|
|
376
|
+
// No message id to correlate on: fall back to the per-event check.
|
|
377
|
+
emitNarration(block?.text, index);
|
|
378
|
+
}
|
|
379
|
+
continue;
|
|
380
|
+
}
|
|
381
|
+
if (blockType === 'tool_use') {
|
|
382
|
+
if (tracker) {
|
|
383
|
+
tracker.sawToolUse = true;
|
|
384
|
+
for (const pending of tracker.pendingTexts.splice(0)) {
|
|
385
|
+
emitNarration(pending.text, pending.index);
|
|
386
|
+
}
|
|
387
|
+
}
|
|
388
|
+
actions.push(...actionsForToolUseBlock(block, parentToolUseId));
|
|
389
|
+
}
|
|
390
|
+
}
|
|
391
|
+
return actions;
|
|
392
|
+
}
|
|
393
|
+
|
|
394
|
+
function normalizeStreamEvent(sdkMessage) {
|
|
395
|
+
const actions = [];
|
|
396
|
+
const event = sdkMessage?.event || {};
|
|
397
|
+
const parentToolUseId = sdkMessage?.parent_tool_use_id || null;
|
|
398
|
+
const state = threadState(parentToolUseId);
|
|
399
|
+
const subagentRunId = subagentRunIdFor(parentToolUseId);
|
|
400
|
+
const eventType = String(event?.type || '');
|
|
401
|
+
|
|
402
|
+
if (eventType === 'message_start') {
|
|
403
|
+
beginAssistantMessage(state, { fromStreamEvent: true });
|
|
404
|
+
state.currentMessageId = String(event?.message?.id || '').trim();
|
|
405
|
+
return actions;
|
|
406
|
+
}
|
|
407
|
+
|
|
408
|
+
if (eventType === 'content_block_start') {
|
|
409
|
+
const blockType = String(event?.content_block?.type || '');
|
|
410
|
+
if (blockType === 'thinking' || blockType === 'redacted_thinking') {
|
|
411
|
+
const index = Number(event?.index ?? 0);
|
|
412
|
+
const reasoningId = thoughtIdFor(state, 'thought', index);
|
|
413
|
+
state.thinking.set(index, { reasoningId, text: '' });
|
|
414
|
+
if (state.currentMessageId) {
|
|
415
|
+
const ids = state.streamedThinkingIds.get(state.currentMessageId) || [];
|
|
416
|
+
ids.push(reasoningId);
|
|
417
|
+
state.streamedThinkingIds.set(state.currentMessageId, ids);
|
|
418
|
+
}
|
|
419
|
+
}
|
|
420
|
+
return actions;
|
|
421
|
+
}
|
|
422
|
+
|
|
423
|
+
if (eventType === 'content_block_delta') {
|
|
424
|
+
const deltaType = String(event?.delta?.type || '');
|
|
425
|
+
const index = Number(event?.index ?? 0);
|
|
426
|
+
if (deltaType === 'text_delta') {
|
|
427
|
+
state.streamText += String(event?.delta?.text || '');
|
|
428
|
+
if (shouldEmitStreamUpdate(state.streamText, state.lastEmittedStreamText)) {
|
|
429
|
+
state.lastEmittedStreamText = state.streamText;
|
|
430
|
+
actions.push({
|
|
431
|
+
channel: 'stream',
|
|
432
|
+
payload: { text: state.streamText, done: false, subagentRunId },
|
|
433
|
+
});
|
|
434
|
+
}
|
|
435
|
+
return actions;
|
|
436
|
+
}
|
|
437
|
+
if (deltaType === 'thinking_delta') {
|
|
438
|
+
const thinking = state.thinking.get(index);
|
|
439
|
+
if (thinking) {
|
|
440
|
+
thinking.text = capThought(thinking.text + String(event?.delta?.thinking || ''));
|
|
441
|
+
actions.push({
|
|
442
|
+
channel: 'thought',
|
|
443
|
+
payload: {
|
|
444
|
+
reasoningId: thinking.reasoningId,
|
|
445
|
+
text: thinking.text,
|
|
446
|
+
done: false,
|
|
447
|
+
subagentRunId,
|
|
448
|
+
},
|
|
449
|
+
});
|
|
450
|
+
}
|
|
451
|
+
return actions;
|
|
452
|
+
}
|
|
453
|
+
return actions;
|
|
454
|
+
}
|
|
455
|
+
|
|
456
|
+
if (eventType === 'content_block_stop') {
|
|
457
|
+
const index = Number(event?.index ?? 0);
|
|
458
|
+
const thinking = state.thinking.get(index);
|
|
459
|
+
if (thinking) {
|
|
460
|
+
state.thinking.delete(index);
|
|
461
|
+
if (thinking.text.trim()) {
|
|
462
|
+
actions.push({
|
|
463
|
+
channel: 'thought',
|
|
464
|
+
payload: {
|
|
465
|
+
reasoningId: thinking.reasoningId,
|
|
466
|
+
text: thinking.text,
|
|
467
|
+
done: true,
|
|
468
|
+
subagentRunId,
|
|
469
|
+
},
|
|
470
|
+
});
|
|
471
|
+
}
|
|
472
|
+
}
|
|
473
|
+
return actions;
|
|
474
|
+
}
|
|
475
|
+
|
|
476
|
+
return actions;
|
|
477
|
+
}
|
|
478
|
+
|
|
479
|
+
function normalize(sdkMessage) {
|
|
480
|
+
if (!sdkMessage || typeof sdkMessage !== 'object') return [];
|
|
481
|
+
const type = String(sdkMessage.type || '');
|
|
482
|
+
|
|
483
|
+
if (type === 'system' && sdkMessage.subtype === 'init') {
|
|
484
|
+
initSessionId = String(sdkMessage.session_id || '').trim();
|
|
485
|
+
initModel = String(sdkMessage.model || '').trim();
|
|
486
|
+
return [{ channel: 'init', payload: { sessionId: initSessionId, model: initModel } }];
|
|
487
|
+
}
|
|
488
|
+
|
|
489
|
+
// The CLI retried a safeguards-refused request on a fallback model
|
|
490
|
+
// (e.g. Fable 5 → Opus 4.8, scope "session"). From here on this turn's
|
|
491
|
+
// output comes from the fallback model, so the recorded model must follow
|
|
492
|
+
// it — the init-channel action updates the turn's responseModel in place.
|
|
493
|
+
// The CLI's own notice is surfaced as an activity line; silently flipping
|
|
494
|
+
// models is exactly the confusion this exists to prevent (conv 3366b9d3).
|
|
495
|
+
if (type === 'system' && sdkMessage.subtype === 'model_refusal_fallback') {
|
|
496
|
+
const actions = [];
|
|
497
|
+
const fallbackModel = String(sdkMessage.fallbackModel || '').trim();
|
|
498
|
+
if (fallbackModel) {
|
|
499
|
+
initModel = fallbackModel;
|
|
500
|
+
actions.push({ channel: 'init', payload: { sessionId: initSessionId, model: fallbackModel } });
|
|
501
|
+
}
|
|
502
|
+
const notice = String(sdkMessage.content || '').trim()
|
|
503
|
+
|| `Model switched to ${fallbackModel || 'a fallback model'} after a refusal.`;
|
|
504
|
+
actions.push({ channel: 'activity', payload: { text: truncate(notice, 500), subagentRunId: null } });
|
|
505
|
+
return actions;
|
|
506
|
+
}
|
|
507
|
+
|
|
508
|
+
// An upstream API request failed and the CLI is retrying with backoff.
|
|
509
|
+
// While that happens the CLI emits no assistant traffic at all, so
|
|
510
|
+
// without this line the relay UI shows a bare typing indicator that is
|
|
511
|
+
// indistinguishable from a wedged worker (three 529 stalls read as relay
|
|
512
|
+
// freezes on 2026-08-18). At most max_retries lines per request.
|
|
513
|
+
if (type === 'system' && sdkMessage.subtype === 'api_retry') {
|
|
514
|
+
return [{
|
|
515
|
+
channel: 'activity',
|
|
516
|
+
payload: { text: formatApiRetryNotice(sdkMessage), subagentRunId: null },
|
|
517
|
+
}];
|
|
518
|
+
}
|
|
519
|
+
|
|
520
|
+
if (type === 'stream_event') {
|
|
521
|
+
return normalizeStreamEvent(sdkMessage);
|
|
522
|
+
}
|
|
523
|
+
|
|
524
|
+
if (type === 'assistant') {
|
|
525
|
+
const parentToolUseId = sdkMessage?.parent_tool_use_id || null;
|
|
526
|
+
const content = Array.isArray(sdkMessage?.message?.content) ? sdkMessage.message.content : [];
|
|
527
|
+
const messageId = String(sdkMessage?.message?.id || '').trim();
|
|
528
|
+
return actionsForAssistantMessage(content, parentToolUseId, messageId);
|
|
529
|
+
}
|
|
530
|
+
|
|
531
|
+
if (type === 'user') {
|
|
532
|
+
const actions = [];
|
|
533
|
+
const parentToolUseId = sdkMessage?.parent_tool_use_id || null;
|
|
534
|
+
const content = Array.isArray(sdkMessage?.message?.content) ? sdkMessage.message.content : [];
|
|
535
|
+
for (const block of content) {
|
|
536
|
+
if (block?.type === 'tool_result') {
|
|
537
|
+
actions.push(...actionsForToolResultBlock(block, parentToolUseId));
|
|
538
|
+
}
|
|
539
|
+
}
|
|
540
|
+
return actions;
|
|
541
|
+
}
|
|
542
|
+
|
|
543
|
+
if (type === 'system' && sdkMessage.subtype === 'compact_boundary') {
|
|
544
|
+
return [compactBoundaryActivityAction(sdkMessage)];
|
|
545
|
+
}
|
|
546
|
+
|
|
547
|
+
// The live background-task set, emitted on every membership change with
|
|
548
|
+
// REPLACE semantics. The turn runner gates the input-gate release on it: a
|
|
549
|
+
// released gate closes the CLI's control transport, and background agents
|
|
550
|
+
// that outlive the visible turn then lose every permission request.
|
|
551
|
+
if (type === 'system' && sdkMessage.subtype === 'background_tasks_changed') {
|
|
552
|
+
const tasks = (Array.isArray(sdkMessage.tasks) ? sdkMessage.tasks : [])
|
|
553
|
+
.map((task) => ({
|
|
554
|
+
taskId: String(task?.task_id || '').trim(),
|
|
555
|
+
taskType: String(task?.task_type || '').trim(),
|
|
556
|
+
description: String(task?.description || '').trim(),
|
|
557
|
+
}))
|
|
558
|
+
.filter((task) => task.taskId);
|
|
559
|
+
return [{ channel: 'background_tasks', payload: { tasks } }];
|
|
560
|
+
}
|
|
561
|
+
|
|
562
|
+
if (type === 'system' && sdkMessage.subtype === 'task_notification') {
|
|
563
|
+
const taskId = String(sdkMessage.task_id || '').trim() || 'unknown';
|
|
564
|
+
const status = String(sdkMessage.status || '').trim() || 'unknown';
|
|
565
|
+
return [
|
|
566
|
+
// Settling is the edge the runner pairs with the level signal above: a
|
|
567
|
+
// settled session-level task means a continuation turn is about to be
|
|
568
|
+
// dequeued, so the gate must stay held even though the set is empty.
|
|
569
|
+
{ channel: 'background_task_settled', payload: { taskId, status } },
|
|
570
|
+
{
|
|
571
|
+
channel: 'activity',
|
|
572
|
+
payload: {
|
|
573
|
+
text: truncate(`Background task ${taskId} ${status}: ${String(sdkMessage.summary || '').trim()}`),
|
|
574
|
+
subagentRunId: null,
|
|
575
|
+
},
|
|
576
|
+
},
|
|
577
|
+
];
|
|
578
|
+
}
|
|
579
|
+
|
|
580
|
+
if (type === 'result') {
|
|
581
|
+
// A resumed session whose previous CLI process died with background tasks
|
|
582
|
+
// still tracked replays a bookkeeping turn for the orphaned-task
|
|
583
|
+
// notification and emits a zero-work result BEFORE the delivered user
|
|
584
|
+
// message's turn (num_turns 0, duration_api_ms 0). Treating that phantom
|
|
585
|
+
// as the turn result makes the runner release the input gate, which
|
|
586
|
+
// closes the CLI's control transport — every subsequent permission
|
|
587
|
+
// request in the real turn then fails with "AbortError: Stream closed"
|
|
588
|
+
// (canUseTool is never reached). Skip it; the real result follows.
|
|
589
|
+
const isPhantom = sdkMessage.subtype === 'success'
|
|
590
|
+
&& sdkMessage.is_error !== true
|
|
591
|
+
&& Number(sdkMessage.num_turns) === 0
|
|
592
|
+
&& Number(sdkMessage.duration_api_ms) === 0;
|
|
593
|
+
if (isPhantom) return [];
|
|
594
|
+
const isError = sdkMessage.subtype !== 'success' || sdkMessage.is_error === true;
|
|
595
|
+
return [{
|
|
596
|
+
channel: 'result',
|
|
597
|
+
payload: {
|
|
598
|
+
text: String(sdkMessage.result || '').trim(),
|
|
599
|
+
isError,
|
|
600
|
+
subtype: String(sdkMessage.subtype || ''),
|
|
601
|
+
sessionId: String(sdkMessage.session_id || initSessionId || '').trim(),
|
|
602
|
+
model: initModel,
|
|
603
|
+
usage: sdkMessage.usage || null,
|
|
604
|
+
// `modelUsage[model].contextWindow` is the authoritative per-model
|
|
605
|
+
// window; it is the fallback when the context-usage control request
|
|
606
|
+
// is unavailable. Its token counts are cumulative across the whole
|
|
607
|
+
// turn, so they must not be read as context occupancy.
|
|
608
|
+
modelUsage: sdkMessage.modelUsage || null,
|
|
609
|
+
totalCostUsd: Number.isFinite(Number(sdkMessage.total_cost_usd))
|
|
610
|
+
? Number(sdkMessage.total_cost_usd)
|
|
611
|
+
: null,
|
|
612
|
+
},
|
|
613
|
+
}];
|
|
614
|
+
}
|
|
615
|
+
|
|
616
|
+
return [];
|
|
617
|
+
}
|
|
618
|
+
|
|
619
|
+
function finalStreamText() {
|
|
620
|
+
return threadState(null).streamText;
|
|
621
|
+
}
|
|
622
|
+
|
|
623
|
+
return {
|
|
624
|
+
normalize,
|
|
625
|
+
finalStreamText,
|
|
626
|
+
get sessionId() { return initSessionId; },
|
|
627
|
+
get model() { return initModel; },
|
|
628
|
+
};
|
|
629
|
+
}
|