@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,167 @@
|
|
|
1
|
+
#!/usr/bin/env node
|
|
2
|
+
// Copilot SDK session worker: a per-conversation Node process that speaks the
|
|
3
|
+
// same relay contracts as every other worker (worker WebSocket, heartbeat,
|
|
4
|
+
// control polling, activity channels) but executes turns through the Copilot
|
|
5
|
+
// SDK's headless runtime instead of the `copilot` TUI under a PTY.
|
|
6
|
+
//
|
|
7
|
+
// Dormant until phase 3 registers a worker kind for it. Run it by hand against
|
|
8
|
+
// a live relay with:
|
|
9
|
+
//
|
|
10
|
+
// COPILOT_WEB_RELAY_CONFIG=server/config.json \
|
|
11
|
+
// COPILOT_WORKSPACE_ROOT=/path/to/workspace \
|
|
12
|
+
// COPILOT_SDK_PATH=~/.cache/copilot/pkg/linux-x64/<version>/copilot-sdk \
|
|
13
|
+
// node server/copilot-worker/copilot-sdk-session-worker.mjs --session-id <sdk-session-id>
|
|
14
|
+
import path from 'path';
|
|
15
|
+
import { fileURLToPath } from 'url';
|
|
16
|
+
|
|
17
|
+
import {
|
|
18
|
+
loadTokenFromConfig,
|
|
19
|
+
resolveRelayServerUrl,
|
|
20
|
+
} from '../../.github/extensions/web-relay/runtime/config-loader.mjs';
|
|
21
|
+
import { createApiClient } from '../../.github/extensions/web-relay/runtime/api-client.mjs';
|
|
22
|
+
import { createWorkerWebSocketLink } from '../../.github/extensions/web-relay/runtime/worker-websocket-link.mjs';
|
|
23
|
+
import { createHeartbeatController } from '../../.github/extensions/web-relay/polling/heartbeat.mjs';
|
|
24
|
+
import { createControlPoller } from '../../shared/control-poller.mjs';
|
|
25
|
+
import { installWorkerCrashGuard } from '../../shared/worker-crash-guard.mjs';
|
|
26
|
+
import {
|
|
27
|
+
createWorkerDebug,
|
|
28
|
+
parseSessionIdArg,
|
|
29
|
+
readOptionalMs,
|
|
30
|
+
} from '../../shared/worker-bootstrap.mjs';
|
|
31
|
+
import { createCopilotSdkSessionRunner } from './copilot-sdk-session-process.mjs';
|
|
32
|
+
|
|
33
|
+
const __dirname = path.dirname(fileURLToPath(import.meta.url));
|
|
34
|
+
const HEARTBEAT_MS = 10_000;
|
|
35
|
+
|
|
36
|
+
const dbg = createWorkerDebug('copilot-sdk-worker');
|
|
37
|
+
|
|
38
|
+
async function main() {
|
|
39
|
+
const sdkSessionId = parseSessionIdArg();
|
|
40
|
+
if (!sdkSessionId) {
|
|
41
|
+
console.error('copilot-sdk-session-worker: missing --session-id');
|
|
42
|
+
process.exit(2);
|
|
43
|
+
}
|
|
44
|
+
|
|
45
|
+
const configPath = String(process.env.COPILOT_WEB_RELAY_CONFIG || '').trim()
|
|
46
|
+
|| path.resolve(__dirname, '..', 'config.json');
|
|
47
|
+
// Two reads of the same small file. Collapsing them to one would mean
|
|
48
|
+
// re-implementing `resolveRelayServerUrl`'s env override, host
|
|
49
|
+
// normalisation and port defaulting inside this worker — a live contract
|
|
50
|
+
// duplicated to save a 1 KB read at process start. Not worth it; the
|
|
51
|
+
// extension helper stays the single source of the relay URL.
|
|
52
|
+
const serverUrl = resolveRelayServerUrl({ configPath });
|
|
53
|
+
const token = loadTokenFromConfig(configPath);
|
|
54
|
+
const cwd = String(process.env.COPILOT_WORKSPACE_ROOT || '').trim() || process.cwd();
|
|
55
|
+
const defaultModel = String(process.env.COPILOT_RELAY_MODEL || '').trim();
|
|
56
|
+
|
|
57
|
+
const api = createApiClient({
|
|
58
|
+
serverUrl,
|
|
59
|
+
token,
|
|
60
|
+
getHeaders: () => ({
|
|
61
|
+
'X-Relay-Process-Pid': String(process.pid),
|
|
62
|
+
'X-Relay-Parent-Pid': String(process.ppid),
|
|
63
|
+
'X-Relay-Session-Id': sdkSessionId,
|
|
64
|
+
'X-Relay-Conversation-Id': sdkSessionId,
|
|
65
|
+
}),
|
|
66
|
+
});
|
|
67
|
+
|
|
68
|
+
const controlPoller = createControlPoller({
|
|
69
|
+
api,
|
|
70
|
+
sdkSessionId,
|
|
71
|
+
abortAckNote: 'copilot session aborted',
|
|
72
|
+
dbg,
|
|
73
|
+
});
|
|
74
|
+
const turnRunner = createCopilotSdkSessionRunner({
|
|
75
|
+
api,
|
|
76
|
+
sdkSessionId,
|
|
77
|
+
cwd,
|
|
78
|
+
defaultModel,
|
|
79
|
+
controlPoller,
|
|
80
|
+
idleShutdownMs: readOptionalMs('COPILOT_SDK_RELAY_IDLE_SHUTDOWN_MS'),
|
|
81
|
+
turnStallTimeoutMs: readOptionalMs('COPILOT_SDK_RELAY_TURN_STALL_TIMEOUT_MS'),
|
|
82
|
+
// How long live detached shells alone may hold the runtime open (0 = no
|
|
83
|
+
// limit). Deliberately NOT the relay's `background_task_timeout_minutes`
|
|
84
|
+
// slider that rides every delivery payload: that slider defaults to
|
|
85
|
+
// 0/unlimited and governs Claude's background tasks, which the composer
|
|
86
|
+
// lists and can stop. A Copilot detached shell has neither, so consuming it
|
|
87
|
+
// would make "a forgotten shell pins a runtime forever" the default with
|
|
88
|
+
// nothing in the UI to reveal it. The runner's own 30-minute cap applies
|
|
89
|
+
// unless this says otherwise.
|
|
90
|
+
...(() => {
|
|
91
|
+
const override = readOptionalMs('COPILOT_SDK_RELAY_BACKGROUND_TASK_TIMEOUT_MS');
|
|
92
|
+
return override === undefined ? {} : { getBackgroundTaskTimeoutMs: () => override };
|
|
93
|
+
})(),
|
|
94
|
+
dbg,
|
|
95
|
+
});
|
|
96
|
+
|
|
97
|
+
let heartbeatTimer = null;
|
|
98
|
+
const heartbeat = createHeartbeatController({
|
|
99
|
+
api,
|
|
100
|
+
pollMs: HEARTBEAT_MS,
|
|
101
|
+
getSessionReady: () => true,
|
|
102
|
+
getHeartbeatTimer: () => heartbeatTimer,
|
|
103
|
+
setHeartbeatTimer: (timer) => { heartbeatTimer = timer; },
|
|
104
|
+
getActiveQueueMessageId: () => turnRunner.getActiveQueueMessageId(),
|
|
105
|
+
getActiveQueueMessageIds: () => turnRunner.getActiveQueueMessageIds(),
|
|
106
|
+
});
|
|
107
|
+
|
|
108
|
+
const wsLink = createWorkerWebSocketLink({
|
|
109
|
+
serverUrl,
|
|
110
|
+
token,
|
|
111
|
+
dbg,
|
|
112
|
+
getSessionReady: () => true,
|
|
113
|
+
getSessionId: () => sdkSessionId,
|
|
114
|
+
getPid: () => process.pid,
|
|
115
|
+
onDeliver: async (pending, reason) => {
|
|
116
|
+
dbg('queue.deliver received', `reason=${reason}`, `msgId=${pending?.message?.id || 'none'}`);
|
|
117
|
+
try {
|
|
118
|
+
return await turnRunner.handlePendingPayload(pending);
|
|
119
|
+
} catch (error) {
|
|
120
|
+
dbg('turn handling failed', error?.message || String(error));
|
|
121
|
+
return false;
|
|
122
|
+
}
|
|
123
|
+
},
|
|
124
|
+
onControl: (control) => {
|
|
125
|
+
// Background-task controls have no Copilot SDK equivalent yet: the
|
|
126
|
+
// runtime's `session.background_tasks_changed` carries an empty payload
|
|
127
|
+
// (23 of them fire during a single bash call), so there is no task id to
|
|
128
|
+
// stop. Logged rather than silently dropped.
|
|
129
|
+
dbg('worker control ignored (unsupported by the SDK worker)', String(control?.type || '(none)'));
|
|
130
|
+
},
|
|
131
|
+
});
|
|
132
|
+
|
|
133
|
+
const shutdown = async (signal) => {
|
|
134
|
+
dbg(`shutting down (${signal})`);
|
|
135
|
+
try { wsLink.stop(); } catch {}
|
|
136
|
+
try { heartbeat.stopHeartbeat(); } catch {}
|
|
137
|
+
try { controlPoller.stop(); } catch {}
|
|
138
|
+
// Bounded, like the Cursor worker: stopping the runtime is an RPC, and a
|
|
139
|
+
// hung stop must never make the process ignore the supervisor's SIGTERM.
|
|
140
|
+
try {
|
|
141
|
+
await Promise.race([
|
|
142
|
+
turnRunner.dispose(),
|
|
143
|
+
new Promise((resolve) => {
|
|
144
|
+
const timer = setTimeout(resolve, 3_000);
|
|
145
|
+
timer.unref?.();
|
|
146
|
+
}),
|
|
147
|
+
]);
|
|
148
|
+
} catch {}
|
|
149
|
+
process.exit(0);
|
|
150
|
+
};
|
|
151
|
+
process.on('SIGTERM', () => { shutdown('SIGTERM'); });
|
|
152
|
+
process.on('SIGINT', () => { shutdown('SIGINT'); });
|
|
153
|
+
installWorkerCrashGuard({
|
|
154
|
+
api,
|
|
155
|
+
workerName: 'copilot-sdk-session-worker',
|
|
156
|
+
getActiveQueueMessageIds: () => turnRunner.getActiveQueueMessageIds(),
|
|
157
|
+
});
|
|
158
|
+
|
|
159
|
+
dbg(`starting session=${sdkSessionId.slice(0, 8)} server=${serverUrl} cwd=${cwd} model=${defaultModel || 'default'}`);
|
|
160
|
+
heartbeat.startHeartbeat();
|
|
161
|
+
wsLink.start();
|
|
162
|
+
}
|
|
163
|
+
|
|
164
|
+
main().catch((error) => {
|
|
165
|
+
console.error('copilot-sdk-session-worker fatal:', error?.stack || error?.message || error);
|
|
166
|
+
process.exit(1);
|
|
167
|
+
});
|
|
@@ -0,0 +1,329 @@
|
|
|
1
|
+
// Spawn-free stand-ins for the Copilot SDK, per DEVELOPING.md: no test in this
|
|
2
|
+
// directory starts a runtime process, imports the installed SDK, or talks to a
|
|
3
|
+
// relay. The injection seam is a constructor option on
|
|
4
|
+
// `createCopilotSdkSessionRunner` (`startClientImpl`), not a module mock.
|
|
5
|
+
//
|
|
6
|
+
// Deliberately NOT named `*.test.mjs` so `node --test`'s glob skips it.
|
|
7
|
+
import fs from 'node:fs';
|
|
8
|
+
import path from 'node:path';
|
|
9
|
+
import { fileURLToPath } from 'node:url';
|
|
10
|
+
|
|
11
|
+
import { createCopilotSdkSessionRunner } from './copilot-sdk-session-process.mjs';
|
|
12
|
+
|
|
13
|
+
const FIXTURE_DIR = path.join(path.dirname(fileURLToPath(import.meta.url)), 'fixtures');
|
|
14
|
+
|
|
15
|
+
/** Load a scrubbed phase-0 event dump as an array of `SessionEvent`s. */
|
|
16
|
+
export function loadFixture(name) {
|
|
17
|
+
return JSON.parse(fs.readFileSync(path.join(FIXTURE_DIR, `${name}.json`), 'utf8'));
|
|
18
|
+
}
|
|
19
|
+
|
|
20
|
+
export function tick(ms = 5) {
|
|
21
|
+
return new Promise((resolve) => setTimeout(resolve, ms));
|
|
22
|
+
}
|
|
23
|
+
|
|
24
|
+
export async function waitFor(predicate, { timeoutMs = 3000, label = 'condition' } = {}) {
|
|
25
|
+
const deadline = Date.now() + timeoutMs;
|
|
26
|
+
while (Date.now() < deadline) {
|
|
27
|
+
if (await predicate()) return true;
|
|
28
|
+
await tick(2);
|
|
29
|
+
}
|
|
30
|
+
throw new Error(`waitFor timed out: ${label}`);
|
|
31
|
+
}
|
|
32
|
+
|
|
33
|
+
/**
|
|
34
|
+
* Records every relay call; `failRoutes` makes a route throw, like a 500.
|
|
35
|
+
*
|
|
36
|
+
* `routeResponses` maps a route to its response body, or to a function of
|
|
37
|
+
* `(body, callIndexForThatRoute)` — which is how a test drives a route that
|
|
38
|
+
* answers differently per attempt (`/api/continuation-turn`'s retry loop).
|
|
39
|
+
* Unlisted routes answer `{}`, as before.
|
|
40
|
+
*/
|
|
41
|
+
export function makeApiStub({ failRoutes = new Set(), routeResponses = {} } = {}) {
|
|
42
|
+
const calls = [];
|
|
43
|
+
const routeCounts = new Map();
|
|
44
|
+
async function api(method, routePath, body) {
|
|
45
|
+
calls.push({ method, routePath, body });
|
|
46
|
+
const route = String(routePath).split('?')[0];
|
|
47
|
+
if (failRoutes.has(route)) throw new Error(`stub route failure: ${route}`);
|
|
48
|
+
const attempt = routeCounts.get(route) || 0;
|
|
49
|
+
routeCounts.set(route, attempt + 1);
|
|
50
|
+
const canned = routeResponses[route];
|
|
51
|
+
if (typeof canned === 'function') return canned(body, attempt);
|
|
52
|
+
return canned === undefined ? {} : canned;
|
|
53
|
+
}
|
|
54
|
+
api.calls = calls;
|
|
55
|
+
api.bodiesFor = (routePath) => calls.filter((c) => c.routePath === routePath).map((c) => c.body);
|
|
56
|
+
return api;
|
|
57
|
+
}
|
|
58
|
+
|
|
59
|
+
/**
|
|
60
|
+
* An api stub that mints synthetic queue rows, so a test can exercise the
|
|
61
|
+
* continuation path end to end. `POST /api/continuation-turn` answers with a
|
|
62
|
+
* fresh `messageId`, exactly as the relay route does.
|
|
63
|
+
*/
|
|
64
|
+
export function makeContinuationApiStub({
|
|
65
|
+
conversationId = 'conv-1',
|
|
66
|
+
messageIds = ['cont-1', 'cont-2', 'cont-3'],
|
|
67
|
+
...options
|
|
68
|
+
} = {}) {
|
|
69
|
+
const minted = [];
|
|
70
|
+
const stub = makeApiStub({
|
|
71
|
+
...options,
|
|
72
|
+
routeResponses: {
|
|
73
|
+
'/api/continuation-turn': (body, attempt) => {
|
|
74
|
+
const messageId = messageIds[Math.min(attempt, messageIds.length - 1)];
|
|
75
|
+
minted.push({ messageId, body });
|
|
76
|
+
return { ok: true, messageId, conversationId };
|
|
77
|
+
},
|
|
78
|
+
...(options.routeResponses || {}),
|
|
79
|
+
},
|
|
80
|
+
});
|
|
81
|
+
stub.mintedContinuations = minted;
|
|
82
|
+
return stub;
|
|
83
|
+
}
|
|
84
|
+
|
|
85
|
+
/**
|
|
86
|
+
* A fake CopilotSession. `send()` accepts one `MessageOptions` object (the
|
|
87
|
+
* real signature: there is no second parameter) and, like the real SDK,
|
|
88
|
+
* resolves immediately — the turn's completion arrives on the event callback,
|
|
89
|
+
* which is what `onSend` is for.
|
|
90
|
+
*/
|
|
91
|
+
export function createFakeCopilotSession({ config, onSend = null, onAbort = null }) {
|
|
92
|
+
const session = {
|
|
93
|
+
sessionId: config?.sessionId || 'fake-session',
|
|
94
|
+
config,
|
|
95
|
+
sends: [],
|
|
96
|
+
abortCalls: 0,
|
|
97
|
+
setModelCalls: [],
|
|
98
|
+
disconnected: false,
|
|
99
|
+
emit(event) {
|
|
100
|
+
config?.onEvent?.(event);
|
|
101
|
+
},
|
|
102
|
+
replay(events) {
|
|
103
|
+
for (const event of events) session.emit(event);
|
|
104
|
+
},
|
|
105
|
+
async send(options) {
|
|
106
|
+
session.sends.push(options);
|
|
107
|
+
// Scheduled rather than synchronous so the runner's `await session.send`
|
|
108
|
+
// resolves before the events land, matching the real ordering.
|
|
109
|
+
setTimeout(() => { onSend?.(session, options); }, 0);
|
|
110
|
+
return 'fake-message-id';
|
|
111
|
+
},
|
|
112
|
+
async abort() {
|
|
113
|
+
session.abortCalls += 1;
|
|
114
|
+
setTimeout(() => { onAbort?.(session); }, 0);
|
|
115
|
+
},
|
|
116
|
+
async setModel(model) {
|
|
117
|
+
session.setModelCalls.push(model);
|
|
118
|
+
},
|
|
119
|
+
async disconnect() {
|
|
120
|
+
session.disconnected = true;
|
|
121
|
+
},
|
|
122
|
+
};
|
|
123
|
+
return session;
|
|
124
|
+
}
|
|
125
|
+
|
|
126
|
+
/**
|
|
127
|
+
* The two ways `resumeSession` fails, which the runner MUST tell apart.
|
|
128
|
+
*
|
|
129
|
+
* - `missing` — the runtime has no state under this id. Shaped like the
|
|
130
|
+
* real thing: the runtime throws `Session not found: <id>`
|
|
131
|
+
* and JSON-RPC wraps it as a generic InternalError (-32603),
|
|
132
|
+
* so the message is the only signal.
|
|
133
|
+
* - `transient` — the call itself failed (connection dropped). Falling
|
|
134
|
+
* through to `createSession` here would blank a live
|
|
135
|
+
* conversation.
|
|
136
|
+
*/
|
|
137
|
+
export function makeResumeFailure(flavor, sessionId = 'conv-1') {
|
|
138
|
+
if (flavor === 'transient') {
|
|
139
|
+
const error = new Error('Pending response rejected since connection got disposed');
|
|
140
|
+
error.code = -32097;
|
|
141
|
+
return error;
|
|
142
|
+
}
|
|
143
|
+
const error = new Error(`Request session.resume failed with message: Session not found: ${sessionId}`);
|
|
144
|
+
error.code = -32603;
|
|
145
|
+
return error;
|
|
146
|
+
}
|
|
147
|
+
|
|
148
|
+
/**
|
|
149
|
+
* A fake CopilotClient. `resumeAvailable: false` models a brand-new
|
|
150
|
+
* conversation (the runtime has no state to resume), which is what makes the
|
|
151
|
+
* runner fall through to `createSession`. `resumeFailure: 'transient'` models
|
|
152
|
+
* the RPC itself failing, which must NOT fall through.
|
|
153
|
+
*/
|
|
154
|
+
export function createFakeCopilotClient({
|
|
155
|
+
resumeAvailable = false,
|
|
156
|
+
resumeFailure = 'missing',
|
|
157
|
+
onSend = null,
|
|
158
|
+
onAbort = null,
|
|
159
|
+
} = {}) {
|
|
160
|
+
const client = {
|
|
161
|
+
stopped: 0,
|
|
162
|
+
resumeAttempts: [],
|
|
163
|
+
createAttempts: [],
|
|
164
|
+
sessions: [],
|
|
165
|
+
resumeAvailable,
|
|
166
|
+
resumeFailure,
|
|
167
|
+
async resumeSession(sessionId, config) {
|
|
168
|
+
client.resumeAttempts.push({ sessionId, config });
|
|
169
|
+
if (!client.resumeAvailable) throw makeResumeFailure(client.resumeFailure, sessionId);
|
|
170
|
+
const session = createFakeCopilotSession({ config, onSend, onAbort });
|
|
171
|
+
session.resumed = true;
|
|
172
|
+
client.sessions.push(session);
|
|
173
|
+
// A session that exists once exists forever, so later reconnects resume.
|
|
174
|
+
client.resumeAvailable = true;
|
|
175
|
+
return session;
|
|
176
|
+
},
|
|
177
|
+
async createSession(config) {
|
|
178
|
+
client.createAttempts.push(config);
|
|
179
|
+
const session = createFakeCopilotSession({ config, onSend, onAbort });
|
|
180
|
+
client.sessions.push(session);
|
|
181
|
+
client.resumeAvailable = true;
|
|
182
|
+
return session;
|
|
183
|
+
},
|
|
184
|
+
async stop() {
|
|
185
|
+
client.stopped += 1;
|
|
186
|
+
return [];
|
|
187
|
+
},
|
|
188
|
+
};
|
|
189
|
+
Object.defineProperty(client, 'session', {
|
|
190
|
+
get: () => client.sessions[client.sessions.length - 1] || null,
|
|
191
|
+
});
|
|
192
|
+
return client;
|
|
193
|
+
}
|
|
194
|
+
|
|
195
|
+
export const FAKE_SDK_PATHS = {
|
|
196
|
+
sdkDir: '/opt/copilot/pkg/linux-x64/1.0.82/copilot-sdk',
|
|
197
|
+
sdkEntry: '/opt/copilot/pkg/linux-x64/1.0.82/copilot-sdk/index.js',
|
|
198
|
+
versionDir: '/opt/copilot/pkg/linux-x64/1.0.82',
|
|
199
|
+
runtimeEntry: '/opt/copilot/pkg/linux-x64/1.0.82/app.js',
|
|
200
|
+
version: '1.0.82',
|
|
201
|
+
};
|
|
202
|
+
|
|
203
|
+
/** Frozen delivery message; tests derive variants by spread. */
|
|
204
|
+
export const baseMessage = Object.freeze({
|
|
205
|
+
id: 'q-1',
|
|
206
|
+
conversationId: 'conv-1',
|
|
207
|
+
relayMode: 'agent',
|
|
208
|
+
text: 'hello',
|
|
209
|
+
model: 'gpt-5-mini',
|
|
210
|
+
});
|
|
211
|
+
|
|
212
|
+
/**
|
|
213
|
+
* A stand-in for the relay question bridge.
|
|
214
|
+
*
|
|
215
|
+
* `answers` maps a matcher to a canned reply, so a test can drive `ask_user`
|
|
216
|
+
* and ask-mode tool approvals without a relay. Every call is recorded.
|
|
217
|
+
*/
|
|
218
|
+
export function makeFakeQuestionBridge({
|
|
219
|
+
userInputAnswer = 'the human answer',
|
|
220
|
+
userInputTimedOut = false,
|
|
221
|
+
approve = true,
|
|
222
|
+
approvalTimedOut = false,
|
|
223
|
+
approvalFeedback = 'The user declined this action.',
|
|
224
|
+
onAsk = null,
|
|
225
|
+
} = {}) {
|
|
226
|
+
const bridge = {
|
|
227
|
+
userInputCalls: [],
|
|
228
|
+
approvalCalls: [],
|
|
229
|
+
cancelledCount: 0,
|
|
230
|
+
async askUserInput(request, options) {
|
|
231
|
+
bridge.userInputCalls.push({ request, options });
|
|
232
|
+
if (onAsk) await onAsk(request, options);
|
|
233
|
+
const choices = Array.isArray(request?.choices) ? request.choices : [];
|
|
234
|
+
const answer = typeof userInputAnswer === 'function' ? userInputAnswer(request) : userInputAnswer;
|
|
235
|
+
return {
|
|
236
|
+
answer,
|
|
237
|
+
wasFreeform: userInputTimedOut ? true : !choices.includes(answer),
|
|
238
|
+
timedOut: userInputTimedOut,
|
|
239
|
+
};
|
|
240
|
+
},
|
|
241
|
+
async askToolApproval(request, options) {
|
|
242
|
+
bridge.approvalCalls.push({ request, options });
|
|
243
|
+
if (onAsk) await onAsk(request, options);
|
|
244
|
+
return {
|
|
245
|
+
approved: approvalTimedOut ? false : approve,
|
|
246
|
+
answer: approve ? 'Approve' : 'Deny',
|
|
247
|
+
feedback: approvalFeedback,
|
|
248
|
+
timedOut: approvalTimedOut,
|
|
249
|
+
description: 'a tool',
|
|
250
|
+
};
|
|
251
|
+
},
|
|
252
|
+
async cancelPendingQuestions() {
|
|
253
|
+
bridge.cancelledCount += 1;
|
|
254
|
+
return 0;
|
|
255
|
+
},
|
|
256
|
+
pendingQuestionCount: () => 0,
|
|
257
|
+
};
|
|
258
|
+
return bridge;
|
|
259
|
+
}
|
|
260
|
+
|
|
261
|
+
/**
|
|
262
|
+
* The relay-context prefix the runner prepends to every prompt, for tests that
|
|
263
|
+
* assert on the prompt. `relayToolInstructions` defaults to '' in the harness,
|
|
264
|
+
* so only the mode marker and the mode's standing instructions appear, and the
|
|
265
|
+
* instructions only on the FIRST turn of a given relay mode.
|
|
266
|
+
*/
|
|
267
|
+
export function expectedPromptPrefix(mode = 'agent', { includeInstructions = true } = {}) {
|
|
268
|
+
const marker = `[Relay mode: ${mode}]`;
|
|
269
|
+
if (!includeInstructions) return marker;
|
|
270
|
+
const instructions = {
|
|
271
|
+
agent: 'Proceed as an interactive coding agent and use tools as needed. If you need clarification, pause and ask through the web relay instead of stalling silently. These instructions remain in effect until relay mode changes.',
|
|
272
|
+
plan: 'Draft a concise plan only. Use read-only inspection tools (glob, rg, view) only when they materially improve plan quality; otherwise draft from provided context. Do not edit repository files or run mutating commands unless the user explicitly asks for implementation. If clarification is required, pause and ask through the web relay. These instructions remain in effect until relay mode changes.',
|
|
273
|
+
ask: 'Prioritize clarification questions before doing any implementation work. If the request is ambiguous or underspecified, pause and ask through the web relay before making assumptions. Do not make broad assumptions when a question would materially change the result. These instructions remain in effect until relay mode changes.',
|
|
274
|
+
autopilot: 'Act directly on the request and use tools when needed. Keep moving unless user input is truly blocking. These instructions remain in effect until relay mode changes.',
|
|
275
|
+
}[mode];
|
|
276
|
+
return `${marker} ${instructions}`;
|
|
277
|
+
}
|
|
278
|
+
|
|
279
|
+
/** The message body with the relay-context prefix in front, as sent. */
|
|
280
|
+
export function promptWithPrefix(body, mode = 'agent', options = {}) {
|
|
281
|
+
return `${expectedPromptPrefix(mode, options)} ${body}`;
|
|
282
|
+
}
|
|
283
|
+
|
|
284
|
+
export function makeRunner({
|
|
285
|
+
stub,
|
|
286
|
+
client,
|
|
287
|
+
startWarning = null,
|
|
288
|
+
runtimeVersion = '1.0.82',
|
|
289
|
+
questionBridge = makeFakeQuestionBridge(),
|
|
290
|
+
...overrides
|
|
291
|
+
} = {}) {
|
|
292
|
+
const started = [];
|
|
293
|
+
return {
|
|
294
|
+
started,
|
|
295
|
+
questionBridge,
|
|
296
|
+
runner: createCopilotSdkSessionRunner({
|
|
297
|
+
api: stub,
|
|
298
|
+
sdkSessionId: 'conv-1',
|
|
299
|
+
cwd: '/tmp/relay-fixture-workspace',
|
|
300
|
+
resolvePathsImpl: () => FAKE_SDK_PATHS,
|
|
301
|
+
// Spawn-free and relay-free by default: no test reads the real
|
|
302
|
+
// relay-tools.md off disk or asks a relay whether previews are on.
|
|
303
|
+
relayToolInstructions: '',
|
|
304
|
+
getPreviewInstructionsImpl: () => '',
|
|
305
|
+
createQuestionBridgeImpl: () => questionBridge,
|
|
306
|
+
startClientImpl: async (options) => {
|
|
307
|
+
started.push(options);
|
|
308
|
+
return {
|
|
309
|
+
sdk: {},
|
|
310
|
+
client,
|
|
311
|
+
paths: options.paths,
|
|
312
|
+
// The real adapter never blocks client start on the version probe;
|
|
313
|
+
// it hands back a promise the runner logs when it settles.
|
|
314
|
+
versionReady: Promise.resolve({ runtimeVersion, versionSkewWarning: startWarning }),
|
|
315
|
+
};
|
|
316
|
+
},
|
|
317
|
+
...overrides,
|
|
318
|
+
}),
|
|
319
|
+
};
|
|
320
|
+
}
|
|
321
|
+
|
|
322
|
+
/**
|
|
323
|
+
* The events of a fixture up to and including the first one matching `type`.
|
|
324
|
+
* Lets a test replay "everything through session.idle" without hand-listing.
|
|
325
|
+
*/
|
|
326
|
+
export function eventsThrough(events, type) {
|
|
327
|
+
const index = events.findIndex((event) => event.type === type);
|
|
328
|
+
return index === -1 ? events.slice() : events.slice(0, index + 1);
|
|
329
|
+
}
|