actoviq-agent-sdk 0.4.21 → 0.4.22
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/README-zh.md +50 -113
- package/README.md +51 -164
- package/assets/hadamard-icon.ico +0 -0
- package/assets/hadamard-icon.png +0 -0
- package/bin/hadamard-gui.js +31 -12
- package/dist/src/codeIntel/codeIntelligenceService.d.ts +16 -0
- package/dist/src/codeIntel/codeIntelligenceService.d.ts.map +1 -1
- package/dist/src/codeIntel/codeIntelligenceService.js +87 -2
- package/dist/src/codeIntel/codeIntelligenceService.js.map +1 -1
- package/dist/src/codeIntel/codeIntelligenceTools.d.ts.map +1 -1
- package/dist/src/codeIntel/codeIntelligenceTools.js +6 -0
- package/dist/src/codeIntel/codeIntelligenceTools.js.map +1 -1
- package/dist/src/codeIntel/index.d.ts +1 -0
- package/dist/src/codeIntel/index.d.ts.map +1 -1
- package/dist/src/codeIntel/index.js +1 -0
- package/dist/src/codeIntel/index.js.map +1 -1
- package/dist/src/codeIntel/languageServerPresets.d.ts +13 -0
- package/dist/src/codeIntel/languageServerPresets.d.ts.map +1 -0
- package/dist/src/codeIntel/languageServerPresets.js +51 -0
- package/dist/src/codeIntel/languageServerPresets.js.map +1 -0
- package/dist/src/codeIntel/languageServerRegistry.d.ts +1 -0
- package/dist/src/codeIntel/languageServerRegistry.d.ts.map +1 -1
- package/dist/src/codeIntel/languageServerRegistry.js +2 -2
- package/dist/src/codeIntel/languageServerRegistry.js.map +1 -1
- package/dist/src/config/resolveRuntimeConfig.d.ts.map +1 -1
- package/dist/src/config/resolveRuntimeConfig.js +5 -0
- package/dist/src/config/resolveRuntimeConfig.js.map +1 -1
- package/dist/src/extensions/builtInExtensions.d.ts +61 -0
- package/dist/src/extensions/builtInExtensions.d.ts.map +1 -0
- package/dist/src/extensions/builtInExtensions.js +218 -0
- package/dist/src/extensions/builtInExtensions.js.map +1 -0
- package/dist/src/extensions/filterOutputPolicy.d.ts +21 -0
- package/dist/src/extensions/filterOutputPolicy.d.ts.map +1 -0
- package/dist/src/extensions/filterOutputPolicy.js +121 -0
- package/dist/src/extensions/filterOutputPolicy.js.map +1 -0
- package/dist/src/extensions/securityPolicy.d.ts +27 -0
- package/dist/src/extensions/securityPolicy.d.ts.map +1 -0
- package/dist/src/extensions/securityPolicy.js +238 -0
- package/dist/src/extensions/securityPolicy.js.map +1 -0
- package/dist/src/extensions/sessionCostTracker.d.ts +63 -0
- package/dist/src/extensions/sessionCostTracker.d.ts.map +1 -0
- package/dist/src/extensions/sessionCostTracker.js +213 -0
- package/dist/src/extensions/sessionCostTracker.js.map +1 -0
- package/dist/src/extensions/taskNotifications.d.ts +31 -0
- package/dist/src/extensions/taskNotifications.d.ts.map +1 -0
- package/dist/src/extensions/taskNotifications.js +50 -0
- package/dist/src/extensions/taskNotifications.js.map +1 -0
- package/dist/src/extensions/usageBar.d.ts +35 -0
- package/dist/src/extensions/usageBar.d.ts.map +1 -0
- package/dist/src/extensions/usageBar.js +67 -0
- package/dist/src/extensions/usageBar.js.map +1 -0
- package/dist/src/gui/contextRailStore.d.ts +13 -0
- package/dist/src/gui/contextRailStore.d.ts.map +1 -1
- package/dist/src/gui/contextRailStore.js +16 -0
- package/dist/src/gui/contextRailStore.js.map +1 -1
- package/dist/src/gui/electronMain.js +45 -5
- package/dist/src/gui/electronMain.js.map +1 -1
- package/dist/src/gui/guiDesignClientScript.d.ts.map +1 -1
- package/dist/src/gui/guiDesignClientScript.js +31 -0
- package/dist/src/gui/guiDesignClientScript.js.map +1 -1
- package/dist/src/gui/guiUsageRoutingAssets.d.ts +7 -0
- package/dist/src/gui/guiUsageRoutingAssets.d.ts.map +1 -0
- package/dist/src/gui/guiUsageRoutingAssets.js +509 -0
- package/dist/src/gui/guiUsageRoutingAssets.js.map +1 -0
- package/dist/src/gui/guiUsageRoutingHttpController.d.ts +11 -0
- package/dist/src/gui/guiUsageRoutingHttpController.d.ts.map +1 -0
- package/dist/src/gui/guiUsageRoutingHttpController.js +125 -0
- package/dist/src/gui/guiUsageRoutingHttpController.js.map +1 -0
- package/dist/src/gui/guiUsageRoutingRuntime.d.ts +36 -0
- package/dist/src/gui/guiUsageRoutingRuntime.d.ts.map +1 -0
- package/dist/src/gui/guiUsageRoutingRuntime.js +98 -0
- package/dist/src/gui/guiUsageRoutingRuntime.js.map +1 -0
- package/dist/src/gui/hadamardGui.d.ts +5 -0
- package/dist/src/gui/hadamardGui.d.ts.map +1 -1
- package/dist/src/gui/hadamardGui.js +159 -16
- package/dist/src/gui/hadamardGui.js.map +1 -1
- package/dist/src/gui/hadamardGuiAssets.d.ts.map +1 -1
- package/dist/src/gui/hadamardGuiAssets.js +297 -41
- package/dist/src/gui/hadamardGuiAssets.js.map +1 -1
- package/dist/src/index.d.ts +32 -2
- package/dist/src/index.d.ts.map +1 -1
- package/dist/src/index.js +16 -1
- package/dist/src/index.js.map +1 -1
- package/dist/src/keyway/electronSecretStore.d.ts +37 -0
- package/dist/src/keyway/electronSecretStore.d.ts.map +1 -0
- package/dist/src/keyway/electronSecretStore.js +105 -0
- package/dist/src/keyway/electronSecretStore.js.map +1 -0
- package/dist/src/keyway/embeddedKeyway.d.ts +34 -0
- package/dist/src/keyway/embeddedKeyway.d.ts.map +1 -0
- package/dist/src/keyway/embeddedKeyway.js +81 -0
- package/dist/src/keyway/embeddedKeyway.js.map +1 -0
- package/dist/src/keyway/keywayLoopbackGateway.d.ts +30 -0
- package/dist/src/keyway/keywayLoopbackGateway.d.ts.map +1 -0
- package/dist/src/keyway/keywayLoopbackGateway.js +71 -0
- package/dist/src/keyway/keywayLoopbackGateway.js.map +1 -0
- package/dist/src/keyway/keywayMigrationService.d.ts +44 -0
- package/dist/src/keyway/keywayMigrationService.d.ts.map +1 -0
- package/dist/src/keyway/keywayMigrationService.js +219 -0
- package/dist/src/keyway/keywayMigrationService.js.map +1 -0
- package/dist/src/keyway/keywayModelApi.d.ts +24 -0
- package/dist/src/keyway/keywayModelApi.d.ts.map +1 -0
- package/dist/src/keyway/keywayModelApi.js +106 -0
- package/dist/src/keyway/keywayModelApi.js.map +1 -0
- package/dist/src/keyway/keywayPorts.d.ts +189 -0
- package/dist/src/keyway/keywayPorts.d.ts.map +1 -0
- package/dist/src/keyway/keywayPorts.js +2 -0
- package/dist/src/keyway/keywayPorts.js.map +1 -0
- package/dist/src/keyway/keywayProviderExecutor.d.ts +30 -0
- package/dist/src/keyway/keywayProviderExecutor.d.ts.map +1 -0
- package/dist/src/keyway/keywayProviderExecutor.js +257 -0
- package/dist/src/keyway/keywayProviderExecutor.js.map +1 -0
- package/dist/src/keyway/routingComposition.d.ts +18 -0
- package/dist/src/keyway/routingComposition.d.ts.map +1 -0
- package/dist/src/keyway/routingComposition.js +19 -0
- package/dist/src/keyway/routingComposition.js.map +1 -0
- package/dist/src/keyway/usageRoutingAdminService.d.ts +86 -0
- package/dist/src/keyway/usageRoutingAdminService.d.ts.map +1 -0
- package/dist/src/keyway/usageRoutingAdminService.js +384 -0
- package/dist/src/keyway/usageRoutingAdminService.js.map +1 -0
- package/dist/src/nativeCli/crushManagedClient.d.ts +84 -0
- package/dist/src/nativeCli/crushManagedClient.d.ts.map +1 -0
- package/dist/src/nativeCli/crushManagedClient.js +978 -0
- package/dist/src/nativeCli/crushManagedClient.js.map +1 -0
- package/dist/src/nativeCli/crushNativeCliClient.d.ts +19 -0
- package/dist/src/nativeCli/crushNativeCliClient.d.ts.map +1 -0
- package/dist/src/nativeCli/crushNativeCliClient.js +243 -0
- package/dist/src/nativeCli/crushNativeCliClient.js.map +1 -0
- package/dist/src/nativeCli/keywayNativeCliAdapter.d.ts +31 -0
- package/dist/src/nativeCli/keywayNativeCliAdapter.d.ts.map +1 -0
- package/dist/src/nativeCli/keywayNativeCliAdapter.js +35 -0
- package/dist/src/nativeCli/keywayNativeCliAdapter.js.map +1 -0
- package/dist/src/nativeCli/nativeCliAuth.d.ts +24 -0
- package/dist/src/nativeCli/nativeCliAuth.d.ts.map +1 -0
- package/dist/src/nativeCli/nativeCliAuth.js +327 -0
- package/dist/src/nativeCli/nativeCliAuth.js.map +1 -0
- package/dist/src/nativeCli/nativeCliClient.d.ts +16 -0
- package/dist/src/nativeCli/nativeCliClient.d.ts.map +1 -0
- package/dist/src/nativeCli/nativeCliClient.js +575 -0
- package/dist/src/nativeCli/nativeCliClient.js.map +1 -0
- package/dist/src/nativeCli/nativeCliCodewhaleProtocol.d.ts +18 -0
- package/dist/src/nativeCli/nativeCliCodewhaleProtocol.d.ts.map +1 -0
- package/dist/src/nativeCli/nativeCliCodewhaleProtocol.js +376 -0
- package/dist/src/nativeCli/nativeCliCodewhaleProtocol.js.map +1 -0
- package/dist/src/nativeCli/nativeCliContracts.d.ts +22 -0
- package/dist/src/nativeCli/nativeCliContracts.d.ts.map +1 -0
- package/dist/src/nativeCli/nativeCliContracts.js +2 -0
- package/dist/src/nativeCli/nativeCliContracts.js.map +1 -0
- package/dist/src/nativeCli/nativeCliCursorProtocol.d.ts +7 -0
- package/dist/src/nativeCli/nativeCliCursorProtocol.d.ts.map +1 -0
- package/dist/src/nativeCli/nativeCliCursorProtocol.js +210 -0
- package/dist/src/nativeCli/nativeCliCursorProtocol.js.map +1 -0
- package/dist/src/nativeCli/nativeCliEnvironment.d.ts +3 -0
- package/dist/src/nativeCli/nativeCliEnvironment.d.ts.map +1 -0
- package/dist/src/nativeCli/nativeCliEnvironment.js +17 -0
- package/dist/src/nativeCli/nativeCliEnvironment.js.map +1 -0
- package/dist/src/nativeCli/nativeCliExecResolver.d.ts +26 -0
- package/dist/src/nativeCli/nativeCliExecResolver.d.ts.map +1 -0
- package/dist/src/nativeCli/nativeCliExecResolver.js +152 -0
- package/dist/src/nativeCli/nativeCliExecResolver.js.map +1 -0
- package/dist/src/nativeCli/nativeCliPiProtocol.d.ts +15 -0
- package/dist/src/nativeCli/nativeCliPiProtocol.d.ts.map +1 -0
- package/dist/src/nativeCli/nativeCliPiProtocol.js +329 -0
- package/dist/src/nativeCli/nativeCliPiProtocol.js.map +1 -0
- package/dist/src/nativeCli/nativeCliProcessTree.d.ts +3 -0
- package/dist/src/nativeCli/nativeCliProcessTree.d.ts.map +1 -0
- package/dist/src/nativeCli/nativeCliProcessTree.js +104 -0
- package/dist/src/nativeCli/nativeCliProcessTree.js.map +1 -0
- package/dist/src/nativeCli/reasonixAcpSession.d.ts +35 -0
- package/dist/src/nativeCli/reasonixAcpSession.d.ts.map +1 -0
- package/dist/src/nativeCli/reasonixAcpSession.js +472 -0
- package/dist/src/nativeCli/reasonixAcpSession.js.map +1 -0
- package/dist/src/nativeCli/reasonixManagedClient.d.ts +56 -0
- package/dist/src/nativeCli/reasonixManagedClient.d.ts.map +1 -0
- package/dist/src/nativeCli/reasonixManagedClient.js +293 -0
- package/dist/src/nativeCli/reasonixManagedClient.js.map +1 -0
- package/dist/src/nativeCli/reasonixNativeCliClient.d.ts +20 -0
- package/dist/src/nativeCli/reasonixNativeCliClient.d.ts.map +1 -0
- package/dist/src/nativeCli/reasonixNativeCliClient.js +317 -0
- package/dist/src/nativeCli/reasonixNativeCliClient.js.map +1 -0
- package/dist/src/parity/bridgeExecResolver.d.ts +2 -25
- package/dist/src/parity/bridgeExecResolver.d.ts.map +1 -1
- package/dist/src/parity/bridgeExecResolver.js +2 -151
- package/dist/src/parity/bridgeExecResolver.js.map +1 -1
- package/dist/src/parity/bridgeProcessTree.d.ts +2 -2
- package/dist/src/parity/bridgeProcessTree.d.ts.map +1 -1
- package/dist/src/parity/bridgeProcessTree.js +2 -82
- package/dist/src/parity/bridgeProcessTree.js.map +1 -1
- package/dist/src/parity/crushManagedClient.d.ts +2 -83
- package/dist/src/parity/crushManagedClient.d.ts.map +1 -1
- package/dist/src/parity/crushManagedClient.js +2 -977
- package/dist/src/parity/crushManagedClient.js.map +1 -1
- package/dist/src/parity/externalCliAuth.d.ts +2 -22
- package/dist/src/parity/externalCliAuth.d.ts.map +1 -1
- package/dist/src/parity/externalCliAuth.js +2 -295
- package/dist/src/parity/externalCliAuth.js.map +1 -1
- package/dist/src/parity/hadamardBridgeSdk.d.ts.map +1 -1
- package/dist/src/parity/hadamardBridgeSdk.js +14 -1
- package/dist/src/parity/hadamardBridgeSdk.js.map +1 -1
- package/dist/src/parity/nativeCliBridgeAdapter.d.ts +6 -0
- package/dist/src/parity/nativeCliBridgeAdapter.d.ts.map +1 -0
- package/dist/src/parity/nativeCliBridgeAdapter.js +38 -0
- package/dist/src/parity/nativeCliBridgeAdapter.js.map +1 -0
- package/dist/src/parity/reasonixAcpSession.d.ts +2 -34
- package/dist/src/parity/reasonixAcpSession.d.ts.map +1 -1
- package/dist/src/parity/reasonixAcpSession.js +2 -471
- package/dist/src/parity/reasonixAcpSession.js.map +1 -1
- package/dist/src/parity/reasonixManagedClient.d.ts +2 -55
- package/dist/src/parity/reasonixManagedClient.d.ts.map +1 -1
- package/dist/src/parity/reasonixManagedClient.js +2 -292
- package/dist/src/parity/reasonixManagedClient.js.map +1 -1
- package/dist/src/provider/openai-client.js +2 -2
- package/dist/src/provider/openai-client.js.map +1 -1
- package/dist/src/provider/openai-model-api.d.ts.map +1 -1
- package/dist/src/provider/openai-model-api.js +5 -1
- package/dist/src/provider/openai-model-api.js.map +1 -1
- package/dist/src/runtime/agentClient.d.ts +14 -1
- package/dist/src/runtime/agentClient.d.ts.map +1 -1
- package/dist/src/runtime/agentClient.js +60 -6
- package/dist/src/runtime/agentClient.js.map +1 -1
- package/dist/src/runtime/toolPolicyPipeline.d.ts +32 -1
- package/dist/src/runtime/toolPolicyPipeline.d.ts.map +1 -1
- package/dist/src/runtime/toolPolicyPipeline.js +43 -3
- package/dist/src/runtime/toolPolicyPipeline.js.map +1 -1
- package/dist/src/tui/hadamardTuiController.d.ts.map +1 -1
- package/dist/src/tui/hadamardTuiController.js +62 -8
- package/dist/src/tui/hadamardTuiController.js.map +1 -1
- package/dist/src/tui/tuiBasicCommandHandler.d.ts +4 -0
- package/dist/src/tui/tuiBasicCommandHandler.d.ts.map +1 -1
- package/dist/src/tui/tuiBasicCommandHandler.js +23 -2
- package/dist/src/tui/tuiBasicCommandHandler.js.map +1 -1
- package/dist/src/tui/tuiCatalogCommandHandler.d.ts +2 -0
- package/dist/src/tui/tuiCatalogCommandHandler.d.ts.map +1 -1
- package/dist/src/tui/tuiCatalogCommandHandler.js +10 -1
- package/dist/src/tui/tuiCatalogCommandHandler.js.map +1 -1
- package/dist/src/tui/tuiUsageRoutingRuntime.d.ts +22 -0
- package/dist/src/tui/tuiUsageRoutingRuntime.d.ts.map +1 -0
- package/dist/src/tui/tuiUsageRoutingRuntime.js +81 -0
- package/dist/src/tui/tuiUsageRoutingRuntime.js.map +1 -0
- package/dist/src/types.d.ts +16 -0
- package/dist/src/types.d.ts.map +1 -1
- package/dist/src/types.js.map +1 -1
- package/dist/src/ui/commandSurface.d.ts.map +1 -1
- package/dist/src/ui/commandSurface.js +45 -1
- package/dist/src/ui/commandSurface.js.map +1 -1
- package/dist/src/ui/extensionsCommandView.d.ts +18 -0
- package/dist/src/ui/extensionsCommandView.d.ts.map +1 -0
- package/dist/src/ui/extensionsCommandView.js +82 -0
- package/dist/src/ui/extensionsCommandView.js.map +1 -0
- package/dist/src/ui/lspCommandView.d.ts +17 -0
- package/dist/src/ui/lspCommandView.d.ts.map +1 -0
- package/dist/src/ui/lspCommandView.js +25 -0
- package/dist/src/ui/lspCommandView.js.map +1 -0
- package/dist/src/ui/usageRoutingCommand.d.ts +13 -0
- package/dist/src/ui/usageRoutingCommand.d.ts.map +1 -0
- package/dist/src/ui/usageRoutingCommand.js +224 -0
- package/dist/src/ui/usageRoutingCommand.js.map +1 -0
- package/dist/src/usage/budgetService.d.ts +28 -0
- package/dist/src/usage/budgetService.d.ts.map +1 -0
- package/dist/src/usage/budgetService.js +50 -0
- package/dist/src/usage/budgetService.js.map +1 -0
- package/dist/src/usage/contracts.d.ts +149 -0
- package/dist/src/usage/contracts.d.ts.map +1 -0
- package/dist/src/usage/contracts.js +2 -0
- package/dist/src/usage/contracts.js.map +1 -0
- package/dist/src/usage/index.d.ts +7 -0
- package/dist/src/usage/index.d.ts.map +1 -0
- package/dist/src/usage/index.js +4 -0
- package/dist/src/usage/index.js.map +1 -0
- package/dist/src/usage/usageLedger.d.ts +19 -0
- package/dist/src/usage/usageLedger.d.ts.map +1 -0
- package/dist/src/usage/usageLedger.js +389 -0
- package/dist/src/usage/usageLedger.js.map +1 -0
- package/dist/src/usage/usageQueryService.d.ts +12 -0
- package/dist/src/usage/usageQueryService.d.ts.map +1 -0
- package/dist/src/usage/usageQueryService.js +29 -0
- package/dist/src/usage/usageQueryService.js.map +1 -0
- package/package.json +14 -2
- package/scripts/prepare-gui-launcher.mjs +62 -0
|
@@ -0,0 +1,978 @@
|
|
|
1
|
+
import { spawn } from 'node:child_process';
|
|
2
|
+
import { randomUUID } from 'node:crypto';
|
|
3
|
+
import { chmod, mkdtemp, rm } from 'node:fs/promises';
|
|
4
|
+
import { request as nodeHttpRequest } from 'node:http';
|
|
5
|
+
import os from 'node:os';
|
|
6
|
+
import path from 'node:path';
|
|
7
|
+
import { findExecutableOnPath, IS_WINDOWS, resolveExecutableInvocation, } from './nativeCliExecResolver.js';
|
|
8
|
+
import { terminateManagedProcessTree } from './nativeCliProcessTree.js';
|
|
9
|
+
const HEALTH_TIMEOUT_MS = 10_000;
|
|
10
|
+
const AGENT_READY_TIMEOUT_MS = 30_000;
|
|
11
|
+
const REQUEST_TIMEOUT_MS = 10_000;
|
|
12
|
+
const CLEANUP_TIMEOUT_MS = 2_000;
|
|
13
|
+
const MAX_RESPONSE_BYTES = 1024 * 1024;
|
|
14
|
+
const MAX_SSE_EVENT_BYTES = 512 * 1024;
|
|
15
|
+
const MAX_STDERR_BYTES = 64 * 1024;
|
|
16
|
+
const UNIX_SOCKET_PATH_LIMIT = 103;
|
|
17
|
+
const CRUSH_CONFIG_SCOPE_GLOBAL = 0;
|
|
18
|
+
const CRUSH_CONFIG_SCOPE_WORKSPACE = 1;
|
|
19
|
+
const SECRET_KEY_PATTERN = /(?:api.?key|access.?key|private.?key|token|authorization|password|secret|cookie|credential)/i;
|
|
20
|
+
/**
|
|
21
|
+
* Runs Crush through its official local server protocol (v0.80-v0.84).
|
|
22
|
+
* The prompt is sent in a JSON request body and never appears in argv.
|
|
23
|
+
*/
|
|
24
|
+
export async function runCrushManaged(options, onEvent) {
|
|
25
|
+
validateRunOptions(options);
|
|
26
|
+
throwIfAborted(options.signal);
|
|
27
|
+
const transport = await (options.transportFactory ?? createPrivateCrushTransport)();
|
|
28
|
+
assertCrushLocalTransport(transport);
|
|
29
|
+
const spawnFn = options.spawnFn ?? defaultSpawn;
|
|
30
|
+
const httpRequest = options.httpRequest ?? defaultCrushHttpRequest;
|
|
31
|
+
const executable = options.executable?.trim() || 'crush';
|
|
32
|
+
const secrets = secretValues(options.env, options.apiKey == null ? [] : [options.apiKey]);
|
|
33
|
+
const clientId = randomUUID();
|
|
34
|
+
const runId = randomUUID();
|
|
35
|
+
const stderr = createStderrCollector(secrets);
|
|
36
|
+
const sseController = new AbortController();
|
|
37
|
+
let child;
|
|
38
|
+
let workspaceId;
|
|
39
|
+
let sessionId;
|
|
40
|
+
let runSubmitted = false;
|
|
41
|
+
let runCompleted = false;
|
|
42
|
+
let streamIsError = false;
|
|
43
|
+
let childSpawnError;
|
|
44
|
+
const emit = async (event) => {
|
|
45
|
+
await onEvent(redactEvent(event, secrets));
|
|
46
|
+
};
|
|
47
|
+
try {
|
|
48
|
+
const invocation = await resolveCrushInvocation(executable, [...(options.executableArgs ?? []), 'server', '--host', transport.serverHost]);
|
|
49
|
+
child = spawnFn(invocation.file, invocation.args, {
|
|
50
|
+
cwd: options.cwd,
|
|
51
|
+
env: options.inheritEnvironment === false
|
|
52
|
+
? { ...(options.env ?? {}) }
|
|
53
|
+
: { ...process.env, ...(options.env ?? {}) },
|
|
54
|
+
shell: false,
|
|
55
|
+
detached: !IS_WINDOWS,
|
|
56
|
+
windowsHide: true,
|
|
57
|
+
stdio: ['ignore', 'ignore', 'pipe'],
|
|
58
|
+
});
|
|
59
|
+
stderr.attach(child);
|
|
60
|
+
child.once('error', error => {
|
|
61
|
+
childSpawnError = error;
|
|
62
|
+
});
|
|
63
|
+
await raceWithAbort(waitForHealth(httpRequest, transport.socketPath, child, () => childSpawnError, options.healthTimeoutMs ?? HEALTH_TIMEOUT_MS, options.signal), options.signal);
|
|
64
|
+
const workspace = await jsonRequest(httpRequest, {
|
|
65
|
+
socketPath: transport.socketPath,
|
|
66
|
+
method: 'POST',
|
|
67
|
+
path: '/v1/workspaces',
|
|
68
|
+
body: JSON.stringify({ path: options.cwd, client_id: clientId }),
|
|
69
|
+
signal: options.signal,
|
|
70
|
+
}, [200]);
|
|
71
|
+
workspaceId = requiredIdentifier(workspace.id, 'Crush workspace id');
|
|
72
|
+
await configureCrushWorkspace(httpRequest, transport.socketPath, workspaceId, options);
|
|
73
|
+
sessionId = options.nativeSessionId
|
|
74
|
+
? await resolveExistingSession(httpRequest, transport.socketPath, workspaceId, options.nativeSessionId, options.signal)
|
|
75
|
+
: requiredIdentifier((await jsonRequest(httpRequest, {
|
|
76
|
+
socketPath: transport.socketPath,
|
|
77
|
+
method: 'POST',
|
|
78
|
+
path: `/v1/workspaces/${encodeURIComponent(workspaceId)}/sessions`,
|
|
79
|
+
body: JSON.stringify({ title: 'Hadamard managed run' }),
|
|
80
|
+
signal: options.signal,
|
|
81
|
+
}, [200])).id, 'Crush session id');
|
|
82
|
+
const permissionMode = options.permissionMode ?? 'default';
|
|
83
|
+
await emptyRequest(httpRequest, {
|
|
84
|
+
socketPath: transport.socketPath,
|
|
85
|
+
method: 'POST',
|
|
86
|
+
path: `/v1/workspaces/${encodeURIComponent(workspaceId)}/permissions/skip`,
|
|
87
|
+
body: JSON.stringify({ skip: permissionMode === 'bypassPermissions' }),
|
|
88
|
+
signal: options.signal,
|
|
89
|
+
}, [200]);
|
|
90
|
+
await emptyRequest(httpRequest, {
|
|
91
|
+
socketPath: transport.socketPath,
|
|
92
|
+
method: 'POST',
|
|
93
|
+
path: `/v1/workspaces/${encodeURIComponent(workspaceId)}/agent/init`,
|
|
94
|
+
signal: options.signal,
|
|
95
|
+
}, [200]);
|
|
96
|
+
await raceWithAbort(waitForAgentReady(httpRequest, transport.socketPath, workspaceId, options.agentReadyTimeoutMs ?? AGENT_READY_TIMEOUT_MS, options.signal), options.signal);
|
|
97
|
+
const streamResponse = await raceWithAbort(httpRequest({
|
|
98
|
+
socketPath: transport.socketPath,
|
|
99
|
+
method: 'GET',
|
|
100
|
+
path: `/v1/workspaces/${encodeURIComponent(workspaceId)}/events?client_id=${encodeURIComponent(clientId)}`,
|
|
101
|
+
headers: {
|
|
102
|
+
Accept: 'text/event-stream',
|
|
103
|
+
'Cache-Control': 'no-cache',
|
|
104
|
+
Connection: 'keep-alive',
|
|
105
|
+
},
|
|
106
|
+
signal: sseController.signal,
|
|
107
|
+
timeoutMs: 0,
|
|
108
|
+
}), options.signal);
|
|
109
|
+
assertStatus(streamResponse, [200], 'subscribe to Crush events');
|
|
110
|
+
await emptyRequest(httpRequest, {
|
|
111
|
+
socketPath: transport.socketPath,
|
|
112
|
+
method: 'POST',
|
|
113
|
+
path: `/v1/workspaces/${encodeURIComponent(workspaceId)}/current-session?client_id=${encodeURIComponent(clientId)}`,
|
|
114
|
+
body: JSON.stringify({ session_id: sessionId }),
|
|
115
|
+
signal: options.signal,
|
|
116
|
+
}, [200]);
|
|
117
|
+
await emit({
|
|
118
|
+
type: 'system',
|
|
119
|
+
subtype: 'init',
|
|
120
|
+
session_id: sessionId,
|
|
121
|
+
cwd: options.cwd,
|
|
122
|
+
runtime: 'crush',
|
|
123
|
+
permission_mode: permissionMode,
|
|
124
|
+
});
|
|
125
|
+
await emptyRequest(httpRequest, {
|
|
126
|
+
socketPath: transport.socketPath,
|
|
127
|
+
method: 'POST',
|
|
128
|
+
path: `/v1/workspaces/${encodeURIComponent(workspaceId)}/agent`,
|
|
129
|
+
body: JSON.stringify({ session_id: sessionId, run_id: runId, prompt: options.prompt }),
|
|
130
|
+
signal: options.signal,
|
|
131
|
+
}, [200, 202]);
|
|
132
|
+
runSubmitted = true;
|
|
133
|
+
const streamResult = await raceWithAbort(consumeCrushEventStream({
|
|
134
|
+
response: streamResponse,
|
|
135
|
+
httpRequest,
|
|
136
|
+
socketPath: transport.socketPath,
|
|
137
|
+
permissionMode,
|
|
138
|
+
permissionHandler: options.permissionHandler,
|
|
139
|
+
scope: { workspaceId, clientId, sessionId, runId },
|
|
140
|
+
emit,
|
|
141
|
+
signal: sseController.signal,
|
|
142
|
+
}), options.signal);
|
|
143
|
+
runCompleted = true;
|
|
144
|
+
streamIsError = streamResult.isError;
|
|
145
|
+
}
|
|
146
|
+
finally {
|
|
147
|
+
// A submitted run must be canceled before its owning process tree is torn down.
|
|
148
|
+
if (child && workspaceId && sessionId && runSubmitted && !runCompleted) {
|
|
149
|
+
await bestEffortWithTimeout(emptyRequest(httpRequest, {
|
|
150
|
+
socketPath: transport.socketPath,
|
|
151
|
+
method: 'POST',
|
|
152
|
+
path: `/v1/workspaces/${encodeURIComponent(workspaceId)}/agent/sessions/${encodeURIComponent(sessionId)}/cancel`,
|
|
153
|
+
}, [200]), CLEANUP_TIMEOUT_MS);
|
|
154
|
+
}
|
|
155
|
+
sseController.abort();
|
|
156
|
+
if (child && workspaceId) {
|
|
157
|
+
await bestEffortWithTimeout(emptyRequest(httpRequest, {
|
|
158
|
+
socketPath: transport.socketPath,
|
|
159
|
+
method: 'POST',
|
|
160
|
+
path: `/v1/workspaces/${encodeURIComponent(workspaceId)}/current-session?client_id=${encodeURIComponent(clientId)}`,
|
|
161
|
+
body: JSON.stringify({ session_id: '' }),
|
|
162
|
+
}, [200]), CLEANUP_TIMEOUT_MS);
|
|
163
|
+
}
|
|
164
|
+
if (child) {
|
|
165
|
+
await bestEffortWithTimeout(emptyRequest(httpRequest, {
|
|
166
|
+
socketPath: transport.socketPath,
|
|
167
|
+
method: 'POST',
|
|
168
|
+
path: '/v1/control',
|
|
169
|
+
body: JSON.stringify({ command: 'shutdown' }),
|
|
170
|
+
}, [200]), CLEANUP_TIMEOUT_MS);
|
|
171
|
+
await terminateManagedProcessTree(child);
|
|
172
|
+
}
|
|
173
|
+
await transport.cleanup?.();
|
|
174
|
+
}
|
|
175
|
+
return {
|
|
176
|
+
sessionId: requiredIdentifier(sessionId, 'Crush session id'),
|
|
177
|
+
stderr: stderr.value(),
|
|
178
|
+
exitCode: child?.exitCode ?? (streamIsError ? 1 : 0),
|
|
179
|
+
};
|
|
180
|
+
}
|
|
181
|
+
/** Creates the private local-only transport used by production runs. */
|
|
182
|
+
export async function createPrivateCrushTransport() {
|
|
183
|
+
if (process.platform === 'win32') {
|
|
184
|
+
const name = `hadamard-crush-${randomUUID()}`;
|
|
185
|
+
return {
|
|
186
|
+
serverHost: `npipe:////./pipe/${name}`,
|
|
187
|
+
socketPath: `\\\\.\\pipe\\${name}`,
|
|
188
|
+
};
|
|
189
|
+
}
|
|
190
|
+
const directory = await mkdtemp(path.join(os.tmpdir(), 'hadamard-crush-'));
|
|
191
|
+
await chmod(directory, 0o700);
|
|
192
|
+
const socketPath = path.join(directory, 'server.sock');
|
|
193
|
+
if (Buffer.byteLength(socketPath, 'utf8') > UNIX_SOCKET_PATH_LIMIT) {
|
|
194
|
+
await rm(directory, { recursive: true, force: true });
|
|
195
|
+
throw new Error('The private Crush Unix socket path exceeds the platform limit.');
|
|
196
|
+
}
|
|
197
|
+
return {
|
|
198
|
+
serverHost: `unix://${socketPath}`,
|
|
199
|
+
socketPath,
|
|
200
|
+
cleanup: async () => {
|
|
201
|
+
await rm(directory, { recursive: true, force: true });
|
|
202
|
+
},
|
|
203
|
+
};
|
|
204
|
+
}
|
|
205
|
+
/** Rejects TCP and mismatched local socket descriptions. */
|
|
206
|
+
export function assertCrushLocalTransport(transport) {
|
|
207
|
+
if (transport.serverHost.startsWith('tcp://')) {
|
|
208
|
+
throw new TypeError('Crush managed mode refuses TCP transports.');
|
|
209
|
+
}
|
|
210
|
+
if (transport.serverHost.startsWith('unix://')) {
|
|
211
|
+
const expected = transport.serverHost.slice('unix://'.length);
|
|
212
|
+
if (!path.isAbsolute(expected) || expected !== transport.socketPath) {
|
|
213
|
+
throw new TypeError('Crush Unix transport must use one matching absolute socket path.');
|
|
214
|
+
}
|
|
215
|
+
return;
|
|
216
|
+
}
|
|
217
|
+
if (transport.serverHost.startsWith('npipe:////./pipe/')) {
|
|
218
|
+
const name = transport.serverHost.slice('npipe:////./pipe/'.length);
|
|
219
|
+
if (!name || transport.socketPath !== `\\\\.\\pipe\\${name}`) {
|
|
220
|
+
throw new TypeError('Crush named-pipe transport has mismatched endpoints.');
|
|
221
|
+
}
|
|
222
|
+
return;
|
|
223
|
+
}
|
|
224
|
+
throw new TypeError('Crush managed mode requires a Unix socket or Windows named pipe.');
|
|
225
|
+
}
|
|
226
|
+
/** Deterministic, fail-closed mapping used when no UI permission handler is supplied. */
|
|
227
|
+
export function decideCrushPermission(mode, request) {
|
|
228
|
+
if (mode === 'bypassPermissions')
|
|
229
|
+
return 'allow';
|
|
230
|
+
if (mode === 'acceptEdits'
|
|
231
|
+
&& request.tool_name != null
|
|
232
|
+
&& ['edit', 'write', 'multiedit'].includes(request.tool_name)) {
|
|
233
|
+
return 'allow_session';
|
|
234
|
+
}
|
|
235
|
+
return 'deny';
|
|
236
|
+
}
|
|
237
|
+
export const defaultCrushHttpRequest = options => new Promise((resolve, reject) => {
|
|
238
|
+
const request = nodeHttpRequest({
|
|
239
|
+
socketPath: options.socketPath,
|
|
240
|
+
method: options.method,
|
|
241
|
+
path: options.path,
|
|
242
|
+
headers: options.headers,
|
|
243
|
+
signal: options.signal,
|
|
244
|
+
}, response => {
|
|
245
|
+
resolve({
|
|
246
|
+
statusCode: response.statusCode ?? 0,
|
|
247
|
+
headers: response.headers,
|
|
248
|
+
body: response,
|
|
249
|
+
});
|
|
250
|
+
});
|
|
251
|
+
request.once('error', reject);
|
|
252
|
+
if (options.timeoutMs !== 0) {
|
|
253
|
+
request.setTimeout(options.timeoutMs ?? REQUEST_TIMEOUT_MS, () => {
|
|
254
|
+
request.destroy(new Error(`Crush HTTP request timed out: ${options.method} ${options.path}`));
|
|
255
|
+
});
|
|
256
|
+
}
|
|
257
|
+
if (options.body != null)
|
|
258
|
+
request.write(options.body);
|
|
259
|
+
request.end();
|
|
260
|
+
});
|
|
261
|
+
function defaultSpawn(executable, args, options) {
|
|
262
|
+
return spawn(executable, [...args], options);
|
|
263
|
+
}
|
|
264
|
+
async function resolveCrushInvocation(executable, args) {
|
|
265
|
+
const configuredPath = path.isAbsolute(executable)
|
|
266
|
+
? executable
|
|
267
|
+
: await findExecutableOnPath(executable) ?? executable;
|
|
268
|
+
return resolveExecutableInvocation(configuredPath, args);
|
|
269
|
+
}
|
|
270
|
+
function validateRunOptions(options) {
|
|
271
|
+
if (!options.cwd || !path.isAbsolute(options.cwd)) {
|
|
272
|
+
throw new TypeError('Crush managed mode requires an absolute cwd.');
|
|
273
|
+
}
|
|
274
|
+
if (typeof options.prompt !== 'string' || options.prompt.length === 0) {
|
|
275
|
+
throw new TypeError('Crush managed mode requires a non-empty prompt.');
|
|
276
|
+
}
|
|
277
|
+
if (options.nativeSessionId != null)
|
|
278
|
+
validateSessionId(options.nativeSessionId);
|
|
279
|
+
if (options.model != null)
|
|
280
|
+
validateCrushModel(options.model);
|
|
281
|
+
if (options.credentialProvider != null)
|
|
282
|
+
validateCrushProvider(options.credentialProvider);
|
|
283
|
+
if (options.apiKey != null)
|
|
284
|
+
validateCrushApiKey(options.apiKey);
|
|
285
|
+
if (options.baseURL != null)
|
|
286
|
+
validateCrushBaseURL(options.baseURL);
|
|
287
|
+
}
|
|
288
|
+
async function configureCrushWorkspace(httpRequest, socketPath, workspaceId, options) {
|
|
289
|
+
if (options.model == null
|
|
290
|
+
&& options.apiKey == null
|
|
291
|
+
&& options.baseURL == null) {
|
|
292
|
+
return;
|
|
293
|
+
}
|
|
294
|
+
if (!options.credentialProvider) {
|
|
295
|
+
throw new TypeError('Crush managed model or API-key configuration requires an explicit native provider id.');
|
|
296
|
+
}
|
|
297
|
+
const provider = validateCrushProvider(options.credentialProvider);
|
|
298
|
+
const workspacePath = `/v1/workspaces/${encodeURIComponent(workspaceId)}`;
|
|
299
|
+
const modelScope = options.apiKey != null || options.baseURL != null
|
|
300
|
+
? CRUSH_CONFIG_SCOPE_GLOBAL
|
|
301
|
+
: CRUSH_CONFIG_SCOPE_WORKSPACE;
|
|
302
|
+
if (options.baseURL != null) {
|
|
303
|
+
await emptyRequest(httpRequest, {
|
|
304
|
+
socketPath,
|
|
305
|
+
method: 'POST',
|
|
306
|
+
path: `${workspacePath}/config/set`,
|
|
307
|
+
body: JSON.stringify({
|
|
308
|
+
scope: CRUSH_CONFIG_SCOPE_GLOBAL,
|
|
309
|
+
key: `providers.${provider}.base_url`,
|
|
310
|
+
value: validateCrushBaseURL(options.baseURL),
|
|
311
|
+
}),
|
|
312
|
+
signal: options.signal,
|
|
313
|
+
}, [200]);
|
|
314
|
+
}
|
|
315
|
+
if (options.apiKey != null) {
|
|
316
|
+
await emptyRequest(httpRequest, {
|
|
317
|
+
socketPath,
|
|
318
|
+
method: 'POST',
|
|
319
|
+
path: `${workspacePath}/config/provider-key`,
|
|
320
|
+
body: JSON.stringify({
|
|
321
|
+
scope: CRUSH_CONFIG_SCOPE_GLOBAL,
|
|
322
|
+
provider_id: provider,
|
|
323
|
+
kind: 'string',
|
|
324
|
+
api_key: validateCrushApiKey(options.apiKey),
|
|
325
|
+
}),
|
|
326
|
+
signal: options.signal,
|
|
327
|
+
}, [200]);
|
|
328
|
+
}
|
|
329
|
+
if (options.model != null) {
|
|
330
|
+
await emptyRequest(httpRequest, {
|
|
331
|
+
socketPath,
|
|
332
|
+
method: 'POST',
|
|
333
|
+
path: `${workspacePath}/config/model`,
|
|
334
|
+
body: JSON.stringify({
|
|
335
|
+
scope: modelScope,
|
|
336
|
+
model_type: 'large',
|
|
337
|
+
model: {
|
|
338
|
+
provider,
|
|
339
|
+
model: validateCrushModel(options.model),
|
|
340
|
+
},
|
|
341
|
+
}),
|
|
342
|
+
signal: options.signal,
|
|
343
|
+
}, [200]);
|
|
344
|
+
}
|
|
345
|
+
}
|
|
346
|
+
function validateCrushProvider(value) {
|
|
347
|
+
const provider = value.trim().toLowerCase();
|
|
348
|
+
if (!provider
|
|
349
|
+
|| provider.length > 128
|
|
350
|
+
|| !/^[a-z0-9][a-z0-9_-]*$/u.test(provider)) {
|
|
351
|
+
throw new TypeError('Crush credential provider must be a native provider id.');
|
|
352
|
+
}
|
|
353
|
+
return provider;
|
|
354
|
+
}
|
|
355
|
+
function validateCrushModel(value) {
|
|
356
|
+
const model = value.trim();
|
|
357
|
+
if (!model
|
|
358
|
+
|| model.length > 512
|
|
359
|
+
|| /[\u0000-\u001f\u007f]/u.test(model)) {
|
|
360
|
+
throw new TypeError('Crush model must be a non-empty native model id.');
|
|
361
|
+
}
|
|
362
|
+
return model;
|
|
363
|
+
}
|
|
364
|
+
function validateCrushApiKey(value) {
|
|
365
|
+
if (!value.trim() || value.length > 16 * 1024 || /[\r\n\u0000]/u.test(value)) {
|
|
366
|
+
throw new TypeError('Crush API key must be a non-empty single-line value.');
|
|
367
|
+
}
|
|
368
|
+
return value;
|
|
369
|
+
}
|
|
370
|
+
function validateCrushBaseURL(value) {
|
|
371
|
+
const normalized = value.trim();
|
|
372
|
+
if (!normalized || normalized.length > 2_048) {
|
|
373
|
+
throw new TypeError('Crush base URL is missing or too long.');
|
|
374
|
+
}
|
|
375
|
+
let parsed;
|
|
376
|
+
try {
|
|
377
|
+
parsed = new URL(normalized);
|
|
378
|
+
}
|
|
379
|
+
catch {
|
|
380
|
+
throw new TypeError('Crush base URL must be an absolute HTTP(S) URL.');
|
|
381
|
+
}
|
|
382
|
+
if ((parsed.protocol !== 'http:' && parsed.protocol !== 'https:')
|
|
383
|
+
|| parsed.username
|
|
384
|
+
|| parsed.password) {
|
|
385
|
+
throw new TypeError('Crush base URL must be an absolute HTTP(S) URL without credentials.');
|
|
386
|
+
}
|
|
387
|
+
return normalized.replace(/\/$/u, '');
|
|
388
|
+
}
|
|
389
|
+
async function resolveExistingSession(httpRequest, socketPath, workspaceId, nativeSessionId, signal) {
|
|
390
|
+
const requested = validateSessionId(nativeSessionId);
|
|
391
|
+
const session = await jsonRequest(httpRequest, {
|
|
392
|
+
socketPath,
|
|
393
|
+
method: 'GET',
|
|
394
|
+
path: `/v1/workspaces/${encodeURIComponent(workspaceId)}/sessions/${encodeURIComponent(requested)}`,
|
|
395
|
+
signal,
|
|
396
|
+
}, [200]);
|
|
397
|
+
const returned = requiredIdentifier(session.id, 'Crush session id');
|
|
398
|
+
if (returned !== requested) {
|
|
399
|
+
throw new Error('Crush returned a different session than the exact requested id.');
|
|
400
|
+
}
|
|
401
|
+
return returned;
|
|
402
|
+
}
|
|
403
|
+
function validateSessionId(value) {
|
|
404
|
+
const sessionId = value.trim();
|
|
405
|
+
if (!sessionId
|
|
406
|
+
|| sessionId.length > 256
|
|
407
|
+
|| sessionId.startsWith('-')
|
|
408
|
+
|| !/^[A-Za-z0-9][A-Za-z0-9_-]*$/u.test(sessionId)) {
|
|
409
|
+
throw new TypeError('Crush session id must be a non-option identifier.');
|
|
410
|
+
}
|
|
411
|
+
return sessionId;
|
|
412
|
+
}
|
|
413
|
+
function requiredIdentifier(value, label) {
|
|
414
|
+
if (typeof value !== 'string' || value.length === 0 || value.length > 512) {
|
|
415
|
+
throw new Error(`${label} is missing or invalid.`);
|
|
416
|
+
}
|
|
417
|
+
return value;
|
|
418
|
+
}
|
|
419
|
+
async function waitForHealth(httpRequest, socketPath, child, getSpawnError, timeoutMs, signal) {
|
|
420
|
+
const deadline = Date.now() + normalizeTimeout(timeoutMs, HEALTH_TIMEOUT_MS);
|
|
421
|
+
let lastError;
|
|
422
|
+
while (Date.now() < deadline) {
|
|
423
|
+
throwIfAborted(signal);
|
|
424
|
+
if (getSpawnError()) {
|
|
425
|
+
throw new Error('Crush server process failed to start.');
|
|
426
|
+
}
|
|
427
|
+
if (child.exitCode !== null || child.signalCode !== null) {
|
|
428
|
+
throw new Error('Crush server exited before its health endpoint became ready.');
|
|
429
|
+
}
|
|
430
|
+
try {
|
|
431
|
+
const response = await httpRequest({
|
|
432
|
+
socketPath,
|
|
433
|
+
method: 'GET',
|
|
434
|
+
path: '/v1/health',
|
|
435
|
+
signal,
|
|
436
|
+
timeoutMs: 500,
|
|
437
|
+
});
|
|
438
|
+
if (response.statusCode === 200) {
|
|
439
|
+
await discardBody(response.body);
|
|
440
|
+
return;
|
|
441
|
+
}
|
|
442
|
+
await discardBody(response.body);
|
|
443
|
+
lastError = new Error(`health status ${response.statusCode}`);
|
|
444
|
+
}
|
|
445
|
+
catch (error) {
|
|
446
|
+
lastError = error;
|
|
447
|
+
}
|
|
448
|
+
await abortableDelay(50, signal);
|
|
449
|
+
}
|
|
450
|
+
throw new Error(`Crush server health check timed out${lastError ? '.' : ''}`);
|
|
451
|
+
}
|
|
452
|
+
async function waitForAgentReady(httpRequest, socketPath, workspaceId, timeoutMs, signal) {
|
|
453
|
+
const deadline = Date.now() + normalizeTimeout(timeoutMs, AGENT_READY_TIMEOUT_MS);
|
|
454
|
+
while (Date.now() < deadline) {
|
|
455
|
+
throwIfAborted(signal);
|
|
456
|
+
try {
|
|
457
|
+
const info = await jsonRequest(httpRequest, {
|
|
458
|
+
socketPath,
|
|
459
|
+
method: 'GET',
|
|
460
|
+
path: `/v1/workspaces/${encodeURIComponent(workspaceId)}/agent`,
|
|
461
|
+
signal,
|
|
462
|
+
timeoutMs: 1_000,
|
|
463
|
+
}, [200]);
|
|
464
|
+
if (info.is_ready === true)
|
|
465
|
+
return;
|
|
466
|
+
}
|
|
467
|
+
catch {
|
|
468
|
+
// Initialization is asynchronous; retry until the bounded deadline.
|
|
469
|
+
}
|
|
470
|
+
await abortableDelay(50, signal);
|
|
471
|
+
}
|
|
472
|
+
throw new Error('Crush agent initialization timed out.');
|
|
473
|
+
}
|
|
474
|
+
function normalizeTimeout(value, fallback) {
|
|
475
|
+
return Number.isFinite(value) && value > 0 ? Math.floor(value) : fallback;
|
|
476
|
+
}
|
|
477
|
+
async function consumeCrushEventStream(options) {
|
|
478
|
+
const state = {
|
|
479
|
+
text: '',
|
|
480
|
+
textByMessage: new Map(),
|
|
481
|
+
reasoningByMessage: new Map(),
|
|
482
|
+
toolCalls: new Map(),
|
|
483
|
+
toolResults: new Set(),
|
|
484
|
+
};
|
|
485
|
+
try {
|
|
486
|
+
for await (const data of parseSseData(options.response.body, options.signal)) {
|
|
487
|
+
const parsed = parseCrushEvent(data);
|
|
488
|
+
if (!parsed || !eventMatchesScope(parsed, options.scope))
|
|
489
|
+
continue;
|
|
490
|
+
if (parsed.kind === 'message') {
|
|
491
|
+
await normalizeMessage(parsed.data, options.scope, state, options.emit);
|
|
492
|
+
continue;
|
|
493
|
+
}
|
|
494
|
+
if (parsed.kind === 'permission_request') {
|
|
495
|
+
const permission = asPermissionRequest(parsed.data);
|
|
496
|
+
if (!permission || permission.session_id !== options.scope.sessionId)
|
|
497
|
+
continue;
|
|
498
|
+
await options.emit({
|
|
499
|
+
type: 'system',
|
|
500
|
+
subtype: 'permission_request',
|
|
501
|
+
session_id: options.scope.sessionId,
|
|
502
|
+
permission: {
|
|
503
|
+
id: permission.id,
|
|
504
|
+
tool_call_id: permission.tool_call_id,
|
|
505
|
+
tool_name: permission.tool_name,
|
|
506
|
+
description: permission.description,
|
|
507
|
+
action: permission.action,
|
|
508
|
+
path: permission.path,
|
|
509
|
+
params: permission.params,
|
|
510
|
+
},
|
|
511
|
+
});
|
|
512
|
+
const decision = options.permissionHandler
|
|
513
|
+
? await options.permissionHandler(permission)
|
|
514
|
+
: decideCrushPermission(options.permissionMode, permission);
|
|
515
|
+
assertPermissionDecision(decision);
|
|
516
|
+
await emptyRequest(options.httpRequest, {
|
|
517
|
+
socketPath: options.socketPath,
|
|
518
|
+
method: 'POST',
|
|
519
|
+
path: `/v1/workspaces/${encodeURIComponent(options.scope.workspaceId)}/permissions/grant`,
|
|
520
|
+
body: JSON.stringify({ permission, action: decision }),
|
|
521
|
+
signal: options.signal,
|
|
522
|
+
}, [200]);
|
|
523
|
+
continue;
|
|
524
|
+
}
|
|
525
|
+
if (parsed.kind === 'permission_notification') {
|
|
526
|
+
await options.emit({
|
|
527
|
+
type: 'system',
|
|
528
|
+
subtype: 'permission_notification',
|
|
529
|
+
session_id: options.scope.sessionId,
|
|
530
|
+
tool_call_id: stringValue(parsed.data.tool_call_id),
|
|
531
|
+
granted: parsed.data.granted === true,
|
|
532
|
+
denied: parsed.data.denied === true,
|
|
533
|
+
});
|
|
534
|
+
continue;
|
|
535
|
+
}
|
|
536
|
+
if (parsed.kind === 'agent_event' && parsed.data.type === 'error') {
|
|
537
|
+
await options.emit({
|
|
538
|
+
type: 'error',
|
|
539
|
+
subtype: 'runtime_error',
|
|
540
|
+
session_id: options.scope.sessionId,
|
|
541
|
+
error: stringValue(parsed.data.error) ?? 'Crush agent failed.',
|
|
542
|
+
});
|
|
543
|
+
continue;
|
|
544
|
+
}
|
|
545
|
+
if (parsed.kind === 'run_complete') {
|
|
546
|
+
const error = stringValue(parsed.data.error);
|
|
547
|
+
const cancelled = parsed.data.cancelled === true;
|
|
548
|
+
const isError = Boolean(error) || cancelled;
|
|
549
|
+
if (isError) {
|
|
550
|
+
await options.emit({
|
|
551
|
+
type: 'error',
|
|
552
|
+
subtype: cancelled ? 'cancelled' : 'runtime_error',
|
|
553
|
+
session_id: options.scope.sessionId,
|
|
554
|
+
error: error || 'Crush run was cancelled.',
|
|
555
|
+
});
|
|
556
|
+
}
|
|
557
|
+
await options.emit({
|
|
558
|
+
type: 'result',
|
|
559
|
+
subtype: isError ? 'error' : 'success',
|
|
560
|
+
session_id: options.scope.sessionId,
|
|
561
|
+
is_error: isError,
|
|
562
|
+
result: error || stringValue(parsed.data.text) || state.text,
|
|
563
|
+
stop_reason: cancelled ? 'cancelled' : isError ? 'error' : 'end_turn',
|
|
564
|
+
num_turns: 1,
|
|
565
|
+
});
|
|
566
|
+
return { isError };
|
|
567
|
+
}
|
|
568
|
+
}
|
|
569
|
+
}
|
|
570
|
+
catch (error) {
|
|
571
|
+
if (options.signal.aborted)
|
|
572
|
+
return { isError: true };
|
|
573
|
+
throw error;
|
|
574
|
+
}
|
|
575
|
+
if (options.signal.aborted)
|
|
576
|
+
return { isError: true };
|
|
577
|
+
throw new Error('Crush event stream ended before a matching run_complete event.');
|
|
578
|
+
}
|
|
579
|
+
async function normalizeMessage(message, scope, state, emit) {
|
|
580
|
+
const role = stringValue(message.role);
|
|
581
|
+
if (role !== 'assistant' && role !== 'tool')
|
|
582
|
+
return;
|
|
583
|
+
const messageId = stringValue(message.id) ?? 'crush-message';
|
|
584
|
+
const parts = Array.isArray(message.parts) ? message.parts : [];
|
|
585
|
+
for (let index = 0; index < parts.length; index += 1) {
|
|
586
|
+
const wrapper = asRecord(parts[index]);
|
|
587
|
+
if (!wrapper)
|
|
588
|
+
continue;
|
|
589
|
+
const type = stringValue(wrapper.type);
|
|
590
|
+
const data = asRecord(wrapper.data) ?? wrapper;
|
|
591
|
+
if (type === 'text' && typeof data.text === 'string') {
|
|
592
|
+
const previous = state.textByMessage.get(messageId) ?? '';
|
|
593
|
+
const delta = stringDelta(previous, data.text);
|
|
594
|
+
state.textByMessage.set(messageId, data.text);
|
|
595
|
+
state.text += delta;
|
|
596
|
+
if (delta) {
|
|
597
|
+
await emit({
|
|
598
|
+
type: 'stream_event',
|
|
599
|
+
session_id: scope.sessionId,
|
|
600
|
+
event: {
|
|
601
|
+
type: 'content_block_delta',
|
|
602
|
+
index,
|
|
603
|
+
delta: { type: 'text_delta', text: delta },
|
|
604
|
+
},
|
|
605
|
+
});
|
|
606
|
+
}
|
|
607
|
+
continue;
|
|
608
|
+
}
|
|
609
|
+
if (type === 'reasoning' && typeof data.thinking === 'string') {
|
|
610
|
+
const previous = state.reasoningByMessage.get(messageId) ?? '';
|
|
611
|
+
const delta = stringDelta(previous, data.thinking);
|
|
612
|
+
state.reasoningByMessage.set(messageId, data.thinking);
|
|
613
|
+
if (delta) {
|
|
614
|
+
await emit({
|
|
615
|
+
type: 'stream_event',
|
|
616
|
+
session_id: scope.sessionId,
|
|
617
|
+
event: {
|
|
618
|
+
type: 'content_block_delta',
|
|
619
|
+
index,
|
|
620
|
+
delta: { type: 'thinking_delta', thinking: delta },
|
|
621
|
+
},
|
|
622
|
+
});
|
|
623
|
+
}
|
|
624
|
+
continue;
|
|
625
|
+
}
|
|
626
|
+
if (type === 'tool_call') {
|
|
627
|
+
const id = stringValue(data.id);
|
|
628
|
+
if (!id)
|
|
629
|
+
continue;
|
|
630
|
+
const fingerprint = JSON.stringify(data);
|
|
631
|
+
const previous = state.toolCalls.get(id);
|
|
632
|
+
if (previous === fingerprint)
|
|
633
|
+
continue;
|
|
634
|
+
state.toolCalls.set(id, fingerprint);
|
|
635
|
+
if (data.finished === false)
|
|
636
|
+
continue;
|
|
637
|
+
await emit({
|
|
638
|
+
type: 'assistant',
|
|
639
|
+
session_id: scope.sessionId,
|
|
640
|
+
message: {
|
|
641
|
+
role: 'assistant',
|
|
642
|
+
content: [{
|
|
643
|
+
type: 'tool_use',
|
|
644
|
+
id,
|
|
645
|
+
name: stringValue(data.name) ?? 'tool',
|
|
646
|
+
input: parseToolInput(data.input),
|
|
647
|
+
}],
|
|
648
|
+
},
|
|
649
|
+
});
|
|
650
|
+
continue;
|
|
651
|
+
}
|
|
652
|
+
if (type === 'tool_result') {
|
|
653
|
+
const toolCallId = stringValue(data.tool_call_id) ?? 'crush-tool-unknown';
|
|
654
|
+
const content = stringValue(data.content) ?? '';
|
|
655
|
+
const fingerprint = `${toolCallId}\u0000${content}\u0000${data.is_error === true}`;
|
|
656
|
+
if (state.toolResults.has(fingerprint))
|
|
657
|
+
continue;
|
|
658
|
+
state.toolResults.add(fingerprint);
|
|
659
|
+
await emit({
|
|
660
|
+
type: 'user',
|
|
661
|
+
session_id: scope.sessionId,
|
|
662
|
+
message: {
|
|
663
|
+
role: 'user',
|
|
664
|
+
content: [{
|
|
665
|
+
type: 'tool_result',
|
|
666
|
+
tool_use_id: toolCallId,
|
|
667
|
+
content,
|
|
668
|
+
is_error: data.is_error === true,
|
|
669
|
+
}],
|
|
670
|
+
},
|
|
671
|
+
});
|
|
672
|
+
}
|
|
673
|
+
}
|
|
674
|
+
}
|
|
675
|
+
async function* parseSseData(body, signal) {
|
|
676
|
+
const decoder = new TextDecoder();
|
|
677
|
+
let buffer = '';
|
|
678
|
+
for await (const chunk of body) {
|
|
679
|
+
throwIfAborted(signal);
|
|
680
|
+
buffer += typeof chunk === 'string' ? chunk : decoder.decode(chunk, { stream: true });
|
|
681
|
+
if (Buffer.byteLength(buffer, 'utf8') > MAX_SSE_EVENT_BYTES * 2) {
|
|
682
|
+
throw new Error('Crush SSE buffer exceeded its safety limit.');
|
|
683
|
+
}
|
|
684
|
+
let boundary = findSseBoundary(buffer);
|
|
685
|
+
while (boundary) {
|
|
686
|
+
const frame = buffer.slice(0, boundary.index);
|
|
687
|
+
buffer = buffer.slice(boundary.index + boundary.length);
|
|
688
|
+
const data = frame.split(/\r?\n/u)
|
|
689
|
+
.filter(line => line.startsWith('data:'))
|
|
690
|
+
.map(line => line.slice(5).trimStart())
|
|
691
|
+
.join('\n');
|
|
692
|
+
if (Buffer.byteLength(data, 'utf8') > MAX_SSE_EVENT_BYTES) {
|
|
693
|
+
throw new Error('Crush SSE event exceeded its safety limit.');
|
|
694
|
+
}
|
|
695
|
+
if (data)
|
|
696
|
+
yield data;
|
|
697
|
+
boundary = findSseBoundary(buffer);
|
|
698
|
+
}
|
|
699
|
+
}
|
|
700
|
+
buffer += decoder.decode();
|
|
701
|
+
const data = buffer.split(/\r?\n/u)
|
|
702
|
+
.filter(line => line.startsWith('data:'))
|
|
703
|
+
.map(line => line.slice(5).trimStart())
|
|
704
|
+
.join('\n');
|
|
705
|
+
if (data)
|
|
706
|
+
yield data;
|
|
707
|
+
}
|
|
708
|
+
function findSseBoundary(buffer) {
|
|
709
|
+
const lf = buffer.indexOf('\n\n');
|
|
710
|
+
const crlf = buffer.indexOf('\r\n\r\n');
|
|
711
|
+
if (lf < 0 && crlf < 0)
|
|
712
|
+
return undefined;
|
|
713
|
+
if (crlf >= 0 && (lf < 0 || crlf < lf))
|
|
714
|
+
return { index: crlf, length: 4 };
|
|
715
|
+
return { index: lf, length: 2 };
|
|
716
|
+
}
|
|
717
|
+
function parseCrushEvent(data) {
|
|
718
|
+
let envelope;
|
|
719
|
+
try {
|
|
720
|
+
envelope = asRecord(JSON.parse(data)) ?? {};
|
|
721
|
+
}
|
|
722
|
+
catch {
|
|
723
|
+
throw new Error('Crush emitted malformed SSE JSON.');
|
|
724
|
+
}
|
|
725
|
+
const kind = stringValue(envelope.type);
|
|
726
|
+
if (!kind)
|
|
727
|
+
return undefined;
|
|
728
|
+
const inner = parseRecord(envelope.payload);
|
|
729
|
+
const eventData = parseRecord(inner.payload);
|
|
730
|
+
return {
|
|
731
|
+
kind,
|
|
732
|
+
envelope,
|
|
733
|
+
inner,
|
|
734
|
+
data: Object.keys(eventData).length > 0 ? eventData : inner,
|
|
735
|
+
};
|
|
736
|
+
}
|
|
737
|
+
function parseRecord(value) {
|
|
738
|
+
if (asRecord(value))
|
|
739
|
+
return value;
|
|
740
|
+
if (typeof value !== 'string')
|
|
741
|
+
return {};
|
|
742
|
+
try {
|
|
743
|
+
return asRecord(JSON.parse(value)) ?? {};
|
|
744
|
+
}
|
|
745
|
+
catch {
|
|
746
|
+
return {};
|
|
747
|
+
}
|
|
748
|
+
}
|
|
749
|
+
function eventMatchesScope(event, scope) {
|
|
750
|
+
const layers = [event.envelope, event.inner, event.data];
|
|
751
|
+
if (hasForeignValue(layers, ['workspace_id', 'workspaceId'], scope.workspaceId))
|
|
752
|
+
return false;
|
|
753
|
+
if (hasForeignValue(layers, ['client_id', 'clientId'], scope.clientId))
|
|
754
|
+
return false;
|
|
755
|
+
if (hasForeignValue(layers, ['session_id', 'sessionId'], scope.sessionId))
|
|
756
|
+
return false;
|
|
757
|
+
if (hasForeignValue(layers, ['run_id', 'runId'], scope.runId))
|
|
758
|
+
return false;
|
|
759
|
+
if (event.kind === 'run_complete') {
|
|
760
|
+
const runId = stringValue(event.data.run_id) ?? stringValue(event.data.runId);
|
|
761
|
+
if (runId !== scope.runId)
|
|
762
|
+
return false;
|
|
763
|
+
}
|
|
764
|
+
return true;
|
|
765
|
+
}
|
|
766
|
+
function hasForeignValue(layers, keys, expected) {
|
|
767
|
+
for (const layer of layers) {
|
|
768
|
+
for (const key of keys) {
|
|
769
|
+
const value = stringValue(layer[key]);
|
|
770
|
+
if (value != null && value !== expected)
|
|
771
|
+
return true;
|
|
772
|
+
}
|
|
773
|
+
}
|
|
774
|
+
return false;
|
|
775
|
+
}
|
|
776
|
+
function asPermissionRequest(value) {
|
|
777
|
+
const id = stringValue(value.id);
|
|
778
|
+
const sessionId = stringValue(value.session_id);
|
|
779
|
+
if (!id || !sessionId)
|
|
780
|
+
return undefined;
|
|
781
|
+
return {
|
|
782
|
+
...value,
|
|
783
|
+
id,
|
|
784
|
+
session_id: sessionId,
|
|
785
|
+
tool_call_id: stringValue(value.tool_call_id),
|
|
786
|
+
tool_name: stringValue(value.tool_name),
|
|
787
|
+
action: stringValue(value.action),
|
|
788
|
+
path: stringValue(value.path),
|
|
789
|
+
};
|
|
790
|
+
}
|
|
791
|
+
function assertPermissionDecision(value) {
|
|
792
|
+
if (value !== 'allow' && value !== 'allow_session' && value !== 'deny') {
|
|
793
|
+
throw new TypeError('Crush permission handler returned an invalid decision.');
|
|
794
|
+
}
|
|
795
|
+
}
|
|
796
|
+
function parseToolInput(value) {
|
|
797
|
+
if (typeof value !== 'string')
|
|
798
|
+
return value ?? {};
|
|
799
|
+
try {
|
|
800
|
+
return JSON.parse(value);
|
|
801
|
+
}
|
|
802
|
+
catch {
|
|
803
|
+
return value;
|
|
804
|
+
}
|
|
805
|
+
}
|
|
806
|
+
function stringDelta(previous, next) {
|
|
807
|
+
return next.startsWith(previous) ? next.slice(previous.length) : next;
|
|
808
|
+
}
|
|
809
|
+
function asRecord(value) {
|
|
810
|
+
return value != null && typeof value === 'object' && !Array.isArray(value)
|
|
811
|
+
? value
|
|
812
|
+
: undefined;
|
|
813
|
+
}
|
|
814
|
+
function stringValue(value) {
|
|
815
|
+
return typeof value === 'string' && value.length > 0 ? value : undefined;
|
|
816
|
+
}
|
|
817
|
+
async function jsonRequest(httpRequest, options, acceptedStatuses) {
|
|
818
|
+
const response = await httpRequest(withJsonHeaders(options));
|
|
819
|
+
assertStatus(response, acceptedStatuses, `${options.method} ${options.path}`);
|
|
820
|
+
const text = await readBody(response.body);
|
|
821
|
+
if (!text)
|
|
822
|
+
return {};
|
|
823
|
+
try {
|
|
824
|
+
return JSON.parse(text);
|
|
825
|
+
}
|
|
826
|
+
catch {
|
|
827
|
+
throw new Error(`Crush returned invalid JSON for ${options.method} ${options.path}.`);
|
|
828
|
+
}
|
|
829
|
+
}
|
|
830
|
+
async function emptyRequest(httpRequest, options, acceptedStatuses) {
|
|
831
|
+
const response = await httpRequest(withJsonHeaders(options));
|
|
832
|
+
assertStatus(response, acceptedStatuses, `${options.method} ${options.path}`);
|
|
833
|
+
await discardBody(response.body);
|
|
834
|
+
}
|
|
835
|
+
function withJsonHeaders(options) {
|
|
836
|
+
if (options.body == null)
|
|
837
|
+
return options;
|
|
838
|
+
return {
|
|
839
|
+
...options,
|
|
840
|
+
headers: {
|
|
841
|
+
'Content-Type': 'application/json',
|
|
842
|
+
'Content-Length': String(Buffer.byteLength(options.body, 'utf8')),
|
|
843
|
+
...(options.headers ?? {}),
|
|
844
|
+
},
|
|
845
|
+
};
|
|
846
|
+
}
|
|
847
|
+
function assertStatus(response, acceptedStatuses, operation) {
|
|
848
|
+
if (!acceptedStatuses.includes(response.statusCode)) {
|
|
849
|
+
throw new Error(`Failed to ${operation}: Crush returned HTTP ${response.statusCode}.`);
|
|
850
|
+
}
|
|
851
|
+
}
|
|
852
|
+
async function readBody(body) {
|
|
853
|
+
const chunks = [];
|
|
854
|
+
let size = 0;
|
|
855
|
+
for await (const chunk of body) {
|
|
856
|
+
const buffer = Buffer.from(chunk);
|
|
857
|
+
size += buffer.length;
|
|
858
|
+
if (size > MAX_RESPONSE_BYTES)
|
|
859
|
+
throw new Error('Crush response exceeded its safety limit.');
|
|
860
|
+
chunks.push(buffer);
|
|
861
|
+
}
|
|
862
|
+
return Buffer.concat(chunks).toString('utf8');
|
|
863
|
+
}
|
|
864
|
+
async function discardBody(body) {
|
|
865
|
+
for await (const _chunk of body) {
|
|
866
|
+
// Drain the response so Node can close/reuse the local socket cleanly.
|
|
867
|
+
}
|
|
868
|
+
}
|
|
869
|
+
function createStderrCollector(secrets) {
|
|
870
|
+
let value = '';
|
|
871
|
+
return {
|
|
872
|
+
attach(child) {
|
|
873
|
+
child.stderr?.setEncoding('utf8');
|
|
874
|
+
child.stderr?.on('data', chunk => {
|
|
875
|
+
if (Buffer.byteLength(value, 'utf8') >= MAX_STDERR_BYTES)
|
|
876
|
+
return;
|
|
877
|
+
value += String(chunk);
|
|
878
|
+
if (Buffer.byteLength(value, 'utf8') > MAX_STDERR_BYTES) {
|
|
879
|
+
value = Buffer.from(value, 'utf8').subarray(0, MAX_STDERR_BYTES).toString('utf8');
|
|
880
|
+
}
|
|
881
|
+
});
|
|
882
|
+
},
|
|
883
|
+
value() {
|
|
884
|
+
return redactString(value, secrets);
|
|
885
|
+
},
|
|
886
|
+
};
|
|
887
|
+
}
|
|
888
|
+
function secretValues(env, additional = []) {
|
|
889
|
+
return [...new Set([
|
|
890
|
+
...Object.entries(env ?? {})
|
|
891
|
+
.filter(([key, value]) => SECRET_KEY_PATTERN.test(key) && value.length >= 4)
|
|
892
|
+
.map(([, value]) => value),
|
|
893
|
+
...additional.filter(value => value.length >= 4),
|
|
894
|
+
])]
|
|
895
|
+
.sort((left, right) => right.length - left.length);
|
|
896
|
+
}
|
|
897
|
+
function redactEvent(event, secrets) {
|
|
898
|
+
return sanitizeValue(event, secrets);
|
|
899
|
+
}
|
|
900
|
+
function sanitizeValue(value, secrets) {
|
|
901
|
+
if (typeof value === 'string')
|
|
902
|
+
return redactString(value, secrets);
|
|
903
|
+
if (Array.isArray(value))
|
|
904
|
+
return value.map(item => sanitizeValue(item, secrets));
|
|
905
|
+
const record = asRecord(value);
|
|
906
|
+
if (!record)
|
|
907
|
+
return value;
|
|
908
|
+
const sanitized = {};
|
|
909
|
+
for (const [key, child] of Object.entries(record)) {
|
|
910
|
+
sanitized[key] = SECRET_KEY_PATTERN.test(key)
|
|
911
|
+
? '[REDACTED]'
|
|
912
|
+
: sanitizeValue(child, secrets);
|
|
913
|
+
}
|
|
914
|
+
return sanitized;
|
|
915
|
+
}
|
|
916
|
+
function redactString(value, secrets) {
|
|
917
|
+
let redacted = value;
|
|
918
|
+
for (const secret of secrets)
|
|
919
|
+
redacted = redacted.split(secret).join('[REDACTED]');
|
|
920
|
+
return redacted
|
|
921
|
+
.replace(/Bearer\s+[^\s,;]+/giu, 'Bearer [REDACTED]')
|
|
922
|
+
.replace(/((?:api[_-]?key|token|authorization|password|secret)\s*[:=]\s*)[^\s,;]+/giu, '$1[REDACTED]');
|
|
923
|
+
}
|
|
924
|
+
function throwIfAborted(signal) {
|
|
925
|
+
if (!signal?.aborted)
|
|
926
|
+
return;
|
|
927
|
+
const error = new Error('Crush managed run was aborted.');
|
|
928
|
+
error.name = 'AbortError';
|
|
929
|
+
throw error;
|
|
930
|
+
}
|
|
931
|
+
async function raceWithAbort(promise, signal) {
|
|
932
|
+
if (!signal)
|
|
933
|
+
return promise;
|
|
934
|
+
throwIfAborted(signal);
|
|
935
|
+
return new Promise((resolve, reject) => {
|
|
936
|
+
const onAbort = () => {
|
|
937
|
+
const error = new Error('Crush managed run was aborted.');
|
|
938
|
+
error.name = 'AbortError';
|
|
939
|
+
reject(error);
|
|
940
|
+
};
|
|
941
|
+
signal.addEventListener('abort', onAbort, { once: true });
|
|
942
|
+
promise.then(value => {
|
|
943
|
+
signal.removeEventListener('abort', onAbort);
|
|
944
|
+
resolve(value);
|
|
945
|
+
}, error => {
|
|
946
|
+
signal.removeEventListener('abort', onAbort);
|
|
947
|
+
reject(error);
|
|
948
|
+
});
|
|
949
|
+
});
|
|
950
|
+
}
|
|
951
|
+
async function abortableDelay(ms, signal) {
|
|
952
|
+
let timer;
|
|
953
|
+
try {
|
|
954
|
+
await raceWithAbort(new Promise(resolve => {
|
|
955
|
+
timer = setTimeout(resolve, ms);
|
|
956
|
+
}), signal);
|
|
957
|
+
}
|
|
958
|
+
finally {
|
|
959
|
+
if (timer)
|
|
960
|
+
clearTimeout(timer);
|
|
961
|
+
}
|
|
962
|
+
}
|
|
963
|
+
async function bestEffortWithTimeout(promise, timeoutMs) {
|
|
964
|
+
let timer;
|
|
965
|
+
try {
|
|
966
|
+
await Promise.race([
|
|
967
|
+
promise.catch(() => undefined),
|
|
968
|
+
new Promise(resolve => {
|
|
969
|
+
timer = setTimeout(resolve, timeoutMs);
|
|
970
|
+
}),
|
|
971
|
+
]);
|
|
972
|
+
}
|
|
973
|
+
finally {
|
|
974
|
+
if (timer)
|
|
975
|
+
clearTimeout(timer);
|
|
976
|
+
}
|
|
977
|
+
}
|
|
978
|
+
//# sourceMappingURL=crushManagedClient.js.map
|