@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,483 @@
|
|
|
1
|
+
'use strict';
|
|
2
|
+
|
|
3
|
+
import { URL } from 'url';
|
|
4
|
+
|
|
5
|
+
function normalizeText(value) {
|
|
6
|
+
const text = String(value || '').trim();
|
|
7
|
+
return text || null;
|
|
8
|
+
}
|
|
9
|
+
|
|
10
|
+
function normalizePathPrefix(value) {
|
|
11
|
+
const raw = String(value || '').trim();
|
|
12
|
+
if (!raw) return '';
|
|
13
|
+
const prefixed = raw.startsWith('/') ? raw : `/${raw}`;
|
|
14
|
+
return prefixed.replace(/\/+$/, '');
|
|
15
|
+
}
|
|
16
|
+
|
|
17
|
+
export function buildAcceptedWorkerPaths(pathPrefix) {
|
|
18
|
+
const basePath = '/api/session-worker/ws';
|
|
19
|
+
const prefixed = `${normalizePathPrefix(pathPrefix)}${basePath}` || basePath;
|
|
20
|
+
return Array.from(new Set([basePath, prefixed]));
|
|
21
|
+
}
|
|
22
|
+
|
|
23
|
+
function parseCookies(header) {
|
|
24
|
+
const out = {};
|
|
25
|
+
for (const part of String(header || '').split(/;\s*/)) {
|
|
26
|
+
if (!part) continue;
|
|
27
|
+
const idx = part.indexOf('=');
|
|
28
|
+
if (idx < 0) continue;
|
|
29
|
+
const key = part.slice(0, idx).trim();
|
|
30
|
+
const value = part.slice(idx + 1).trim();
|
|
31
|
+
if (!key) continue;
|
|
32
|
+
out[key] = decodeURIComponent(value);
|
|
33
|
+
}
|
|
34
|
+
return out;
|
|
35
|
+
}
|
|
36
|
+
|
|
37
|
+
function readAuthTokenFromUpgrade(req) {
|
|
38
|
+
const host = String(req?.headers?.host || 'localhost');
|
|
39
|
+
let parsedUrl = null;
|
|
40
|
+
try {
|
|
41
|
+
parsedUrl = new URL(String(req?.url || ''), `http://${host}`);
|
|
42
|
+
} catch {
|
|
43
|
+
parsedUrl = null;
|
|
44
|
+
}
|
|
45
|
+
const authHeader = String(req?.headers?.authorization || '').replace(/^Bearer\s+/i, '').trim();
|
|
46
|
+
const queryToken = normalizeText(parsedUrl?.searchParams?.get('token'));
|
|
47
|
+
const cookies = parseCookies(req?.headers?.cookie);
|
|
48
|
+
const cookieToken = normalizeText(cookies.copilot_auth);
|
|
49
|
+
return authHeader || queryToken || cookieToken || null;
|
|
50
|
+
}
|
|
51
|
+
|
|
52
|
+
function normalizeQueueSnapshot(value) {
|
|
53
|
+
const queue = value && typeof value === 'object' ? value : {};
|
|
54
|
+
const pendingCount = Math.max(0, Number(queue.pendingCount) || 0);
|
|
55
|
+
const processingCount = Math.max(0, Number(queue.processingCount) || 0);
|
|
56
|
+
const parkedCount = Math.max(0, Number(queue.parkedCount) || 0);
|
|
57
|
+
return { pendingCount, processingCount, parkedCount };
|
|
58
|
+
}
|
|
59
|
+
|
|
60
|
+
function normalizePositiveInt(value) {
|
|
61
|
+
const numeric = Number(value);
|
|
62
|
+
if (!Number.isFinite(numeric)) return null;
|
|
63
|
+
const intValue = Math.trunc(numeric);
|
|
64
|
+
return intValue > 0 ? intValue : null;
|
|
65
|
+
}
|
|
66
|
+
|
|
67
|
+
function parseUpgradeIdentity(req) {
|
|
68
|
+
const host = String(req?.headers?.host || 'localhost');
|
|
69
|
+
let parsedUrl = null;
|
|
70
|
+
try {
|
|
71
|
+
parsedUrl = new URL(String(req?.url || ''), `http://${host}`);
|
|
72
|
+
} catch {
|
|
73
|
+
parsedUrl = null;
|
|
74
|
+
}
|
|
75
|
+
return {
|
|
76
|
+
sessionId: normalizeText(parsedUrl?.searchParams?.get('sessionId')),
|
|
77
|
+
pid: normalizePositiveInt(parsedUrl?.searchParams?.get('pid')),
|
|
78
|
+
};
|
|
79
|
+
}
|
|
80
|
+
|
|
81
|
+
function queueSnapshotChanged(left, right) {
|
|
82
|
+
if (!left || !right) return true;
|
|
83
|
+
return left.pendingCount !== right.pendingCount
|
|
84
|
+
|| left.processingCount !== right.processingCount
|
|
85
|
+
|| left.parkedCount !== right.parkedCount;
|
|
86
|
+
}
|
|
87
|
+
|
|
88
|
+
export function createSessionWorkerWebSocketService({
|
|
89
|
+
WebSocketServerImpl,
|
|
90
|
+
httpServer,
|
|
91
|
+
authToken,
|
|
92
|
+
queueCounts,
|
|
93
|
+
touchCli = () => {},
|
|
94
|
+
noteWorkerHeartbeat = () => {},
|
|
95
|
+
onDeliverySendFailed = async () => {},
|
|
96
|
+
onWorkerSocketClosed = async () => {},
|
|
97
|
+
requestWork = async () => null,
|
|
98
|
+
// Liveness probe for a socket's worker process. A killed worker's socket
|
|
99
|
+
// survives its process by an event-loop turn (the kill path blocks the loop,
|
|
100
|
+
// so this service's timer runs before the socket close lands), and asking for
|
|
101
|
+
// work on its behalf makes the relay spawn a replacement for the session that
|
|
102
|
+
// was just killed.
|
|
103
|
+
isWorkerProcessAlive = () => true,
|
|
104
|
+
pathPrefix = '',
|
|
105
|
+
pollIntervalMs = 1000,
|
|
106
|
+
nowIso = () => new Date().toISOString(),
|
|
107
|
+
logger = console,
|
|
108
|
+
} = {}) {
|
|
109
|
+
if (!WebSocketServerImpl) throw new Error('createSessionWorkerWebSocketService requires WebSocketServerImpl');
|
|
110
|
+
if (!httpServer) throw new Error('createSessionWorkerWebSocketService requires httpServer');
|
|
111
|
+
if (typeof queueCounts !== 'function') throw new Error('createSessionWorkerWebSocketService requires queueCounts');
|
|
112
|
+
|
|
113
|
+
const acceptedPaths = buildAcceptedWorkerPaths(pathPrefix);
|
|
114
|
+
const wsPath = acceptedPaths[acceptedPaths.length - 1];
|
|
115
|
+
const wss = new WebSocketServerImpl({ noServer: true });
|
|
116
|
+
const clients = new Set();
|
|
117
|
+
const clientState = new Map();
|
|
118
|
+
const noop = () => {};
|
|
119
|
+
const logDebug = typeof logger?.debug === 'function' ? logger.debug.bind(logger) : noop;
|
|
120
|
+
const logWarn = typeof logger?.warn === 'function' ? logger.warn.bind(logger) : noop;
|
|
121
|
+
|
|
122
|
+
let interval = null;
|
|
123
|
+
let lastSnapshot = null;
|
|
124
|
+
let attached = false;
|
|
125
|
+
|
|
126
|
+
function toQueueChangedEvent(reason = 'queue-update') {
|
|
127
|
+
const snapshot = normalizeQueueSnapshot(queueCounts());
|
|
128
|
+
return {
|
|
129
|
+
type: 'queue.changed',
|
|
130
|
+
reason: String(reason || 'queue-update'),
|
|
131
|
+
pendingCount: snapshot.pendingCount,
|
|
132
|
+
processingCount: snapshot.processingCount,
|
|
133
|
+
parkedCount: snapshot.parkedCount,
|
|
134
|
+
timestamp: nowIso(),
|
|
135
|
+
};
|
|
136
|
+
}
|
|
137
|
+
|
|
138
|
+
function emitQueueChanged(reason = 'queue-update') {
|
|
139
|
+
const event = toQueueChangedEvent(reason);
|
|
140
|
+
const payload = JSON.stringify(event);
|
|
141
|
+
for (const socket of clients) {
|
|
142
|
+
if (socket.readyState !== socket.OPEN) continue;
|
|
143
|
+
try {
|
|
144
|
+
socket.send(payload);
|
|
145
|
+
} catch {
|
|
146
|
+
// Ignore transient socket send failures. close/cleanup handlers own lifecycle.
|
|
147
|
+
}
|
|
148
|
+
}
|
|
149
|
+
return event;
|
|
150
|
+
}
|
|
151
|
+
|
|
152
|
+
function emitEvent(socket, event) {
|
|
153
|
+
if (!socket || socket.readyState !== socket.OPEN) return false;
|
|
154
|
+
try {
|
|
155
|
+
socket.send(JSON.stringify(event));
|
|
156
|
+
return true;
|
|
157
|
+
} catch {
|
|
158
|
+
return false;
|
|
159
|
+
}
|
|
160
|
+
}
|
|
161
|
+
|
|
162
|
+
function noteHeartbeat(meta, reason = 'worker-message') {
|
|
163
|
+
if (!meta?.sessionId) return;
|
|
164
|
+
try {
|
|
165
|
+
noteWorkerHeartbeat(meta.sessionId, {
|
|
166
|
+
pid: meta.pid,
|
|
167
|
+
reason,
|
|
168
|
+
timestamp: nowIso(),
|
|
169
|
+
});
|
|
170
|
+
} catch {
|
|
171
|
+
// Heartbeat bookkeeping should not break socket delivery.
|
|
172
|
+
}
|
|
173
|
+
}
|
|
174
|
+
|
|
175
|
+
async function maybeDeliverToSocket(socket, reason = 'ready') {
|
|
176
|
+
const meta = clientState.get(socket);
|
|
177
|
+
if (!meta || meta.delivering || !meta.ready) return false;
|
|
178
|
+
if (!meta.sessionId) return false;
|
|
179
|
+
if (socket.readyState !== socket.OPEN) return false;
|
|
180
|
+
if (meta.pid && isWorkerProcessAlive(meta.pid) === false) return false;
|
|
181
|
+
meta.delivering = true;
|
|
182
|
+
try {
|
|
183
|
+
const pending = await requestWork({
|
|
184
|
+
sessionId: meta.sessionId,
|
|
185
|
+
pid: meta.pid,
|
|
186
|
+
reason,
|
|
187
|
+
});
|
|
188
|
+
touchCli();
|
|
189
|
+
if (!pending || typeof pending !== 'object') return false;
|
|
190
|
+
if (pending.message) {
|
|
191
|
+
const deliveredAt = nowIso();
|
|
192
|
+
const sent = emitEvent(socket, {
|
|
193
|
+
type: 'queue.deliver',
|
|
194
|
+
reason,
|
|
195
|
+
pending,
|
|
196
|
+
timestamp: deliveredAt,
|
|
197
|
+
});
|
|
198
|
+
if (sent) {
|
|
199
|
+
meta.ready = false;
|
|
200
|
+
meta.lastDeliveredAt = deliveredAt;
|
|
201
|
+
meta.deliveryFailures = 0;
|
|
202
|
+
return true;
|
|
203
|
+
}
|
|
204
|
+
meta.deliveryFailures = Number(meta.deliveryFailures || 0) + 1;
|
|
205
|
+
meta.ready = true;
|
|
206
|
+
logWarn(`[worker-ws] delivery send failed for ${String(meta.sessionId || 'unknown').slice(0, 8)} reason=${reason}`);
|
|
207
|
+
await Promise.resolve(onDeliverySendFailed({
|
|
208
|
+
pending,
|
|
209
|
+
sessionId: meta.sessionId,
|
|
210
|
+
pid: meta.pid,
|
|
211
|
+
reason,
|
|
212
|
+
deliveryFailures: meta.deliveryFailures,
|
|
213
|
+
})).catch((error) => {
|
|
214
|
+
logWarn(`[worker-ws] delivery send failure recovery failed for ${String(meta.sessionId || 'unknown').slice(0, 8)}: ${error?.message || error}`);
|
|
215
|
+
});
|
|
216
|
+
try { socket.close(); } catch {}
|
|
217
|
+
return false;
|
|
218
|
+
}
|
|
219
|
+
if (pending.paused || pending.reason || pending.routing?.blockedReason) {
|
|
220
|
+
emitEvent(socket, {
|
|
221
|
+
type: 'queue.blocked',
|
|
222
|
+
reason: String(pending.reason || pending.routing?.blockedReason || reason || 'blocked'),
|
|
223
|
+
pending,
|
|
224
|
+
timestamp: nowIso(),
|
|
225
|
+
});
|
|
226
|
+
}
|
|
227
|
+
return false;
|
|
228
|
+
} catch (error) {
|
|
229
|
+
logWarn(`[worker-ws] delivery failed for ${String(meta.sessionId || 'unknown').slice(0, 8)}: ${error?.message || error}`);
|
|
230
|
+
emitEvent(socket, {
|
|
231
|
+
type: 'queue.error',
|
|
232
|
+
reason: 'delivery-failed',
|
|
233
|
+
error: String(error?.message || error || 'unknown delivery failure'),
|
|
234
|
+
timestamp: nowIso(),
|
|
235
|
+
});
|
|
236
|
+
return false;
|
|
237
|
+
} finally {
|
|
238
|
+
meta.delivering = false;
|
|
239
|
+
}
|
|
240
|
+
}
|
|
241
|
+
|
|
242
|
+
async function maybeDeliverToReadyClients(reason = 'queue-update') {
|
|
243
|
+
const deliveries = [];
|
|
244
|
+
for (const socket of clients) {
|
|
245
|
+
const meta = clientState.get(socket);
|
|
246
|
+
if (!meta?.ready || meta.delivering) continue;
|
|
247
|
+
deliveries.push(maybeDeliverToSocket(socket, reason));
|
|
248
|
+
}
|
|
249
|
+
if (!deliveries.length) return;
|
|
250
|
+
await Promise.allSettled(deliveries);
|
|
251
|
+
}
|
|
252
|
+
|
|
253
|
+
function monitorQueue() {
|
|
254
|
+
const snapshot = normalizeQueueSnapshot(queueCounts());
|
|
255
|
+
const changed = queueSnapshotChanged(snapshot, lastSnapshot);
|
|
256
|
+
lastSnapshot = snapshot;
|
|
257
|
+
if (changed) {
|
|
258
|
+
emitQueueChanged('queue-count-changed');
|
|
259
|
+
}
|
|
260
|
+
void maybeDeliverToReadyClients(changed ? 'queue-count-changed' : 'ready-check');
|
|
261
|
+
}
|
|
262
|
+
|
|
263
|
+
function rejectUpgrade(socket) {
|
|
264
|
+
try {
|
|
265
|
+
socket.write('HTTP/1.1 401 Unauthorized\r\nConnection: close\r\n\r\n');
|
|
266
|
+
} catch {}
|
|
267
|
+
try {
|
|
268
|
+
socket.destroy();
|
|
269
|
+
} catch {}
|
|
270
|
+
}
|
|
271
|
+
|
|
272
|
+
function matchesPath(req) {
|
|
273
|
+
const host = String(req?.headers?.host || 'localhost');
|
|
274
|
+
let pathname = '';
|
|
275
|
+
try {
|
|
276
|
+
pathname = new URL(String(req?.url || ''), `http://${host}`).pathname;
|
|
277
|
+
} catch {
|
|
278
|
+
return false;
|
|
279
|
+
}
|
|
280
|
+
return acceptedPaths.includes(pathname);
|
|
281
|
+
}
|
|
282
|
+
|
|
283
|
+
function handleUpgrade(req, socket, head) {
|
|
284
|
+
if (!matchesPath(req)) return false;
|
|
285
|
+
const token = readAuthTokenFromUpgrade(req);
|
|
286
|
+
if (String(token || '') !== String(authToken || '')) {
|
|
287
|
+
rejectUpgrade(socket);
|
|
288
|
+
return true;
|
|
289
|
+
}
|
|
290
|
+
wss.handleUpgrade(req, socket, head, (ws) => {
|
|
291
|
+
wss.emit('connection', ws, req);
|
|
292
|
+
});
|
|
293
|
+
return true;
|
|
294
|
+
}
|
|
295
|
+
|
|
296
|
+
function onConnection(ws, req) {
|
|
297
|
+
clients.add(ws);
|
|
298
|
+
const identity = parseUpgradeIdentity(req);
|
|
299
|
+
clientState.set(ws, {
|
|
300
|
+
sessionId: identity.sessionId,
|
|
301
|
+
pid: identity.pid,
|
|
302
|
+
ready: false,
|
|
303
|
+
delivering: false,
|
|
304
|
+
connectedAt: nowIso(),
|
|
305
|
+
lastDeliveredAt: null,
|
|
306
|
+
deliveryFailures: 0,
|
|
307
|
+
});
|
|
308
|
+
touchCli();
|
|
309
|
+
emitEvent(ws, {
|
|
310
|
+
type: 'server.hello',
|
|
311
|
+
reason: 'connected',
|
|
312
|
+
queue: normalizeQueueSnapshot(queueCounts()),
|
|
313
|
+
sessionId: identity.sessionId,
|
|
314
|
+
timestamp: nowIso(),
|
|
315
|
+
});
|
|
316
|
+
|
|
317
|
+
ws.on('message', (raw) => {
|
|
318
|
+
touchCli();
|
|
319
|
+
let payload = null;
|
|
320
|
+
try {
|
|
321
|
+
payload = JSON.parse(String(raw || ''));
|
|
322
|
+
} catch {
|
|
323
|
+
payload = null;
|
|
324
|
+
}
|
|
325
|
+
const meta = clientState.get(ws);
|
|
326
|
+
if (!meta || !payload || typeof payload !== 'object') return;
|
|
327
|
+
if (payload.type === 'worker.hello') {
|
|
328
|
+
meta.sessionId = normalizeText(payload.sessionId) || meta.sessionId;
|
|
329
|
+
meta.pid = normalizePositiveInt(payload.pid) || meta.pid;
|
|
330
|
+
meta.ready = true;
|
|
331
|
+
meta.lastHelloAt = nowIso();
|
|
332
|
+
noteHeartbeat(meta, 'worker-hello');
|
|
333
|
+
emitEvent(ws, {
|
|
334
|
+
type: 'server.hello',
|
|
335
|
+
reason: 'ack',
|
|
336
|
+
queue: normalizeQueueSnapshot(queueCounts()),
|
|
337
|
+
sessionId: meta.sessionId,
|
|
338
|
+
timestamp: nowIso(),
|
|
339
|
+
});
|
|
340
|
+
void maybeDeliverToSocket(ws, 'worker-hello');
|
|
341
|
+
return;
|
|
342
|
+
}
|
|
343
|
+
if (payload.type === 'worker.ready') {
|
|
344
|
+
meta.sessionId = normalizeText(payload.sessionId) || meta.sessionId;
|
|
345
|
+
meta.pid = normalizePositiveInt(payload.pid) || meta.pid;
|
|
346
|
+
meta.ready = true;
|
|
347
|
+
meta.lastReadyAt = nowIso();
|
|
348
|
+
noteHeartbeat(meta, String(payload.reason || 'worker-ready'));
|
|
349
|
+
void maybeDeliverToSocket(ws, String(payload.reason || 'worker-ready'));
|
|
350
|
+
return;
|
|
351
|
+
}
|
|
352
|
+
if (payload.type === 'worker.ping') {
|
|
353
|
+
meta.sessionId = normalizeText(payload.sessionId) || meta.sessionId;
|
|
354
|
+
meta.pid = normalizePositiveInt(payload.pid) || meta.pid;
|
|
355
|
+
meta.lastPingAt = nowIso();
|
|
356
|
+
noteHeartbeat(meta, String(payload.reason || 'worker-ping'));
|
|
357
|
+
emitEvent(ws, {
|
|
358
|
+
type: 'server.pong',
|
|
359
|
+
reason: String(payload.reason || 'worker-ping'),
|
|
360
|
+
sessionId: meta.sessionId,
|
|
361
|
+
timestamp: nowIso(),
|
|
362
|
+
});
|
|
363
|
+
}
|
|
364
|
+
});
|
|
365
|
+
const dropSocket = (reason) => {
|
|
366
|
+
const meta = clientState.get(ws);
|
|
367
|
+
clients.delete(ws);
|
|
368
|
+
clientState.delete(ws);
|
|
369
|
+
// Death-detection hook: a worker socket closing while its session owns
|
|
370
|
+
// an in-flight processing row used to be silently forgotten, leaving
|
|
371
|
+
// the row to the 600s stale sweep. The handler PID-probes and recovers.
|
|
372
|
+
if (meta?.sessionId) {
|
|
373
|
+
Promise.resolve(onWorkerSocketClosed({
|
|
374
|
+
sessionId: meta.sessionId,
|
|
375
|
+
pid: meta.pid || null,
|
|
376
|
+
reason,
|
|
377
|
+
})).catch(() => {});
|
|
378
|
+
}
|
|
379
|
+
};
|
|
380
|
+
ws.on('close', () => dropSocket('close'));
|
|
381
|
+
ws.on('error', () => dropSocket('error'));
|
|
382
|
+
}
|
|
383
|
+
|
|
384
|
+
function hasWorkerSocket(sessionId) {
|
|
385
|
+
const wanted = normalizeText(sessionId);
|
|
386
|
+
if (!wanted) return false;
|
|
387
|
+
for (const meta of clientState.values()) {
|
|
388
|
+
if (meta?.sessionId === wanted) return true;
|
|
389
|
+
}
|
|
390
|
+
return false;
|
|
391
|
+
}
|
|
392
|
+
|
|
393
|
+
function start() {
|
|
394
|
+
if (!attached) {
|
|
395
|
+
httpServer.on('upgrade', onUpgrade);
|
|
396
|
+
wss.on('connection', onConnection);
|
|
397
|
+
attached = true;
|
|
398
|
+
}
|
|
399
|
+
if (!interval) {
|
|
400
|
+
interval = setInterval(monitorQueue, Math.max(250, Number(pollIntervalMs) || 1000));
|
|
401
|
+
if (typeof interval.unref === 'function') interval.unref();
|
|
402
|
+
}
|
|
403
|
+
lastSnapshot = normalizeQueueSnapshot(queueCounts());
|
|
404
|
+
emitQueueChanged('started');
|
|
405
|
+
logDebug(`[worker-ws] listening on ${wsPath}`);
|
|
406
|
+
}
|
|
407
|
+
|
|
408
|
+
function stop() {
|
|
409
|
+
if (interval) {
|
|
410
|
+
clearInterval(interval);
|
|
411
|
+
interval = null;
|
|
412
|
+
}
|
|
413
|
+
if (attached) {
|
|
414
|
+
httpServer.off('upgrade', onUpgrade);
|
|
415
|
+
attached = false;
|
|
416
|
+
}
|
|
417
|
+
for (const socket of clients) {
|
|
418
|
+
try { socket.close(); } catch {}
|
|
419
|
+
}
|
|
420
|
+
clients.clear();
|
|
421
|
+
clientState.clear();
|
|
422
|
+
try { wss.close(); } catch (error) {
|
|
423
|
+
logWarn(`[worker-ws] close failed: ${error?.message || error}`);
|
|
424
|
+
}
|
|
425
|
+
}
|
|
426
|
+
|
|
427
|
+
function emitDraining(reason = 'relay-shutdown') {
|
|
428
|
+
for (const socket of clients) {
|
|
429
|
+
emitEvent(socket, {
|
|
430
|
+
type: 'server.draining',
|
|
431
|
+
reason,
|
|
432
|
+
timestamp: nowIso(),
|
|
433
|
+
});
|
|
434
|
+
}
|
|
435
|
+
}
|
|
436
|
+
|
|
437
|
+
function status() {
|
|
438
|
+
let readyCount = 0;
|
|
439
|
+
let deliveringCount = 0;
|
|
440
|
+
for (const meta of clientState.values()) {
|
|
441
|
+
if (meta?.ready) readyCount += 1;
|
|
442
|
+
if (meta?.delivering) deliveringCount += 1;
|
|
443
|
+
}
|
|
444
|
+
return {
|
|
445
|
+
connectedCount: clients.size,
|
|
446
|
+
readyCount,
|
|
447
|
+
deliveringCount,
|
|
448
|
+
path: wsPath,
|
|
449
|
+
acceptedPaths,
|
|
450
|
+
};
|
|
451
|
+
}
|
|
452
|
+
|
|
453
|
+
function onUpgrade(req, socket, head) {
|
|
454
|
+
handleUpgrade(req, socket, head);
|
|
455
|
+
}
|
|
456
|
+
|
|
457
|
+
/**
|
|
458
|
+
* Fire-and-forget control push to a connected session worker (e.g. "stop
|
|
459
|
+
* background task"). Returns false when the session has no open socket —
|
|
460
|
+
* the caller surfaces that as "worker offline" instead of queueing.
|
|
461
|
+
*/
|
|
462
|
+
function sendControlToSession(sessionId, control) {
|
|
463
|
+
const normalized = normalizeText(sessionId);
|
|
464
|
+
if (!normalized) return false;
|
|
465
|
+
for (const socket of clients) {
|
|
466
|
+
const meta = clientState.get(socket);
|
|
467
|
+
if (meta?.sessionId !== normalized) continue;
|
|
468
|
+
if (emitEvent(socket, { type: 'worker.control', control, timestamp: nowIso() })) return true;
|
|
469
|
+
}
|
|
470
|
+
return false;
|
|
471
|
+
}
|
|
472
|
+
|
|
473
|
+
return {
|
|
474
|
+
start,
|
|
475
|
+
stop,
|
|
476
|
+
status,
|
|
477
|
+
emitQueueChanged,
|
|
478
|
+
emitDraining,
|
|
479
|
+
handleUpgrade,
|
|
480
|
+
sendControlToSession,
|
|
481
|
+
hasWorkerSocket,
|
|
482
|
+
};
|
|
483
|
+
}
|