@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,494 @@
|
|
|
1
|
+
// The only module in the Copilot SDK worker that touches the installed Copilot
|
|
2
|
+
// SDK. Everything else takes an injected client, which is what keeps the unit
|
|
3
|
+
// suite spawn-free (DEVELOPING.md): no test in this directory imports the real
|
|
4
|
+
// SDK or starts a runtime process.
|
|
5
|
+
//
|
|
6
|
+
// Two paths matter and they are NOT interchangeable. `COPILOT_SDK_PATH` points
|
|
7
|
+
// at the `copilot-sdk` directory bundled inside one CLI version directory:
|
|
8
|
+
//
|
|
9
|
+
// ~/.cache/copilot/pkg/<platform>/<version>/copilot-sdk ← the SDK we import
|
|
10
|
+
// ~/.cache/copilot/pkg/<platform>/<version>/app.js ← the runtime we spawn
|
|
11
|
+
//
|
|
12
|
+
// The phase-0 spike observed the npm-loader `copilot` binary running an OLDER
|
|
13
|
+
// runtime (1.0.78) than the SDK resolved from the cache (1.0.82). Both spoke
|
|
14
|
+
// protocol 3, so nothing broke visibly, but the event shapes this worker
|
|
15
|
+
// normalizes are per-version — so the runtime is derived from the SDK's own
|
|
16
|
+
// version directory rather than from PATH, and a disagreement between the
|
|
17
|
+
// directory version and the runtime's self-reported version is logged.
|
|
18
|
+
import path from 'path';
|
|
19
|
+
import { pathToFileURL } from 'url';
|
|
20
|
+
|
|
21
|
+
import {
|
|
22
|
+
buildTerminalFailureText,
|
|
23
|
+
isStructuredQuotaError,
|
|
24
|
+
normalizeTerminalSendAndWaitError,
|
|
25
|
+
toKebabToken,
|
|
26
|
+
} from '../../.github/extensions/web-relay/runtime/send-and-wait-errors.mjs';
|
|
27
|
+
import { copilotRuntimeEntry } from '../copilot-sdk-runtime.mjs';
|
|
28
|
+
|
|
29
|
+
const VERSION_DIR_RE = /^\d+\.\d+\.\d+$/;
|
|
30
|
+
|
|
31
|
+
/**
|
|
32
|
+
* The answer handed back to the runtime when the model calls `ask_user` before
|
|
33
|
+
* phase 2 wires the relay question round-trip. Deliberately an *answer* rather
|
|
34
|
+
* than a hang or a thrown handler: the runtime blocks the turn until the
|
|
35
|
+
* handler settles, so refusing in-band lets the model carry on (or say it is
|
|
36
|
+
* blocked) instead of wedging the queue row until the delivery watchdog fires.
|
|
37
|
+
*/
|
|
38
|
+
export const USER_INPUT_UNSUPPORTED_ANSWER =
|
|
39
|
+
'Interactive questions are not yet supported by the SDK worker, so this question could not be '
|
|
40
|
+
+ 'shown to the user. Continue with your best judgement and state the assumption you made, or '
|
|
41
|
+
+ 'end the turn explaining what you need.';
|
|
42
|
+
|
|
43
|
+
export const ELICITATION_UNSUPPORTED_MESSAGE =
|
|
44
|
+
'Structured elicitation is not yet supported by the SDK worker.';
|
|
45
|
+
|
|
46
|
+
/**
|
|
47
|
+
* Resolve the SDK entry point and its sibling runtime from `COPILOT_SDK_PATH`.
|
|
48
|
+
* Tolerates a path to the SDK's `index.js` as well as to its directory, since
|
|
49
|
+
* the relay currently exports the directory and a hand-run worker
|
|
50
|
+
* (DEVELOPING.md) is easy to point at the file.
|
|
51
|
+
*/
|
|
52
|
+
export function resolveCopilotSdkPaths({ env = process.env } = {}) {
|
|
53
|
+
const raw = String(env.COPILOT_SDK_PATH || '').trim();
|
|
54
|
+
if (!raw) {
|
|
55
|
+
throw new Error(
|
|
56
|
+
'COPILOT_SDK_PATH is not set — install the GitHub Copilot CLI (so '
|
|
57
|
+
+ '~/.cache/copilot/pkg/<platform>/<version>/copilot-sdk exists) or set sdkPath in the relay config.',
|
|
58
|
+
);
|
|
59
|
+
}
|
|
60
|
+
const sdkDir = /[\\/]index\.m?js$/i.test(raw) ? path.dirname(raw) : raw;
|
|
61
|
+
const versionDir = path.dirname(sdkDir);
|
|
62
|
+
const version = path.basename(versionDir);
|
|
63
|
+
return {
|
|
64
|
+
sdkDir,
|
|
65
|
+
sdkEntry: path.join(sdkDir, 'index.js'),
|
|
66
|
+
versionDir,
|
|
67
|
+
// Same version directory as the SDK, and `app.js` rather than the sibling
|
|
68
|
+
// `index.js` launcher — see `copilotRuntimeEntry` and the header.
|
|
69
|
+
runtimeEntry: copilotRuntimeEntry(versionDir),
|
|
70
|
+
version: VERSION_DIR_RE.test(version) ? version : '',
|
|
71
|
+
};
|
|
72
|
+
}
|
|
73
|
+
|
|
74
|
+
/**
|
|
75
|
+
* Compare the version directory the SDK came from with whatever the running
|
|
76
|
+
* runtime reports. Returns a warning string, or null when they agree (or when
|
|
77
|
+
* either side could not be read — an unknown version is not evidence of skew).
|
|
78
|
+
*/
|
|
79
|
+
export function describeVersionSkew({ bundleVersion = '', runtimeVersion = '' } = {}) {
|
|
80
|
+
const bundle = String(bundleVersion || '').trim();
|
|
81
|
+
const runtime = String(runtimeVersion || '').trim();
|
|
82
|
+
if (!bundle || !runtime || bundle === runtime) return null;
|
|
83
|
+
return `Copilot SDK/runtime version skew: SDK bundle ${bundle}, runtime reports ${runtime}. `
|
|
84
|
+
+ 'Event shapes are per-version; reinstall the Copilot CLI if turns behave oddly.';
|
|
85
|
+
}
|
|
86
|
+
|
|
87
|
+
/**
|
|
88
|
+
* Read the runtime's self-reported version without assuming the RPC exists —
|
|
89
|
+
* `getStatus` is not in every CLI build, and a missing status must degrade to
|
|
90
|
+
* "unknown version" rather than failing the worker's first turn.
|
|
91
|
+
*/
|
|
92
|
+
export async function readRuntimeVersion(client, dbg = () => {}) {
|
|
93
|
+
if (typeof client?.getStatus !== 'function') return '';
|
|
94
|
+
try {
|
|
95
|
+
const status = await client.getStatus();
|
|
96
|
+
return String(status?.version || '').trim();
|
|
97
|
+
} catch (error) {
|
|
98
|
+
dbg('copilot runtime getStatus failed', error?.message || String(error));
|
|
99
|
+
return '';
|
|
100
|
+
}
|
|
101
|
+
}
|
|
102
|
+
|
|
103
|
+
/**
|
|
104
|
+
* Build the stdio connection descriptor for the runtime. `RuntimeConnection`
|
|
105
|
+
* is the documented constructor; the plain object literal is the shape it
|
|
106
|
+
* produces and is accepted directly by older bundles, so it stands in when the
|
|
107
|
+
* export is missing rather than hard-failing on a version difference.
|
|
108
|
+
*/
|
|
109
|
+
export function buildRuntimeConnection(sdk, runtimeEntry) {
|
|
110
|
+
if (typeof sdk?.RuntimeConnection?.forStdio === 'function') {
|
|
111
|
+
return sdk.RuntimeConnection.forStdio({ path: runtimeEntry });
|
|
112
|
+
}
|
|
113
|
+
return { kind: 'stdio', path: runtimeEntry };
|
|
114
|
+
}
|
|
115
|
+
|
|
116
|
+
/**
|
|
117
|
+
* Watch for the runtime process dying under a live session.
|
|
118
|
+
*
|
|
119
|
+
* The SDK exposes no public hook for this (verified against 1.0.82: no
|
|
120
|
+
* `on`/`addEventListener`, no `onExit`/`onClose` client option, and the
|
|
121
|
+
* JSON-RPC `onClose`/`onError` handlers set a private `state` field and
|
|
122
|
+
* discard the error). What it does have is `processExitPromise` — a
|
|
123
|
+
* TS-private, runtime-visible promise that rejects with the exit code and
|
|
124
|
+
* captured stderr when the spawned CLI exits. It is already `.catch()`-ed
|
|
125
|
+
* internally, so attaching another handler is safe.
|
|
126
|
+
*
|
|
127
|
+
* Reaching past the public API is deliberate and bounded: without it a dead
|
|
128
|
+
* runtime wedges the queue row until the relay's delivery watchdog gives up
|
|
129
|
+
* ~an hour later, because the worker's 10s heartbeat keeps renewing the
|
|
130
|
+
* processing lease. If a future bundle drops the field this degrades to no
|
|
131
|
+
* detection (the session-process's `session.shutdown` handling is the second,
|
|
132
|
+
* public-API line of defence), never to a crash.
|
|
133
|
+
*/
|
|
134
|
+
export function observeRuntimeExit(client, onExit) {
|
|
135
|
+
if (!client || typeof onExit !== 'function') return () => {};
|
|
136
|
+
let live = true;
|
|
137
|
+
const fire = (detail) => {
|
|
138
|
+
if (!live) return;
|
|
139
|
+
live = false;
|
|
140
|
+
onExit(String(detail || '').trim() || 'the runtime process exited');
|
|
141
|
+
};
|
|
142
|
+
|
|
143
|
+
const exitPromise = client.processExitPromise;
|
|
144
|
+
if (exitPromise && typeof exitPromise.then === 'function') {
|
|
145
|
+
exitPromise.then(
|
|
146
|
+
() => fire('the runtime process exited'),
|
|
147
|
+
(error) => fire(error?.message || String(error)),
|
|
148
|
+
);
|
|
149
|
+
} else if (typeof client.on === 'function') {
|
|
150
|
+
// Forward-compatibility only: no shipped bundle exposes this.
|
|
151
|
+
try {
|
|
152
|
+
client.on('exit', (code) => fire(`the runtime process exited with code ${code}`));
|
|
153
|
+
} catch {
|
|
154
|
+
// An `on` that is not an emitter is not worth failing the worker over.
|
|
155
|
+
}
|
|
156
|
+
}
|
|
157
|
+
return () => { live = false; };
|
|
158
|
+
}
|
|
159
|
+
|
|
160
|
+
/**
|
|
161
|
+
* Import the installed SDK and start a client against the matching runtime.
|
|
162
|
+
*
|
|
163
|
+
* The version-skew probe is deliberately NOT awaited: `getStatus` is a round
|
|
164
|
+
* trip to a process that has just started, and blocking the first turn's
|
|
165
|
+
* readiness on a diagnostic is the wrong trade. `versionReady` resolves with
|
|
166
|
+
* the answer for callers that want to log it.
|
|
167
|
+
*/
|
|
168
|
+
export async function startCopilotClient({
|
|
169
|
+
paths,
|
|
170
|
+
cwd,
|
|
171
|
+
logLevel = 'error',
|
|
172
|
+
clientName = 'copilot-web-relay',
|
|
173
|
+
importImpl = (href) => import(href),
|
|
174
|
+
dbg = () => {},
|
|
175
|
+
} = {}) {
|
|
176
|
+
const sdk = await importImpl(pathToFileURL(paths.sdkEntry).href);
|
|
177
|
+
if (typeof sdk?.CopilotClient !== 'function') {
|
|
178
|
+
throw new Error(`CopilotClient not found in the installed Copilot SDK (${paths.sdkEntry})`);
|
|
179
|
+
}
|
|
180
|
+
const client = new sdk.CopilotClient({
|
|
181
|
+
connection: buildRuntimeConnection(sdk, paths.runtimeEntry),
|
|
182
|
+
workingDirectory: cwd,
|
|
183
|
+
clientName,
|
|
184
|
+
logLevel,
|
|
185
|
+
});
|
|
186
|
+
await client.start();
|
|
187
|
+
const versionReady = readRuntimeVersion(client, dbg).then((runtimeVersion) => ({
|
|
188
|
+
runtimeVersion,
|
|
189
|
+
versionSkewWarning: describeVersionSkew({ bundleVersion: paths.version, runtimeVersion }),
|
|
190
|
+
}));
|
|
191
|
+
// The caller logs it; this only guarantees the promise is never unhandled.
|
|
192
|
+
versionReady.catch(() => {});
|
|
193
|
+
return { sdk, client, paths, versionReady };
|
|
194
|
+
}
|
|
195
|
+
|
|
196
|
+
// ------------------------------------------------------------- permissions --
|
|
197
|
+
|
|
198
|
+
/**
|
|
199
|
+
* Permission request kinds that only READ. `read` is the runtime's own
|
|
200
|
+
* read-only variant; `mcp` and `custom-tool` carry an explicit `readOnly`
|
|
201
|
+
* flag; a `shell` request is read-only when every parsed command segment says
|
|
202
|
+
* so. Everything else (write, url, memory, hook, extension management) mutates
|
|
203
|
+
* something and is treated as an action.
|
|
204
|
+
*/
|
|
205
|
+
export function isReadOnlyPermissionRequest(request) {
|
|
206
|
+
const kind = String(request?.kind || '').trim().toLowerCase();
|
|
207
|
+
if (kind === 'read') return true;
|
|
208
|
+
if (kind === 'mcp' || kind === 'custom-tool') return request?.readOnly === true;
|
|
209
|
+
if (kind === 'shell') {
|
|
210
|
+
const commands = Array.isArray(request?.commands) ? request.commands : [];
|
|
211
|
+
return commands.length > 0 && commands.every((command) => command?.readOnly === true);
|
|
212
|
+
}
|
|
213
|
+
return false;
|
|
214
|
+
}
|
|
215
|
+
|
|
216
|
+
/** A short, user-readable name for what a permission request wanted to do. */
|
|
217
|
+
export function describePermissionRequest(request) {
|
|
218
|
+
const kind = String(request?.kind || 'tool').trim() || 'tool';
|
|
219
|
+
const detail = String(
|
|
220
|
+
request?.fullCommandText
|
|
221
|
+
|| request?.fileName
|
|
222
|
+
|| request?.path
|
|
223
|
+
|| request?.url
|
|
224
|
+
|| request?.toolName
|
|
225
|
+
|| request?.intention
|
|
226
|
+
|| '',
|
|
227
|
+
).trim();
|
|
228
|
+
return detail ? `${kind}: ${detail}` : kind;
|
|
229
|
+
}
|
|
230
|
+
|
|
231
|
+
/**
|
|
232
|
+
* Map the relay's conversation mode onto a permission decision, mirroring the
|
|
233
|
+
* policy the Claude worker enforces through `permissionModeForRelayMode`:
|
|
234
|
+
* plan-shaped modes must not mutate the workspace, and everything else runs.
|
|
235
|
+
*
|
|
236
|
+
* `plan` and `ask` deny anything that is not read-only. `agent`, `autopilot`
|
|
237
|
+
* and anything unrecognised auto-approve, which is phase 1's contract — there
|
|
238
|
+
* is no relay question round-trip yet, so a prompt would only be a hang.
|
|
239
|
+
*
|
|
240
|
+
* The decision `kind`s here are the runtime's own vocabulary. `approve-once`
|
|
241
|
+
* and `reject` are verified against the runtime; the typed-looking
|
|
242
|
+
* `{kind:"allow"}` is REJECTED ("unknown variant `allow`") and silently fails
|
|
243
|
+
* the tool call, so it must never be emitted.
|
|
244
|
+
*/
|
|
245
|
+
export function copilotPermissionDecision(relayMode, request) {
|
|
246
|
+
const mode = String(relayMode || 'agent').trim().toLowerCase();
|
|
247
|
+
if (mode !== 'plan' && mode !== 'ask') return { kind: 'approve-once' };
|
|
248
|
+
if (isReadOnlyPermissionRequest(request)) return { kind: 'approve-once' };
|
|
249
|
+
return {
|
|
250
|
+
kind: 'reject',
|
|
251
|
+
feedback: `This conversation is in ${mode} mode, so the relay declined "${describePermissionRequest(request)}". `
|
|
252
|
+
+ 'Read-only work is allowed; describe the change you would make instead of making it, or ask the user to '
|
|
253
|
+
+ 'switch the conversation to agent mode.',
|
|
254
|
+
};
|
|
255
|
+
}
|
|
256
|
+
|
|
257
|
+
/**
|
|
258
|
+
* Does this relay mode ask the human before a mutating tool runs, rather than
|
|
259
|
+
* deciding locally? Only `ask` does: `plan` is a "describe, do not act" mode
|
|
260
|
+
* where a prompt on every tool would be noise, and agent/autopilot run
|
|
261
|
+
* unattended by definition.
|
|
262
|
+
*/
|
|
263
|
+
export function relayModeAsksBeforeActing(relayMode) {
|
|
264
|
+
return String(relayMode || '').trim().toLowerCase() === 'ask';
|
|
265
|
+
}
|
|
266
|
+
|
|
267
|
+
/**
|
|
268
|
+
* Build the runtime's `onPermissionRequest` handler.
|
|
269
|
+
*
|
|
270
|
+
* Modes split three ways:
|
|
271
|
+
*
|
|
272
|
+
* - agent / autopilot / anything unrecognised — auto-approve, unchanged.
|
|
273
|
+
* - plan — deny non-read tools with feedback, unchanged from phase 1. A card
|
|
274
|
+
* per tool call would be noise in a mode whose whole point is not acting.
|
|
275
|
+
* - ask — route the decision to the human through a relay question card.
|
|
276
|
+
*
|
|
277
|
+
* Read-only requests short-circuit to `approve-once` in every mode: prompting
|
|
278
|
+
* to read a file the model is already allowed to read is pure friction.
|
|
279
|
+
*
|
|
280
|
+
* The returned decision `kind`s stay inside the runtime's verified vocabulary
|
|
281
|
+
* (`approve-once` / `reject` / `user-not-available`). `{kind:"allow"}` does not
|
|
282
|
+
* exist and is rejected with "unknown variant `allow`".
|
|
283
|
+
*/
|
|
284
|
+
export function createCopilotPermissionHandler({
|
|
285
|
+
bridge = null,
|
|
286
|
+
getRelayMode = () => 'agent',
|
|
287
|
+
getSignal = () => null,
|
|
288
|
+
dbg = () => {},
|
|
289
|
+
} = {}) {
|
|
290
|
+
return async function onPermissionRequest(request) {
|
|
291
|
+
const relayMode = getRelayMode();
|
|
292
|
+
if (!relayModeAsksBeforeActing(relayMode) || !bridge) {
|
|
293
|
+
return copilotPermissionDecision(relayMode, request);
|
|
294
|
+
}
|
|
295
|
+
if (isReadOnlyPermissionRequest(request)) return { kind: 'approve-once' };
|
|
296
|
+
try {
|
|
297
|
+
const { approved, feedback, timedOut, description } = await bridge.askToolApproval(request, {
|
|
298
|
+
signal: getSignal(),
|
|
299
|
+
});
|
|
300
|
+
if (timedOut) {
|
|
301
|
+
// The runtime's own "nobody answered" verdict. Deliberately not a
|
|
302
|
+
// `reject`: a rejection reads to the model as a considered refusal it
|
|
303
|
+
// should work around, while this reads as an absent human.
|
|
304
|
+
dbg('permission question timed out', description);
|
|
305
|
+
return { kind: 'user-not-available' };
|
|
306
|
+
}
|
|
307
|
+
if (approved) return { kind: 'approve-once' };
|
|
308
|
+
// The bridge composes the note (a freeform denial carries the human's own
|
|
309
|
+
// reason; a plain "Deny" click gets a generic one), so the choice labels
|
|
310
|
+
// stay in one module.
|
|
311
|
+
return { kind: 'reject', feedback: feedback || 'The user declined this action.' };
|
|
312
|
+
} catch (error) {
|
|
313
|
+
// The relay is unreachable or the card could not be created. Falling
|
|
314
|
+
// back to the local policy keeps the turn moving; throwing would be
|
|
315
|
+
// auto-answered `user-not-available` by the SDK with no explanation.
|
|
316
|
+
dbg('permission question failed, falling back to the local policy', error?.message || String(error));
|
|
317
|
+
return copilotPermissionDecision(relayMode, request);
|
|
318
|
+
}
|
|
319
|
+
};
|
|
320
|
+
}
|
|
321
|
+
|
|
322
|
+
/**
|
|
323
|
+
* The relay mode expressed in the runtime's own `MessageOptions.agentMode`
|
|
324
|
+
* vocabulary, so the runtime's built-in mode behaviour lines up with the
|
|
325
|
+
* permission policy above instead of fighting it. Unknown relay modes fall
|
|
326
|
+
* back to `interactive` (the runtime's normal agent mode).
|
|
327
|
+
*/
|
|
328
|
+
export function copilotAgentModeForRelayMode(relayMode) {
|
|
329
|
+
const mode = String(relayMode || '').trim().toLowerCase();
|
|
330
|
+
if (mode === 'plan') return 'plan';
|
|
331
|
+
if (mode === 'autopilot') return 'autopilot';
|
|
332
|
+
return 'interactive';
|
|
333
|
+
}
|
|
334
|
+
|
|
335
|
+
// ---------------------------------------------------------- classification --
|
|
336
|
+
|
|
337
|
+
/**
|
|
338
|
+
* Quota exhaustion is the one Copilot failure that must never be retried: the
|
|
339
|
+
* billing window has to reset first. The runtime signals it three redundant
|
|
340
|
+
* ways (`errorCode`, `errorType`, HTTP 402) and the spike saw all three on the
|
|
341
|
+
* same event, so any one of them is enough — the prose is only the last resort
|
|
342
|
+
* because GitHub has reworded it before ("premium requests" → "AI credits").
|
|
343
|
+
*/
|
|
344
|
+
export function isCopilotQuotaError(data) {
|
|
345
|
+
if (isStructuredQuotaError(data)) return true;
|
|
346
|
+
const message = String(data?.message || '');
|
|
347
|
+
return /quota/i.test(message) && /(exceed|exhaust)/i.test(message);
|
|
348
|
+
}
|
|
349
|
+
|
|
350
|
+
/**
|
|
351
|
+
* Classify a `session.error` payload into a terminal failure record.
|
|
352
|
+
*
|
|
353
|
+
* The wording, `stableCode` and guidance come from the extension's own
|
|
354
|
+
* classifier (`send-and-wait-errors.mjs`) rather than being re-typed here, so
|
|
355
|
+
* a conversation that fails on the SDK engine reads identically to one that
|
|
356
|
+
* failed on the extension engine — including `relay.quota-exhausted` and the
|
|
357
|
+
* "Open Check Usage" pointer shipped in 37d2899. The structured quota verdict
|
|
358
|
+
* is passed to that classifier as a hint rather than smuggled in as forged
|
|
359
|
+
* prose, so a quota error is non-retryable however GitHub worded it.
|
|
360
|
+
*/
|
|
361
|
+
export function classifyCopilotSessionError(data) {
|
|
362
|
+
const detail = String(data?.message || '').trim() || 'the Copilot runtime reported a session error';
|
|
363
|
+
const quota = isCopilotQuotaError(data);
|
|
364
|
+
const probe = new Error(detail);
|
|
365
|
+
const hint = { quota };
|
|
366
|
+
const normalized = normalizeTerminalSendAndWaitError(probe, hint);
|
|
367
|
+
if (normalized) {
|
|
368
|
+
return {
|
|
369
|
+
code: normalized.code,
|
|
370
|
+
stableCode: normalized.stableCode,
|
|
371
|
+
text: buildTerminalFailureText(probe, hint),
|
|
372
|
+
detail,
|
|
373
|
+
quota,
|
|
374
|
+
};
|
|
375
|
+
}
|
|
376
|
+
const code = toKebabToken(data?.errorType) || toKebabToken(data?.errorCode) || 'session-error';
|
|
377
|
+
return {
|
|
378
|
+
code,
|
|
379
|
+
stableCode: `copilot.${code}`,
|
|
380
|
+
text: `System note: the Copilot turn failed (${detail}). Retry or send a new message.`,
|
|
381
|
+
detail,
|
|
382
|
+
quota: false,
|
|
383
|
+
};
|
|
384
|
+
}
|
|
385
|
+
|
|
386
|
+
// Phrases that only ever appear in a real auth failure. Deliberately narrow:
|
|
387
|
+
// the loose predecessor matched an unanchored `401` (so `ECONNREFUSED
|
|
388
|
+
// 127.0.0.1:40123` read as an auth failure) and a bare `log in` (so `backlog
|
|
389
|
+
// in progress` did), and a misfiled auth error tells the user to go re-run
|
|
390
|
+
// `copilot` on the relay host for a problem that a retry would have fixed.
|
|
391
|
+
const AUTH_ERROR_RE = new RegExp([
|
|
392
|
+
'authentication (?:failed|error|required)',
|
|
393
|
+
'authorization (?:failed|error|required)',
|
|
394
|
+
'unauthenti(?:cated|cation)',
|
|
395
|
+
'\\bunauthorized\\b',
|
|
396
|
+
'\\b401\\b',
|
|
397
|
+
'not (?:logged|signed) in',
|
|
398
|
+
'(?:logged|signed) out',
|
|
399
|
+
'please (?:log|sign) in',
|
|
400
|
+
'(?:log|sign) in to',
|
|
401
|
+
'(?:invalid|missing|expired) (?:credential|token|api key)',
|
|
402
|
+
'credentials? (?:are )?(?:invalid|missing|expired)',
|
|
403
|
+
'token (?:is )?(?:invalid|expired)',
|
|
404
|
+
].join('|'), 'i');
|
|
405
|
+
|
|
406
|
+
// The runtime's own `ErrorData.errorType` category for auth, plus the codes a
|
|
407
|
+
// thrown transport error carries.
|
|
408
|
+
const AUTH_ERROR_CODES = new Set([
|
|
409
|
+
'authentication',
|
|
410
|
+
'authorization',
|
|
411
|
+
'unauthorized',
|
|
412
|
+
'unauthenticated',
|
|
413
|
+
'authentication_failed',
|
|
414
|
+
'not_logged_in',
|
|
415
|
+
'credentials_invalid',
|
|
416
|
+
]);
|
|
417
|
+
|
|
418
|
+
/**
|
|
419
|
+
* Structured auth signals first (the runtime tags `errorType: "authentication"`
|
|
420
|
+
* and HTTP 401), prose only as the fallback.
|
|
421
|
+
*/
|
|
422
|
+
export function isCopilotAuthError(error) {
|
|
423
|
+
if (error && typeof error === 'object') {
|
|
424
|
+
for (const field of ['code', 'errorCode', 'errorType']) {
|
|
425
|
+
const token = String(error[field] || '').trim().toLowerCase();
|
|
426
|
+
if (token && AUTH_ERROR_CODES.has(token)) return true;
|
|
427
|
+
}
|
|
428
|
+
if (Number(error.statusCode ?? error.status) === 401) return true;
|
|
429
|
+
}
|
|
430
|
+
return AUTH_ERROR_RE.test(String(error?.message || error || ''));
|
|
431
|
+
}
|
|
432
|
+
|
|
433
|
+
/**
|
|
434
|
+
* Classify a thrown error (client start, session create/resume, `send`) rather
|
|
435
|
+
* than a `session.error` event. Auth gets its own branch because the fix is a
|
|
436
|
+
* user action on the relay host, not a retry.
|
|
437
|
+
*/
|
|
438
|
+
export function classifyCopilotTurnException(error) {
|
|
439
|
+
const detail = String(error?.message || error || '').trim() || 'unknown error';
|
|
440
|
+
const terminal = normalizeTerminalSendAndWaitError(error);
|
|
441
|
+
if (terminal) {
|
|
442
|
+
return {
|
|
443
|
+
code: terminal.code,
|
|
444
|
+
stableCode: terminal.stableCode,
|
|
445
|
+
text: buildTerminalFailureText(error),
|
|
446
|
+
detail,
|
|
447
|
+
};
|
|
448
|
+
}
|
|
449
|
+
if (isCopilotAuthError(error)) {
|
|
450
|
+
return {
|
|
451
|
+
code: 'authentication_failed',
|
|
452
|
+
stableCode: 'copilot.authentication_failed',
|
|
453
|
+
text: `System note: the Copilot runtime could not authenticate (${detail}). `
|
|
454
|
+
+ 'Run `copilot` on the relay host and sign in, then retry.',
|
|
455
|
+
detail,
|
|
456
|
+
};
|
|
457
|
+
}
|
|
458
|
+
return {
|
|
459
|
+
code: 'turn-error',
|
|
460
|
+
stableCode: 'copilot.turn-error',
|
|
461
|
+
text: `System note: the Copilot turn failed (${detail}). Retry or send a new message.`,
|
|
462
|
+
detail,
|
|
463
|
+
};
|
|
464
|
+
}
|
|
465
|
+
|
|
466
|
+
// JSON-RPC reserved codes the SDK's transport uses for a connection that went
|
|
467
|
+
// away (`MessageWriteError` … `ConnectionInactive`), as opposed to a server
|
|
468
|
+
// handler that threw.
|
|
469
|
+
const TRANSPORT_ERROR_CODES = new Set([-32099, -32098, -32097, -32096]);
|
|
470
|
+
|
|
471
|
+
/**
|
|
472
|
+
* Did `resumeSession` fail because the session does not exist, or because the
|
|
473
|
+
* call itself failed?
|
|
474
|
+
*
|
|
475
|
+
* This distinction decides whether the worker may start a fresh session over
|
|
476
|
+
* an existing conversation. The runtime throws a plain
|
|
477
|
+
* `Error("Session not found: <id>")` which JSON-RPC wraps as a generic
|
|
478
|
+
* InternalError (-32603), so the code cannot carry the answer and the message
|
|
479
|
+
* has to. Anything that is not recognisably "no such session" — a dropped
|
|
480
|
+
* connection, a transport error, an unrecognised failure — is treated as
|
|
481
|
+
* transient, because guessing wrong there silently discards the whole
|
|
482
|
+
* conversation history.
|
|
483
|
+
*/
|
|
484
|
+
export function isSessionNotFoundError(error) {
|
|
485
|
+
const code = Number(error?.code);
|
|
486
|
+
if (Number.isFinite(code) && TRANSPORT_ERROR_CODES.has(code)) return false;
|
|
487
|
+
if (error?.name === 'ConnectionError') return false;
|
|
488
|
+
const detail = String(error?.message || error || '').toLowerCase();
|
|
489
|
+
if (!detail) return false;
|
|
490
|
+
if (/\b(?:session|conversation)\b[^.]{0,40}\bnot found\b/.test(detail)) return true;
|
|
491
|
+
if (/\bno (?:such|saved) (?:session|conversation)\b/.test(detail)) return true;
|
|
492
|
+
if (/\bunknown session\b/.test(detail)) return true;
|
|
493
|
+
return /\bsession\b[^.]{0,40}\bdoes not exist\b/.test(detail);
|
|
494
|
+
}
|