@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,277 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Client-side ACP host services for the Grok worker: the `terminal/*` and
|
|
3
|
+
* `fs/*` request handlers backing the client capabilities advertised in
|
|
4
|
+
* `AcpClient.initialize()`.
|
|
5
|
+
*
|
|
6
|
+
* Advertising a capability is a contract to answer the agent's requests for
|
|
7
|
+
* it — an advertised-but-unanswered request deadlocks the agent's turn: it
|
|
8
|
+
* waits forever on the reply, streaming nothing further, while the worker
|
|
9
|
+
* process stays healthy and heartbeating (the 2026-08-12 stall, where a
|
|
10
|
+
* `terminal/create` went unanswered for 9 minutes until the user hit Stop).
|
|
11
|
+
* If a capability is dropped here, drop it from the initialize payload too.
|
|
12
|
+
*/
|
|
13
|
+
import { spawn, spawnSync } from 'node:child_process';
|
|
14
|
+
import fs from 'node:fs/promises';
|
|
15
|
+
|
|
16
|
+
const DEFAULT_OUTPUT_BYTE_LIMIT = 1024 * 1024;
|
|
17
|
+
const MAX_OUTPUT_BYTE_LIMIT = 8 * 1024 * 1024;
|
|
18
|
+
// How long a running terminal's silence still counts as "the agent is
|
|
19
|
+
// waiting on us" for the prompt watchdog. See hasPendingWork.
|
|
20
|
+
const TERMINAL_ACTIVITY_WINDOW_MS = 5 * 60_000;
|
|
21
|
+
|
|
22
|
+
function envArrayToObject(env) {
|
|
23
|
+
const out = {};
|
|
24
|
+
if (!Array.isArray(env)) return out;
|
|
25
|
+
for (const entry of env) {
|
|
26
|
+
const name = String(entry?.name || '').trim();
|
|
27
|
+
if (name) out[name] = String(entry?.value ?? '');
|
|
28
|
+
}
|
|
29
|
+
return out;
|
|
30
|
+
}
|
|
31
|
+
|
|
32
|
+
export function createAcpHostServices({
|
|
33
|
+
cwd = process.cwd(),
|
|
34
|
+
env = process.env,
|
|
35
|
+
platform = process.platform,
|
|
36
|
+
spawnImpl = spawn,
|
|
37
|
+
spawnSyncImpl = spawnSync,
|
|
38
|
+
fsImpl = fs,
|
|
39
|
+
dbg = () => {},
|
|
40
|
+
} = {}) {
|
|
41
|
+
const terminals = new Map();
|
|
42
|
+
let nextTerminalId = 1;
|
|
43
|
+
let inflightRequests = 0;
|
|
44
|
+
let cachedWindowsShell = '';
|
|
45
|
+
let disposed = false;
|
|
46
|
+
|
|
47
|
+
// Grok emits PowerShell-flavored command lines on Windows hosts
|
|
48
|
+
// ("pwd; Get-Location"), so cmd.exe is not an option. Prefer pwsh (7+),
|
|
49
|
+
// fall back to Windows PowerShell.
|
|
50
|
+
function resolveWindowsShell() {
|
|
51
|
+
if (cachedWindowsShell) return cachedWindowsShell;
|
|
52
|
+
try {
|
|
53
|
+
const probe = spawnSyncImpl('pwsh', ['-NoProfile', '-Command', '$null'], {
|
|
54
|
+
windowsHide: true,
|
|
55
|
+
timeout: 10_000,
|
|
56
|
+
stdio: 'ignore',
|
|
57
|
+
});
|
|
58
|
+
cachedWindowsShell = probe && probe.status === 0 ? 'pwsh' : 'powershell';
|
|
59
|
+
} catch {
|
|
60
|
+
cachedWindowsShell = 'powershell';
|
|
61
|
+
}
|
|
62
|
+
return cachedWindowsShell;
|
|
63
|
+
}
|
|
64
|
+
|
|
65
|
+
function buildSpawnPlan(command, args) {
|
|
66
|
+
if (Array.isArray(args) && args.length) {
|
|
67
|
+
return { file: command, args: args.map((value) => String(value)) };
|
|
68
|
+
}
|
|
69
|
+
if (platform === 'win32') {
|
|
70
|
+
return {
|
|
71
|
+
file: resolveWindowsShell(),
|
|
72
|
+
args: ['-NoProfile', '-NonInteractive', '-Command', command],
|
|
73
|
+
};
|
|
74
|
+
}
|
|
75
|
+
const shell = String(env.SHELL || '').trim() || '/bin/sh';
|
|
76
|
+
return { file: shell, args: ['-c', command] };
|
|
77
|
+
}
|
|
78
|
+
|
|
79
|
+
function appendOutput(entry, chunk) {
|
|
80
|
+
const buf = Buffer.isBuffer(chunk) ? chunk : Buffer.from(String(chunk), 'utf8');
|
|
81
|
+
entry.lastActivityAt = Date.now();
|
|
82
|
+
entry.chunks.push(buf);
|
|
83
|
+
entry.byteLength += buf.length;
|
|
84
|
+
// ACP truncates from the start of the output: the most recent bytes win.
|
|
85
|
+
while (entry.byteLength > entry.outputByteLimit && entry.chunks.length) {
|
|
86
|
+
const head = entry.chunks[0];
|
|
87
|
+
const excess = entry.byteLength - entry.outputByteLimit;
|
|
88
|
+
if (head.length <= excess) {
|
|
89
|
+
entry.chunks.shift();
|
|
90
|
+
entry.byteLength -= head.length;
|
|
91
|
+
} else {
|
|
92
|
+
entry.chunks[0] = head.subarray(excess);
|
|
93
|
+
entry.byteLength -= excess;
|
|
94
|
+
}
|
|
95
|
+
entry.truncated = true;
|
|
96
|
+
}
|
|
97
|
+
}
|
|
98
|
+
|
|
99
|
+
function settleExit(entry, exitStatus) {
|
|
100
|
+
if (entry.exitStatus) return;
|
|
101
|
+
entry.exitStatus = exitStatus;
|
|
102
|
+
for (const resolve of entry.exitWaiters.splice(0)) resolve(exitStatus);
|
|
103
|
+
}
|
|
104
|
+
|
|
105
|
+
function getTerminal(terminalId) {
|
|
106
|
+
const entry = terminals.get(String(terminalId || ''));
|
|
107
|
+
if (!entry) throw new Error(`unknown terminalId: ${terminalId}`);
|
|
108
|
+
return entry;
|
|
109
|
+
}
|
|
110
|
+
|
|
111
|
+
async function terminalCreate(params = {}) {
|
|
112
|
+
if (disposed) throw new Error('host services disposed');
|
|
113
|
+
const command = String(params.command || '').trim();
|
|
114
|
+
if (!command) throw new Error('terminal/create requires a command');
|
|
115
|
+
const requestedLimit = Number(params.outputByteLimit);
|
|
116
|
+
const outputByteLimit = Number.isFinite(requestedLimit) && requestedLimit > 0
|
|
117
|
+
? Math.min(Math.round(requestedLimit), MAX_OUTPUT_BYTE_LIMIT)
|
|
118
|
+
: DEFAULT_OUTPUT_BYTE_LIMIT;
|
|
119
|
+
const plan = buildSpawnPlan(command, params.args);
|
|
120
|
+
const proc = spawnImpl(plan.file, plan.args, {
|
|
121
|
+
cwd: String(params.cwd || '').trim() || cwd,
|
|
122
|
+
env: { ...env, ...envArrayToObject(params.env) },
|
|
123
|
+
stdio: ['ignore', 'pipe', 'pipe'],
|
|
124
|
+
windowsHide: true,
|
|
125
|
+
shell: false,
|
|
126
|
+
});
|
|
127
|
+
const terminalId = `term-${nextTerminalId++}`;
|
|
128
|
+
const entry = {
|
|
129
|
+
terminalId,
|
|
130
|
+
proc,
|
|
131
|
+
chunks: [],
|
|
132
|
+
byteLength: 0,
|
|
133
|
+
truncated: false,
|
|
134
|
+
outputByteLimit,
|
|
135
|
+
exitStatus: null,
|
|
136
|
+
exitWaiters: [],
|
|
137
|
+
lastActivityAt: Date.now(),
|
|
138
|
+
};
|
|
139
|
+
proc.stdout?.on('data', (chunk) => appendOutput(entry, chunk));
|
|
140
|
+
proc.stderr?.on('data', (chunk) => appendOutput(entry, chunk));
|
|
141
|
+
proc.on('error', (error) => {
|
|
142
|
+
// Spawn failures (ENOENT etc.) surface as output + a synthetic exit so
|
|
143
|
+
// the agent's wait_for_exit settles instead of hanging.
|
|
144
|
+
appendOutput(entry, `[terminal spawn error] ${error?.message || String(error)}\n`);
|
|
145
|
+
settleExit(entry, { exitCode: -1, signal: null });
|
|
146
|
+
});
|
|
147
|
+
proc.on('exit', (code, signal) => {
|
|
148
|
+
settleExit(entry, {
|
|
149
|
+
exitCode: Number.isInteger(code) ? code : null,
|
|
150
|
+
signal: signal || null,
|
|
151
|
+
});
|
|
152
|
+
});
|
|
153
|
+
terminals.set(terminalId, entry);
|
|
154
|
+
dbg('terminal/create', terminalId, command.slice(0, 160));
|
|
155
|
+
return { terminalId };
|
|
156
|
+
}
|
|
157
|
+
|
|
158
|
+
function terminalOutput(params = {}) {
|
|
159
|
+
const entry = getTerminal(params.terminalId);
|
|
160
|
+
return {
|
|
161
|
+
output: Buffer.concat(entry.chunks, entry.byteLength).toString('utf8'),
|
|
162
|
+
truncated: entry.truncated,
|
|
163
|
+
...(entry.exitStatus ? { exitStatus: entry.exitStatus } : {}),
|
|
164
|
+
};
|
|
165
|
+
}
|
|
166
|
+
|
|
167
|
+
function terminalWaitForExit(params = {}) {
|
|
168
|
+
const entry = getTerminal(params.terminalId);
|
|
169
|
+
if (entry.exitStatus) return Promise.resolve(entry.exitStatus);
|
|
170
|
+
return new Promise((resolve) => entry.exitWaiters.push(resolve));
|
|
171
|
+
}
|
|
172
|
+
|
|
173
|
+
function killEntryProcess(entry) {
|
|
174
|
+
if (!entry.proc || entry.exitStatus) return;
|
|
175
|
+
try {
|
|
176
|
+
entry.proc.kill();
|
|
177
|
+
} catch { /* ignore */ }
|
|
178
|
+
if (platform === 'win32' && entry.proc.pid) {
|
|
179
|
+
// proc.kill() only reaches the shell; shells leave grandchildren behind.
|
|
180
|
+
try {
|
|
181
|
+
spawnImpl('taskkill', ['/pid', String(entry.proc.pid), '/t', '/f'], {
|
|
182
|
+
stdio: 'ignore',
|
|
183
|
+
windowsHide: true,
|
|
184
|
+
});
|
|
185
|
+
} catch { /* ignore */ }
|
|
186
|
+
}
|
|
187
|
+
}
|
|
188
|
+
|
|
189
|
+
function terminalKill(params = {}) {
|
|
190
|
+
killEntryProcess(getTerminal(params.terminalId));
|
|
191
|
+
return null;
|
|
192
|
+
}
|
|
193
|
+
|
|
194
|
+
function terminalRelease(params = {}) {
|
|
195
|
+
const entry = terminals.get(String(params?.terminalId || ''));
|
|
196
|
+
if (!entry) return null;
|
|
197
|
+
killEntryProcess(entry);
|
|
198
|
+
terminals.delete(entry.terminalId);
|
|
199
|
+
return null;
|
|
200
|
+
}
|
|
201
|
+
|
|
202
|
+
async function fsReadTextFile(params = {}) {
|
|
203
|
+
const filePath = String(params.path || '').trim();
|
|
204
|
+
if (!filePath) throw new Error('fs/read_text_file requires a path');
|
|
205
|
+
const content = await fsImpl.readFile(filePath, 'utf8');
|
|
206
|
+
const line = Number(params.line);
|
|
207
|
+
const limit = Number(params.limit);
|
|
208
|
+
const hasLine = Number.isFinite(line) && line > 0;
|
|
209
|
+
const hasLimit = Number.isFinite(limit) && limit > 0;
|
|
210
|
+
if (!hasLine && !hasLimit) return { content };
|
|
211
|
+
const lines = content.split('\n');
|
|
212
|
+
const start = hasLine ? line - 1 : 0;
|
|
213
|
+
const count = hasLimit ? limit : lines.length - start;
|
|
214
|
+
return { content: lines.slice(start, start + count).join('\n') };
|
|
215
|
+
}
|
|
216
|
+
|
|
217
|
+
async function fsWriteTextFile(params = {}) {
|
|
218
|
+
const filePath = String(params.path || '').trim();
|
|
219
|
+
if (!filePath) throw new Error('fs/write_text_file requires a path');
|
|
220
|
+
await fsImpl.writeFile(filePath, String(params.content ?? ''), 'utf8');
|
|
221
|
+
return null;
|
|
222
|
+
}
|
|
223
|
+
|
|
224
|
+
const handlers = {
|
|
225
|
+
'terminal/create': terminalCreate,
|
|
226
|
+
'terminal/output': terminalOutput,
|
|
227
|
+
'terminal/wait_for_exit': terminalWaitForExit,
|
|
228
|
+
'terminal/kill': terminalKill,
|
|
229
|
+
'terminal/release': terminalRelease,
|
|
230
|
+
'fs/read_text_file': fsReadTextFile,
|
|
231
|
+
'fs/write_text_file': fsWriteTextFile,
|
|
232
|
+
};
|
|
233
|
+
|
|
234
|
+
function attach(client) {
|
|
235
|
+
// wait_for_exit is a passive park, not active servicing: counting it as
|
|
236
|
+
// inflight would defer the stall watchdog forever for a process that
|
|
237
|
+
// never exits. Its pending-ness is represented by the unfinished
|
|
238
|
+
// terminal entry below instead.
|
|
239
|
+
const passiveWaits = new Set(['terminal/wait_for_exit']);
|
|
240
|
+
for (const [method, handler] of Object.entries(handlers)) {
|
|
241
|
+
const passive = passiveWaits.has(method);
|
|
242
|
+
client.setRequestHandler(method, async (params) => {
|
|
243
|
+
if (!passive) inflightRequests += 1;
|
|
244
|
+
try {
|
|
245
|
+
return await handler(params);
|
|
246
|
+
} finally {
|
|
247
|
+
if (!passive) inflightRequests -= 1;
|
|
248
|
+
}
|
|
249
|
+
});
|
|
250
|
+
}
|
|
251
|
+
}
|
|
252
|
+
|
|
253
|
+
/**
|
|
254
|
+
* True while the agent is legitimately waiting on us: a handler request is
|
|
255
|
+
* being serviced, or a terminal process is running AND recently active. A
|
|
256
|
+
* long-quiet background command (a dev server, a hung build) stops
|
|
257
|
+
* deferring the stall watchdog after the activity window — otherwise one
|
|
258
|
+
* `npm run dev` made every turn immune to stall detection forever.
|
|
259
|
+
*/
|
|
260
|
+
function hasPendingWork() {
|
|
261
|
+
if (inflightRequests > 0) return true;
|
|
262
|
+
const now = Date.now();
|
|
263
|
+
for (const entry of terminals.values()) {
|
|
264
|
+
if (entry.exitStatus) continue;
|
|
265
|
+
if (now - entry.lastActivityAt <= TERMINAL_ACTIVITY_WINDOW_MS) return true;
|
|
266
|
+
}
|
|
267
|
+
return false;
|
|
268
|
+
}
|
|
269
|
+
|
|
270
|
+
function disposeAll() {
|
|
271
|
+
disposed = true;
|
|
272
|
+
for (const entry of terminals.values()) killEntryProcess(entry);
|
|
273
|
+
terminals.clear();
|
|
274
|
+
}
|
|
275
|
+
|
|
276
|
+
return { attach, hasPendingWork, disposeAll, handlers };
|
|
277
|
+
}
|
|
@@ -0,0 +1,77 @@
|
|
|
1
|
+
import { resolveFallbackContextLimitTokens } from '../../shared/context-window-fallbacks.mjs';
|
|
2
|
+
/**
|
|
3
|
+
* Maps a normalized Grok turn usage blob (per-prompt `_meta` tokens from
|
|
4
|
+
* `normalizeGrokTurnUsage`) onto the payload shape the relay already persists
|
|
5
|
+
* for Claude/Cursor sessions, so the existing context view renders it
|
|
6
|
+
* unchanged. Mirrors `buildCursorContextUsage`.
|
|
7
|
+
*
|
|
8
|
+
* ACP reports no per-category breakdown, so `categories` is always empty.
|
|
9
|
+
*/
|
|
10
|
+
|
|
11
|
+
// Best-effort fallbacks when ACP model discovery reports no window size come
|
|
12
|
+
// from the shared static table (shared/context-window-fallbacks.mjs), which
|
|
13
|
+
// exists precisely so per-provider copies cannot drift. Unknown models get no
|
|
14
|
+
// fill metric (token totals only, like Cursor's partial state).
|
|
15
|
+
export function resolveGrokContextWindow(model = '', contextWindowsByModel = {}) {
|
|
16
|
+
const key = String(model || '').trim().toLowerCase();
|
|
17
|
+
if (!key) return null;
|
|
18
|
+
const discovered = Number(contextWindowsByModel?.[key]);
|
|
19
|
+
if (Number.isFinite(discovered) && discovered > 0) return Math.round(discovered);
|
|
20
|
+
return resolveFallbackContextLimitTokens(key);
|
|
21
|
+
}
|
|
22
|
+
|
|
23
|
+
function tokenCount(value) {
|
|
24
|
+
if (value === null || value === undefined || value === '') return null;
|
|
25
|
+
const n = Number(value);
|
|
26
|
+
return Number.isFinite(n) && n >= 0 ? Math.round(n) : null;
|
|
27
|
+
}
|
|
28
|
+
|
|
29
|
+
export function buildGrokContextUsage({ usage, model = '', contextWindow = null } = {}) {
|
|
30
|
+
if (!usage || typeof usage !== 'object') return null;
|
|
31
|
+
|
|
32
|
+
const inputTokens = tokenCount(usage.inputTokens);
|
|
33
|
+
const outputTokens = tokenCount(usage.outputTokens);
|
|
34
|
+
const cacheReadTokens = tokenCount(usage.cachedReadTokens);
|
|
35
|
+
const cacheWriteTokens = tokenCount(usage.cacheCreationTokens);
|
|
36
|
+
if (inputTokens === null && outputTokens === null
|
|
37
|
+
&& cacheReadTokens === null && cacheWriteTokens === null) {
|
|
38
|
+
return null;
|
|
39
|
+
}
|
|
40
|
+
|
|
41
|
+
// Context occupancy approximates what the next request re-sends: fresh
|
|
42
|
+
// input + cache reads + cache writes + the reply. The blob's `totalTokens`
|
|
43
|
+
// may be cumulative across the turn's model calls, so it is not used.
|
|
44
|
+
const totalTokens = (inputTokens || 0) + (cacheReadTokens || 0)
|
|
45
|
+
+ (cacheWriteTokens || 0) + (outputTokens || 0);
|
|
46
|
+
const maxTokens = (typeof contextWindow === 'number' && Number.isFinite(contextWindow)
|
|
47
|
+
&& contextWindow > 0)
|
|
48
|
+
? Math.round(contextWindow)
|
|
49
|
+
: null;
|
|
50
|
+
|
|
51
|
+
const contextUsage = {
|
|
52
|
+
model,
|
|
53
|
+
totalTokens,
|
|
54
|
+
...(maxTokens !== null
|
|
55
|
+
? { maxTokens, percentage: Math.round((totalTokens / maxTokens) * 10000) / 100 }
|
|
56
|
+
: {}),
|
|
57
|
+
categories: [],
|
|
58
|
+
apiUsage: {
|
|
59
|
+
input_tokens: inputTokens || 0,
|
|
60
|
+
output_tokens: outputTokens || 0,
|
|
61
|
+
cache_read_input_tokens: cacheReadTokens || 0,
|
|
62
|
+
cache_creation_input_tokens: cacheWriteTokens || 0,
|
|
63
|
+
},
|
|
64
|
+
};
|
|
65
|
+
|
|
66
|
+
const modelUsage = {
|
|
67
|
+
[model]: {
|
|
68
|
+
...(maxTokens !== null ? { contextWindow: maxTokens } : {}),
|
|
69
|
+
inputTokens: inputTokens || 0,
|
|
70
|
+
outputTokens: outputTokens || 0,
|
|
71
|
+
cacheReadInputTokens: cacheReadTokens || 0,
|
|
72
|
+
cacheCreationInputTokens: cacheWriteTokens || 0,
|
|
73
|
+
},
|
|
74
|
+
};
|
|
75
|
+
|
|
76
|
+
return { contextUsage, modelUsage };
|
|
77
|
+
}
|
|
@@ -0,0 +1,334 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Thin adapter around the Grok CLI ACP surface. One long-lived agent process
|
|
3
|
+
* per worker; sessions are created/loaded and prompts streamed via AcpClient.
|
|
4
|
+
*/
|
|
5
|
+
import { AcpClient, extractGrokModelsFromInitialize, pickAllowOnceOptionId } from './acp-client.mjs';
|
|
6
|
+
import { createAcpHostServices } from './acp-host-services.mjs';
|
|
7
|
+
import { createSdkMessageNormalizer } from './sdk-message-normalizer.mjs';
|
|
8
|
+
|
|
9
|
+
export { extractGrokModelsFromInitialize };
|
|
10
|
+
|
|
11
|
+
export function classifyGrokError(error) {
|
|
12
|
+
const message = String(error?.message || error || '').trim();
|
|
13
|
+
const lower = message.toLowerCase();
|
|
14
|
+
// No bare 'auth' token: it would match unrelated words ("author…") in
|
|
15
|
+
// arbitrary agent error text and misclassify them as login failures.
|
|
16
|
+
if (
|
|
17
|
+
lower.includes('authentication')
|
|
18
|
+
|| lower.includes('not logged in')
|
|
19
|
+
|| lower.includes('not authenticated')
|
|
20
|
+
|| lower.includes('unauthorized')
|
|
21
|
+
|| lower.includes('api key')
|
|
22
|
+
) {
|
|
23
|
+
return {
|
|
24
|
+
code: 'grok.authentication_failed',
|
|
25
|
+
// Points at the relay's own UI, which renders a Sign in button for this
|
|
26
|
+
// stable code (relay-error-ctas.mjs); the host-shell advice stays as the
|
|
27
|
+
// fallback for anyone reading this outside a browser.
|
|
28
|
+
message: 'Grok authentication failed. Sign in from Settings → Providers → Grok, '
|
|
29
|
+
+ 'or run `grok login` on the relay host / set XAI_API_KEY.',
|
|
30
|
+
isAuth: true,
|
|
31
|
+
isBusy: false,
|
|
32
|
+
};
|
|
33
|
+
}
|
|
34
|
+
if (lower.includes('already has an active') || lower.includes('busy')) {
|
|
35
|
+
return {
|
|
36
|
+
code: 'grok.agent_busy',
|
|
37
|
+
message: message || 'Grok agent is busy',
|
|
38
|
+
isAuth: false,
|
|
39
|
+
isBusy: true,
|
|
40
|
+
};
|
|
41
|
+
}
|
|
42
|
+
// Watchdog trips from AcpClient.sessionPrompt (inactivity or turn ceiling).
|
|
43
|
+
if (lower.includes('turn stalled') || lower.includes('turn exceeded')) {
|
|
44
|
+
return {
|
|
45
|
+
code: 'grok.turn-stalled',
|
|
46
|
+
message: message || 'Grok turn stalled',
|
|
47
|
+
isAuth: false,
|
|
48
|
+
isBusy: false,
|
|
49
|
+
isStalled: true,
|
|
50
|
+
};
|
|
51
|
+
}
|
|
52
|
+
if (lower.includes('enoent') || lower.includes('not found') || lower.includes('spawn')) {
|
|
53
|
+
return {
|
|
54
|
+
code: 'grok.cli_missing',
|
|
55
|
+
// The dead end this plan exists for: the only fix used to be a shell on
|
|
56
|
+
// the host. The relay now installs it, and this stable code renders an
|
|
57
|
+
// Install button (relay-error-ctas.mjs).
|
|
58
|
+
message: 'Grok CLI was not found on PATH. Install it from Settings → Providers → Grok.',
|
|
59
|
+
isAuth: false,
|
|
60
|
+
isBusy: false,
|
|
61
|
+
};
|
|
62
|
+
}
|
|
63
|
+
return {
|
|
64
|
+
code: 'grok.turn-error',
|
|
65
|
+
message: message || 'Grok turn failed',
|
|
66
|
+
isAuth: false,
|
|
67
|
+
isBusy: false,
|
|
68
|
+
};
|
|
69
|
+
}
|
|
70
|
+
|
|
71
|
+
/**
|
|
72
|
+
* Create or resume a Grok ACP session handle for one conversation worker.
|
|
73
|
+
*/
|
|
74
|
+
export async function createGrokAgentHandle({
|
|
75
|
+
command = 'grok',
|
|
76
|
+
args = ['agent', '--no-leader', 'stdio'],
|
|
77
|
+
cwd = process.cwd(),
|
|
78
|
+
env = process.env,
|
|
79
|
+
alwaysApprove = true,
|
|
80
|
+
nativeSessionId = '',
|
|
81
|
+
model = '',
|
|
82
|
+
AcpClientImpl = AcpClient,
|
|
83
|
+
createHostServicesImpl = createAcpHostServices,
|
|
84
|
+
dbg = () => {},
|
|
85
|
+
} = {}) {
|
|
86
|
+
const client = new AcpClientImpl({
|
|
87
|
+
command,
|
|
88
|
+
args,
|
|
89
|
+
cwd,
|
|
90
|
+
env,
|
|
91
|
+
alwaysApprove,
|
|
92
|
+
});
|
|
93
|
+
|
|
94
|
+
// Without any 'error' listener, a spawn failure (ENOENT when the Grok CLI
|
|
95
|
+
// is not installed) would make the emitter throw ERR_UNHANDLED_ERROR and
|
|
96
|
+
// crash the process. The pending-request rejection already surfaces the
|
|
97
|
+
// failure to the awaiting caller, so the listener only needs to exist.
|
|
98
|
+
client.on('error', () => {});
|
|
99
|
+
|
|
100
|
+
client.on('stderr', (text) => {
|
|
101
|
+
const line = String(text || '').trim();
|
|
102
|
+
if (line) dbg('grok stderr', line.slice(0, 400));
|
|
103
|
+
});
|
|
104
|
+
|
|
105
|
+
// The terminal/fs handlers backing the capabilities initialize() advertises.
|
|
106
|
+
// They must be attached before the first prompt, or the agent's first shell
|
|
107
|
+
// command deadlocks the turn waiting on terminal/create.
|
|
108
|
+
const hostServices = createHostServicesImpl({ cwd, env, dbg });
|
|
109
|
+
hostServices.attach(client);
|
|
110
|
+
|
|
111
|
+
await client.initialize();
|
|
112
|
+
const discovered = extractGrokModelsFromInitialize(client.initializeResult);
|
|
113
|
+
|
|
114
|
+
let sessionId = '';
|
|
115
|
+
let resumeFailed = false;
|
|
116
|
+
const resumeId = String(nativeSessionId || '').trim();
|
|
117
|
+
if (resumeId) {
|
|
118
|
+
// Attempting session/load against an agent that never advertised the
|
|
119
|
+
// capability is a guaranteed failure; either way a fresh session is a
|
|
120
|
+
// silent history loss the caller must be able to surface.
|
|
121
|
+
const capabilities = client.initializeResult?.agentCapabilities
|
|
122
|
+
|| client.initializeResult?.capabilities
|
|
123
|
+
|| {};
|
|
124
|
+
const supportsLoad = capabilities.loadSession === true || capabilities.load_session === true;
|
|
125
|
+
if (!supportsLoad) {
|
|
126
|
+
resumeFailed = true;
|
|
127
|
+
dbg('grok agent does not advertise loadSession; starting fresh');
|
|
128
|
+
} else {
|
|
129
|
+
try {
|
|
130
|
+
const loaded = await client.sessionLoad(resumeId, cwd);
|
|
131
|
+
sessionId = String(loaded?.sessionId || loaded?.session_id || resumeId).trim();
|
|
132
|
+
} catch (error) {
|
|
133
|
+
resumeFailed = true;
|
|
134
|
+
dbg('grok session/load failed, creating new session', error?.message || String(error));
|
|
135
|
+
}
|
|
136
|
+
}
|
|
137
|
+
}
|
|
138
|
+
if (!sessionId) {
|
|
139
|
+
const extra = {};
|
|
140
|
+
const modelId = String(model || '').trim();
|
|
141
|
+
if (modelId) {
|
|
142
|
+
// Best-effort: some Grok builds accept model via _meta on session/new.
|
|
143
|
+
extra._meta = { modelId };
|
|
144
|
+
}
|
|
145
|
+
const created = await client.sessionNew(cwd, extra);
|
|
146
|
+
sessionId = String(created?.sessionId || created?.session_id || '').trim();
|
|
147
|
+
}
|
|
148
|
+
if (!sessionId) {
|
|
149
|
+
hostServices.disposeAll();
|
|
150
|
+
await client.dispose();
|
|
151
|
+
throw new Error('Grok session/new did not return sessionId');
|
|
152
|
+
}
|
|
153
|
+
|
|
154
|
+
return {
|
|
155
|
+
client,
|
|
156
|
+
sessionId,
|
|
157
|
+
model: String(model || discovered.defaultModel || '').trim(),
|
|
158
|
+
discovered,
|
|
159
|
+
hostServices,
|
|
160
|
+
resumeFailed,
|
|
161
|
+
async close() {
|
|
162
|
+
hostServices.disposeAll();
|
|
163
|
+
await client.dispose();
|
|
164
|
+
},
|
|
165
|
+
};
|
|
166
|
+
}
|
|
167
|
+
|
|
168
|
+
/**
|
|
169
|
+
* Stream-friendly turn starter used by the turn runner. Yields the same
|
|
170
|
+
* channel actions the normalizer produces, draining pending actions interleaved
|
|
171
|
+
* by polling a queue that the ACP update handler fills.
|
|
172
|
+
*/
|
|
173
|
+
export function startGrokTurn({
|
|
174
|
+
handle,
|
|
175
|
+
text = '',
|
|
176
|
+
reasoningEffort = '',
|
|
177
|
+
abortSignal = null,
|
|
178
|
+
watchdog = null,
|
|
179
|
+
createNormalizerImpl = createSdkMessageNormalizer,
|
|
180
|
+
dbg = () => {},
|
|
181
|
+
} = {}) {
|
|
182
|
+
if (!handle?.client || !handle?.sessionId) {
|
|
183
|
+
throw new Error('missing grok agent handle');
|
|
184
|
+
}
|
|
185
|
+
|
|
186
|
+
const normalizer = createNormalizerImpl();
|
|
187
|
+
const queue = [];
|
|
188
|
+
let done = false;
|
|
189
|
+
let settleError = null;
|
|
190
|
+
let resolveWait = null;
|
|
191
|
+
|
|
192
|
+
function wake() {
|
|
193
|
+
if (resolveWait) {
|
|
194
|
+
const r = resolveWait;
|
|
195
|
+
resolveWait = null;
|
|
196
|
+
r();
|
|
197
|
+
}
|
|
198
|
+
}
|
|
199
|
+
|
|
200
|
+
function enqueue(action) {
|
|
201
|
+
queue.push(action);
|
|
202
|
+
wake();
|
|
203
|
+
}
|
|
204
|
+
|
|
205
|
+
enqueue({
|
|
206
|
+
channel: 'init',
|
|
207
|
+
payload: {
|
|
208
|
+
sessionId: handle.sessionId,
|
|
209
|
+
model: handle.model || '',
|
|
210
|
+
},
|
|
211
|
+
});
|
|
212
|
+
|
|
213
|
+
let cancelled = false;
|
|
214
|
+
const onAbort = () => {
|
|
215
|
+
cancelled = true;
|
|
216
|
+
try {
|
|
217
|
+
handle.client.sessionCancel(handle.sessionId);
|
|
218
|
+
} catch (error) {
|
|
219
|
+
dbg('session/cancel failed', error?.message || String(error));
|
|
220
|
+
}
|
|
221
|
+
};
|
|
222
|
+
if (abortSignal) {
|
|
223
|
+
if (abortSignal.aborted) onAbort();
|
|
224
|
+
else abortSignal.addEventListener('abort', onAbort, { once: true });
|
|
225
|
+
}
|
|
226
|
+
|
|
227
|
+
const onPermission = (msg) => {
|
|
228
|
+
// Prefer a one-shot allow: a blanket "allow always" would grant more than
|
|
229
|
+
// the relay's per-request approval parity intends.
|
|
230
|
+
const optionId = pickAllowOnceOptionId(msg?.params?.options);
|
|
231
|
+
try {
|
|
232
|
+
handle.client.respond(msg.id, {
|
|
233
|
+
outcome: { outcome: 'selected', optionId },
|
|
234
|
+
});
|
|
235
|
+
} catch (error) {
|
|
236
|
+
dbg('permission respond failed', error?.message || String(error));
|
|
237
|
+
}
|
|
238
|
+
};
|
|
239
|
+
handle.client.on('permission', onPermission);
|
|
240
|
+
|
|
241
|
+
// Best-effort per-turn reasoning effort: forwarded on the prompt `_meta`
|
|
242
|
+
// the same way the model is passed on session/new. Unknown _meta keys are
|
|
243
|
+
// ignored by agent builds that do not support them.
|
|
244
|
+
const effort = String(reasoningEffort || '').trim().toLowerCase();
|
|
245
|
+
const promptExtra = effort && effort !== 'none'
|
|
246
|
+
? { _meta: { reasoningEffort: effort } }
|
|
247
|
+
: {};
|
|
248
|
+
|
|
249
|
+
const promptPromise = handle.client.sessionPrompt(
|
|
250
|
+
handle.sessionId,
|
|
251
|
+
[{ type: 'text', text: String(text || '') }],
|
|
252
|
+
(update) => {
|
|
253
|
+
for (const action of normalizer.normalizeAcpUpdate(update)) {
|
|
254
|
+
enqueue(action);
|
|
255
|
+
}
|
|
256
|
+
},
|
|
257
|
+
promptExtra,
|
|
258
|
+
{
|
|
259
|
+
// A running client-hosted terminal (or an in-flight terminal/fs request)
|
|
260
|
+
// means the agent is waiting on us, not stalled.
|
|
261
|
+
hasPendingWork: () => handle.hostServices?.hasPendingWork?.() === true,
|
|
262
|
+
...(watchdog || {}),
|
|
263
|
+
},
|
|
264
|
+
).then((promptResult) => {
|
|
265
|
+
const stopReason = String(promptResult?.stopReason || promptResult?.stop_reason || '').trim();
|
|
266
|
+
const isError = stopReason.toLowerCase() === 'error' || promptResult?.isError === true;
|
|
267
|
+
// Per-prompt usage lives on the result `_meta` (tokens + costUsdTicks).
|
|
268
|
+
// Prefer that over any mid-stream usage_update the normalizer may have seen.
|
|
269
|
+
const meta = promptResult?._meta && typeof promptResult._meta === 'object'
|
|
270
|
+
? promptResult._meta
|
|
271
|
+
: null;
|
|
272
|
+
const usage = meta
|
|
273
|
+
? {
|
|
274
|
+
...meta,
|
|
275
|
+
usage: meta.usage,
|
|
276
|
+
modelId: meta.modelId || handle.model || '',
|
|
277
|
+
}
|
|
278
|
+
: null;
|
|
279
|
+
for (const action of normalizer.finalizeResult({
|
|
280
|
+
stopReason: cancelled ? 'cancelled' : stopReason,
|
|
281
|
+
text: normalizer.finalStreamText(),
|
|
282
|
+
isError: cancelled ? false : isError,
|
|
283
|
+
errorMessage: isError ? String(promptResult?.error || promptResult?.message || stopReason) : '',
|
|
284
|
+
model: String(meta?.modelId || handle.model || '').trim(),
|
|
285
|
+
usage,
|
|
286
|
+
})) {
|
|
287
|
+
enqueue(action);
|
|
288
|
+
}
|
|
289
|
+
}).catch((error) => {
|
|
290
|
+
if (cancelled || abortSignal?.aborted) {
|
|
291
|
+
for (const action of normalizer.finalizeResult({
|
|
292
|
+
stopReason: 'cancelled',
|
|
293
|
+
text: normalizer.finalStreamText(),
|
|
294
|
+
})) {
|
|
295
|
+
enqueue(action);
|
|
296
|
+
}
|
|
297
|
+
return;
|
|
298
|
+
}
|
|
299
|
+
settleError = error;
|
|
300
|
+
}).finally(() => {
|
|
301
|
+
done = true;
|
|
302
|
+
wake();
|
|
303
|
+
handle.client.off('permission', onPermission);
|
|
304
|
+
if (abortSignal) {
|
|
305
|
+
try {
|
|
306
|
+
abortSignal.removeEventListener('abort', onAbort);
|
|
307
|
+
} catch {
|
|
308
|
+
/* ignore */
|
|
309
|
+
}
|
|
310
|
+
}
|
|
311
|
+
});
|
|
312
|
+
|
|
313
|
+
async function* iterator() {
|
|
314
|
+
while (true) {
|
|
315
|
+
while (queue.length) {
|
|
316
|
+
yield queue.shift();
|
|
317
|
+
}
|
|
318
|
+
if (done) {
|
|
319
|
+
if (settleError) throw settleError;
|
|
320
|
+
return;
|
|
321
|
+
}
|
|
322
|
+
await new Promise((resolve) => {
|
|
323
|
+
resolveWait = resolve;
|
|
324
|
+
});
|
|
325
|
+
}
|
|
326
|
+
}
|
|
327
|
+
|
|
328
|
+
const iter = iterator();
|
|
329
|
+
iter.cancel = async () => {
|
|
330
|
+
onAbort();
|
|
331
|
+
await promptPromise.catch(() => {});
|
|
332
|
+
};
|
|
333
|
+
return iter;
|
|
334
|
+
}
|