@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.
Files changed (293) hide show
  1. package/.github/extensions/web-relay/README.md +190 -0
  2. package/.github/extensions/web-relay/extension.mjs +972 -0
  3. package/.github/extensions/web-relay/model-api/model-switching.mjs +338 -0
  4. package/.github/extensions/web-relay/polling/heartbeat.mjs +55 -0
  5. package/.github/extensions/web-relay/polling/polling-loop.mjs +1105 -0
  6. package/.github/extensions/web-relay/runtime/api-client.mjs +34 -0
  7. package/.github/extensions/web-relay/runtime/banner-state.mjs +126 -0
  8. package/.github/extensions/web-relay/runtime/config-loader.mjs +104 -0
  9. package/.github/extensions/web-relay/runtime/debug-log.mjs +27 -0
  10. package/.github/extensions/web-relay/runtime/send-and-wait-errors.mjs +170 -0
  11. package/.github/extensions/web-relay/runtime/session-binding.mjs +78 -0
  12. package/.github/extensions/web-relay/runtime/session-io.mjs +349 -0
  13. package/.github/extensions/web-relay/runtime/session-join-retry.mjs +44 -0
  14. package/.github/extensions/web-relay/runtime/session-registry.mjs +29 -0
  15. package/.github/extensions/web-relay/runtime/session-runtime-manager.mjs +156 -0
  16. package/.github/extensions/web-relay/runtime/session-sync-bridge.mjs +74 -0
  17. package/.github/extensions/web-relay/runtime/worker-websocket-link.mjs +325 -0
  18. package/.github/extensions/web-relay/runtime/workspace-root.mjs +55 -0
  19. package/.github/extensions/web-relay/server-lifecycle/managed-server.mjs +386 -0
  20. package/.github/extensions/web-relay/skills/prompt-context.mjs +160 -0
  21. package/.github/extensions/web-relay/skills/question-bridge.mjs +141 -0
  22. package/.github/extensions/web-relay/skills/question-routing-hooks.mjs +463 -0
  23. package/.github/extensions/web-relay/skills/reasoning-stream.mjs +285 -0
  24. package/.github/extensions/web-relay/skills/subagent-lifecycle.mjs +345 -0
  25. package/.github/extensions/web-relay/skills/tool-activity.mjs +243 -0
  26. package/.github/extensions/web-relay/utils/process-utils.mjs +25 -0
  27. package/.github/extensions/web-relay/utils/token-generation.mjs +58 -0
  28. package/CHANGELOG.md +25 -0
  29. package/LICENSE +21 -0
  30. package/README.md +717 -0
  31. package/bin/bat/oar.bat +46 -0
  32. package/bin/oar.js +665 -0
  33. package/package.json +68 -0
  34. package/server/README.md +1379 -0
  35. package/server/claude-worker/claude-attachments.mjs +134 -0
  36. package/server/claude-worker/claude-preview-tool.mjs +57 -0
  37. package/server/claude-worker/claude-sdk-adapter.mjs +420 -0
  38. package/server/claude-worker/claude-session-process.mjs +2391 -0
  39. package/server/claude-worker/claude-session-test-harness.mjs +236 -0
  40. package/server/claude-worker/claude-session-worker.mjs +186 -0
  41. package/server/claude-worker/claude-transcript-relocator.mjs +189 -0
  42. package/server/claude-worker/claude-turn-publisher.mjs +291 -0
  43. package/server/claude-worker/fixtures/workflow-run-record.json +1 -0
  44. package/server/claude-worker/sdk-message-normalizer.mjs +629 -0
  45. package/server/claude-worker/workflow-progress-digest.mjs +269 -0
  46. package/server/copilot-sdk-runtime.mjs +126 -0
  47. package/server/copilot-worker/copilot-attachments.mjs +97 -0
  48. package/server/copilot-worker/copilot-byok-provider.mjs +121 -0
  49. package/server/copilot-worker/copilot-continuation-signals.mjs +349 -0
  50. package/server/copilot-worker/copilot-plan-board.mjs +110 -0
  51. package/server/copilot-worker/copilot-prompt-context.mjs +86 -0
  52. package/server/copilot-worker/copilot-question-bridge.mjs +216 -0
  53. package/server/copilot-worker/copilot-sdk-adapter.mjs +494 -0
  54. package/server/copilot-worker/copilot-sdk-event-normalizer.mjs +800 -0
  55. package/server/copilot-worker/copilot-sdk-session-process.mjs +1729 -0
  56. package/server/copilot-worker/copilot-sdk-session-worker.mjs +167 -0
  57. package/server/copilot-worker/copilot-sdk-test-harness.mjs +329 -0
  58. package/server/copilot-worker/fixtures/abort-turn.json +588 -0
  59. package/server/copilot-worker/fixtures/ask-user-turn.json +1154 -0
  60. package/server/copilot-worker/fixtures/background-timer-continuation.json +445 -0
  61. package/server/copilot-worker/fixtures/background-timer-turn.json +497 -0
  62. package/server/copilot-worker/fixtures/happy-turn.json +566 -0
  63. package/server/copilot-worker/fixtures/quota-turn.json +647 -0
  64. package/server/copilot-worker/fixtures/reasoning-turn.json +625 -0
  65. package/server/copilot-worker/fixtures/resume-turn.json +700 -0
  66. package/server/copilot-worker/fixtures/subagent-turn.json +1378 -0
  67. package/server/copilot-worker/fixtures/tool-permission-turn.json +1293 -0
  68. package/server/cursor-worker/cursor-ask-user-tool.mjs +83 -0
  69. package/server/cursor-worker/cursor-attachments.mjs +88 -0
  70. package/server/cursor-worker/cursor-context-usage.mjs +85 -0
  71. package/server/cursor-worker/cursor-preview-tool.mjs +44 -0
  72. package/server/cursor-worker/cursor-sdk-adapter.mjs +554 -0
  73. package/server/cursor-worker/cursor-session-worker.mjs +145 -0
  74. package/server/cursor-worker/cursor-subagent-roster.mjs +81 -0
  75. package/server/cursor-worker/cursor-turn-runner.mjs +717 -0
  76. package/server/cursor-worker/sdk-message-normalizer.mjs +485 -0
  77. package/server/db-schema.mjs +932 -0
  78. package/server/features.mjs +106 -0
  79. package/server/grok-worker/acp-client.mjs +434 -0
  80. package/server/grok-worker/acp-host-services.mjs +277 -0
  81. package/server/grok-worker/grok-context-usage.mjs +77 -0
  82. package/server/grok-worker/grok-sdk-adapter.mjs +334 -0
  83. package/server/grok-worker/grok-session-worker.mjs +145 -0
  84. package/server/grok-worker/grok-turn-runner.mjs +523 -0
  85. package/server/grok-worker/sdk-message-normalizer.mjs +311 -0
  86. package/server/migrations/0001-add-structured-answer.mjs +80 -0
  87. package/server/migrations/0002-recent-workspace-roots-path-key.mjs +121 -0
  88. package/server/migrations/0003-push-subscriptions.mjs +87 -0
  89. package/server/public/app/action-confirmations.js +267 -0
  90. package/server/public/app/activity-replay-state.mjs +149 -0
  91. package/server/public/app/api-client.js +1082 -0
  92. package/server/public/app/ask-user-view.js +545 -0
  93. package/server/public/app/attachments-view.js +2112 -0
  94. package/server/public/app/auto-compact-window-options.mjs +59 -0
  95. package/server/public/app/background-tasks-view.mjs +494 -0
  96. package/server/public/app/bootstrap.js +4347 -0
  97. package/server/public/app/claude-auth-ui.js +394 -0
  98. package/server/public/app/claude-thinking-options.mjs +38 -0
  99. package/server/public/app/cli-install-ui.js +455 -0
  100. package/server/public/app/code-copy.mjs +85 -0
  101. package/server/public/app/composer-attachment-cache.mjs +151 -0
  102. package/server/public/app/composer-control-state.mjs +107 -0
  103. package/server/public/app/composer-paste.mjs +132 -0
  104. package/server/public/app/context-tier-options.mjs +68 -0
  105. package/server/public/app/context-usage-view.mjs +338 -0
  106. package/server/public/app/conversation-draft-timestamp-utils.mjs +11 -0
  107. package/server/public/app/conversation-preferences.mjs +91 -0
  108. package/server/public/app/conversation-provider-indicator.mjs +88 -0
  109. package/server/public/app/conversation-view.js +2898 -0
  110. package/server/public/app/cwd-menu-placement.mjs +85 -0
  111. package/server/public/app/cwd-picker.js +605 -0
  112. package/server/public/app/emoji-view.js +101 -0
  113. package/server/public/app/external-link-policy.mjs +80 -0
  114. package/server/public/app/file-preview-navigation.mjs +51 -0
  115. package/server/public/app/font-scaling.js +262 -0
  116. package/server/public/app/git-changes-view.js +339 -0
  117. package/server/public/app/git-diff-model.mjs +109 -0
  118. package/server/public/app/grok-auth-ui.js +387 -0
  119. package/server/public/app/image-downscale.mjs +174 -0
  120. package/server/public/app/in-flight-snapshot.mjs +40 -0
  121. package/server/public/app/infinite-loader.js +164 -0
  122. package/server/public/app/journal-view.js +1141 -0
  123. package/server/public/app/known-cwd-options.mjs +41 -0
  124. package/server/public/app/launch-cwd-path.mjs +23 -0
  125. package/server/public/app/live-message-dedupe.mjs +57 -0
  126. package/server/public/app/message-search-view.js +242 -0
  127. package/server/public/app/model-selector-options.mjs +31 -0
  128. package/server/public/app/model-vision-support.mjs +56 -0
  129. package/server/public/app/new-conversation-model-choice.mjs +67 -0
  130. package/server/public/app/plan-usage-view.mjs +307 -0
  131. package/server/public/app/preview-cards.mjs +273 -0
  132. package/server/public/app/preview-command.mjs +103 -0
  133. package/server/public/app/push-settings.js +370 -0
  134. package/server/public/app/pwa-install.js +456 -0
  135. package/server/public/app/question-schema-view.mjs +71 -0
  136. package/server/public/app/reasoning-effort-labels.mjs +42 -0
  137. package/server/public/app/relay-board-view.js +117 -0
  138. package/server/public/app/relay-dot-state.mjs +69 -0
  139. package/server/public/app/relay-error-ctas.mjs +52 -0
  140. package/server/public/app/relay-prompt-sanitizer.mjs +114 -0
  141. package/server/public/app/relay-thoughts.mjs +98 -0
  142. package/server/public/app/relay-watchdog-policy.mjs +29 -0
  143. package/server/public/app/repo-browser-preferences.mjs +79 -0
  144. package/server/public/app/repo-browser-tree-state.mjs +97 -0
  145. package/server/public/app/router.js +657 -0
  146. package/server/public/app/selection-guard.mjs +93 -0
  147. package/server/public/app/settings-modal.js +1484 -0
  148. package/server/public/app/settings-tabs.js +129 -0
  149. package/server/public/app/slash-autocomplete.mjs +188 -0
  150. package/server/public/app/slash-commands.mjs +183 -0
  151. package/server/public/app/socket-handlers.js +703 -0
  152. package/server/public/app/status-store.mjs +242 -0
  153. package/server/public/app/status-view.mjs +315 -0
  154. package/server/public/app/store.js +1454 -0
  155. package/server/public/app/stream-state.mjs +73 -0
  156. package/server/public/app/streaming-dom-patch.mjs +23 -0
  157. package/server/public/app/sync-outbox.mjs +208 -0
  158. package/server/public/app/tap-actions.js +62 -0
  159. package/server/public/app/thread-order.mjs +54 -0
  160. package/server/public/app/tmux-inspector-view.js +422 -0
  161. package/server/public/app/transcript-separators.mjs +282 -0
  162. package/server/public/app-icon-192.png +0 -0
  163. package/server/public/app-icon-512.png +0 -0
  164. package/server/public/app-icon.svg +4 -0
  165. package/server/public/favicon.ico +0 -0
  166. package/server/public/index.html +5743 -0
  167. package/server/public/manifest.webmanifest +32 -0
  168. package/server/public/sw.js +307 -0
  169. package/server/relay-exit-codes.mjs +4 -0
  170. package/server/relay-self-restart.mjs +170 -0
  171. package/server/relay-tools.md +27 -0
  172. package/server/relay.mjs +1026 -0
  173. package/server/repositories/image-conversation-repository.mjs +317 -0
  174. package/server/repositories/message-repository.mjs +434 -0
  175. package/server/repositories/question-repository.mjs +161 -0
  176. package/server/repositories/session-repository.mjs +320 -0
  177. package/server/repositories/share-visibility-statements.mjs +24 -0
  178. package/server/routes/ask-user-routes.mjs +292 -0
  179. package/server/routes/cache-routes.mjs +44 -0
  180. package/server/routes/git-routes.mjs +92 -0
  181. package/server/routes/messages-routes-test-harness.mjs +111 -0
  182. package/server/routes/messages-routes.mjs +7107 -0
  183. package/server/routes/preview-routes.mjs +108 -0
  184. package/server/routes/push-routes.mjs +59 -0
  185. package/server/routes/relay-board-routes.mjs +268 -0
  186. package/server/routes/sessions-routes.mjs +6150 -0
  187. package/server/server-runtime.mjs +7463 -0
  188. package/server/server.js +24 -0
  189. package/server/services/ask-user-routing-service.mjs +117 -0
  190. package/server/services/cache-rebuild-service.mjs +236 -0
  191. package/server/services/claude-auth-service.mjs +578 -0
  192. package/server/services/claude-context-usage.mjs +240 -0
  193. package/server/services/claude-session-root-service.mjs +228 -0
  194. package/server/services/cli-install-service.mjs +1152 -0
  195. package/server/services/cli-process-runner.mjs +181 -0
  196. package/server/services/cloudflared-tunnel-service.mjs +358 -0
  197. package/server/services/context-snapshot-service.mjs +499 -0
  198. package/server/services/context-usage-view.mjs +160 -0
  199. package/server/services/conversation-preferences-service.mjs +208 -0
  200. package/server/services/cursor-dashboard-usage.mjs +217 -0
  201. package/server/services/cursor-session-root-service.mjs +62 -0
  202. package/server/services/delete-archive-service.mjs +154 -0
  203. package/server/services/drives-path-helpers.mjs +66 -0
  204. package/server/services/fixtures/claude-auth-stub.sh +62 -0
  205. package/server/services/fixtures/cli-install-stub.sh +75 -0
  206. package/server/services/fixtures/grok-stub.sh +86 -0
  207. package/server/services/generated-image-cleanup-service.mjs +114 -0
  208. package/server/services/git-changes-service.mjs +195 -0
  209. package/server/services/github-billing-usage.mjs +133 -0
  210. package/server/services/grok-auth-service.mjs +593 -0
  211. package/server/services/grok-billing-usage.mjs +120 -0
  212. package/server/services/image-context-service.mjs +106 -0
  213. package/server/services/image-operation-service.mjs +135 -0
  214. package/server/services/keyed-mutex-service.mjs +82 -0
  215. package/server/services/mime-sniffer.mjs +118 -0
  216. package/server/services/oar-cli-helpers.mjs +109 -0
  217. package/server/services/oar-state-migration-service.mjs +243 -0
  218. package/server/services/plan-usage-claude.mjs +387 -0
  219. package/server/services/plan-usage-contract.mjs +271 -0
  220. package/server/services/plan-usage-copilot.mjs +421 -0
  221. package/server/services/plan-usage-cursor.mjs +566 -0
  222. package/server/services/plan-usage-grok.mjs +436 -0
  223. package/server/services/plan-usage-service.mjs +487 -0
  224. package/server/services/preview-health-probe.mjs +82 -0
  225. package/server/services/preview-proxy-server.mjs +498 -0
  226. package/server/services/preview-registry-service.mjs +457 -0
  227. package/server/services/preview-static-handler.mjs +237 -0
  228. package/server/services/provider-model-selection.mjs +91 -0
  229. package/server/services/provider-reasoning-effort.mjs +94 -0
  230. package/server/services/push-active-device-service.mjs +57 -0
  231. package/server/services/push-dispatch-service.mjs +459 -0
  232. package/server/services/rate-limit-service.mjs +63 -0
  233. package/server/services/relay-bridge-owner-service.mjs +84 -0
  234. package/server/services/relay-cli-launcher-service.mjs +209 -0
  235. package/server/services/relay-prompt-sanitizer.mjs +137 -0
  236. package/server/services/relay-queue-gate-service.mjs +38 -0
  237. package/server/services/relay-restart-orchestrator-service.mjs +935 -0
  238. package/server/services/relay-singleton-guard.mjs +124 -0
  239. package/server/services/safe-served-content.mjs +57 -0
  240. package/server/services/sdk-session-import-service.mjs +271 -0
  241. package/server/services/sdk-session-sync-service.mjs +207 -0
  242. package/server/services/session-history-refresh-service.mjs +497 -0
  243. package/server/services/session-transcript-service.mjs +419 -0
  244. package/server/services/session-worker-launch-service.mjs +786 -0
  245. package/server/services/session-worker-process-service.mjs +319 -0
  246. package/server/services/session-worker-registry-service.mjs +194 -0
  247. package/server/services/session-worker-stop-service.mjs +141 -0
  248. package/server/services/session-worker-supervisor-service.mjs +898 -0
  249. package/server/services/session-worker-websocket-service.mjs +483 -0
  250. package/server/services/ssh-tunnel-manager-service.mjs +423 -0
  251. package/server/services/status-event-service.mjs +158 -0
  252. package/server/services/tmux-inspector-access-policy.mjs +77 -0
  253. package/server/services/tmux-inspector-socket-service.mjs +191 -0
  254. package/server/services/tmux-inspector-stream-service.mjs +552 -0
  255. package/server/services/tunnel-worker-path-guard.mjs +107 -0
  256. package/server/services/upload-sweep.mjs +60 -0
  257. package/server/services/usage-snapshot-helpers.mjs +209 -0
  258. package/server/services/windows-autostart-service.mjs +115 -0
  259. package/server/services/workspace-root-defaults-service.mjs +34 -0
  260. package/server/services/workspace-root-path-policy.mjs +196 -0
  261. package/server/services/workspace-root-relaunch-service.mjs +132 -0
  262. package/server/services/workspace-symlink-guard.mjs +54 -0
  263. package/server/tools/capture-proxy.mjs +173 -0
  264. package/server/tty-console-bootstrap.mjs +75 -0
  265. package/server/uploads/README.md +36 -0
  266. package/server/uploads/image_input.md +540 -0
  267. package/server/windows-terminal-launcher.mjs +29 -0
  268. package/server/workspace-root.mjs +87 -0
  269. package/shared/ask-user-bridge.mjs +131 -0
  270. package/shared/auto-compact-window.mjs +116 -0
  271. package/shared/background-task-timeout.mjs +52 -0
  272. package/shared/claude-thinking.mjs +101 -0
  273. package/shared/context-window-fallbacks.mjs +148 -0
  274. package/shared/control-poller.mjs +115 -0
  275. package/shared/empty-turn-completion.mjs +20 -0
  276. package/shared/model-catalog-freshness.mjs +11 -0
  277. package/shared/model-descriptors.mjs +118 -0
  278. package/shared/model-id.mjs +147 -0
  279. package/shared/model-refresh.mjs +24 -0
  280. package/shared/openai-reasoning.mjs +13 -0
  281. package/shared/plan-lines.mjs +13 -0
  282. package/shared/preview-instructions.mjs +77 -0
  283. package/shared/preview-tool-core.mjs +191 -0
  284. package/shared/provider-routing.mjs +29 -0
  285. package/shared/question-schema.mjs +315 -0
  286. package/shared/question-timeout.mjs +18 -0
  287. package/shared/stream-emit-gating.mjs +28 -0
  288. package/shared/subagent-run-id.mjs +17 -0
  289. package/shared/thought-cap.mjs +13 -0
  290. package/shared/tool-activity.mjs +98 -0
  291. package/shared/turn-ceiling.mjs +53 -0
  292. package/shared/worker-bootstrap.mjs +44 -0
  293. package/shared/worker-crash-guard.mjs +60 -0
@@ -0,0 +1,319 @@
1
+ 'use strict';
2
+
3
+ import { execFileSync } from 'child_process';
4
+
5
+ function normalizeSessionId(value) {
6
+ const text = String(value || '').trim();
7
+ return text || null;
8
+ }
9
+
10
+ function escapeRegExp(value) {
11
+ return String(value || '').replace(/[.*+?^${}()|[\]\\]/g, '\\$&');
12
+ }
13
+
14
+ function parseSessionIdFromCommandLine(commandLine) {
15
+ const text = String(commandLine || '');
16
+ if (!text) return null;
17
+ const match = text.match(/["']?--(?:session-id|resume)["']?(?:=|\s+)(?:"([^"]+)"|'([^']+)'|([^\s"'=]+))/i);
18
+ const value = match?.[1] || match?.[2] || match?.[3] || '';
19
+ return normalizeSessionId(value);
20
+ }
21
+
22
+ function buildSessionArgPattern(targetSessionId, { includeResume = true } = {}) {
23
+ const target = normalizeSessionId(targetSessionId);
24
+ if (!target) return null;
25
+ const argName = includeResume ? 'session-id|resume' : 'session-id';
26
+ return new RegExp(
27
+ `["']?--(?:${argName})["']?(?:=|\\s+)(?:"${escapeRegExp(target)}"|'${escapeRegExp(target)}'|${escapeRegExp(target)})(?:\\s|$)`,
28
+ 'i',
29
+ );
30
+ }
31
+
32
+ function scoreWindowsWorkerCandidate(proc) {
33
+ const name = String(proc?.name || '').trim().toLowerCase();
34
+ const cmd = String(proc?.commandLine || '').trim().toLowerCase();
35
+ if (name === 'copilot.exe') return 400;
36
+ if (name === 'gh.exe' && cmd.includes('copilot')) return 300;
37
+ if (cmd.includes('gh copilot')) return 250;
38
+ if (cmd.includes('copilot.cmd') || cmd.includes('copilot-win32')) return 225;
39
+ if (name === 'cmd.exe' || name === 'powershell.exe' || name === 'conhost.exe') return 50;
40
+ return 100;
41
+ }
42
+
43
+ function isWindowsWrapperProcess(proc) {
44
+ const name = String(proc?.name || '').trim().toLowerCase();
45
+ return name === 'cmd.exe' || name === 'powershell.exe' || name === 'conhost.exe';
46
+ }
47
+
48
+ function normalizeWindowsProcess(proc) {
49
+ return {
50
+ processId: Number.isInteger(Number(proc?.processId)) ? Number(proc.processId) : null,
51
+ parentProcessId: Number.isInteger(Number(proc?.parentProcessId)) ? Number(proc.parentProcessId) : null,
52
+ name: String(proc?.name || ''),
53
+ commandLine: String(proc?.commandLine || ''),
54
+ };
55
+ }
56
+
57
+ function looksLikeCopilotWorkerProcess(proc) {
58
+ const name = String(proc?.name || '').trim().toLowerCase();
59
+ const cmd = String(proc?.commandLine || '').trim().toLowerCase();
60
+ if (!name && !cmd) return false;
61
+ if (cmd.includes('\\server\\server.js') || cmd.includes('/server/server.js')) return false;
62
+ // A tmux process is never a worker. The shared tmux server keeps the argv of
63
+ // whichever `tmux new-session -d -s <session-id> ...` first started it, so a
64
+ // session-id scan would otherwise match the server itself — and killing it
65
+ // tears down every tmux-hosted worker on the socket, not just the target
66
+ // session. (The server's comm is "tmux: server", which the ps parser splits
67
+ // into name "tmux:" with the command line shifted to start with "server".)
68
+ if (name === 'tmux' || name.startsWith('tmux:')) return false;
69
+ if (/^(?:server\s+)?tmux(?:\s|$)/.test(cmd)) return false;
70
+ if (name === 'copilot.exe') return true;
71
+ if (name === 'gh.exe' && cmd.includes('gh') && cmd.includes('copilot')) return true;
72
+ if (cmd.includes('gh copilot')) return true;
73
+ // Every node worker's entry script is named `<kind>-session-worker.mjs` and
74
+ // is invoked as `node <script> --session-id <id>` — none of the CLI markers
75
+ // below (no `--allow-all`, no `@github/copilot` path) appear on their command
76
+ // lines, so a worker missing from this check is invisible to discovery: the
77
+ // kill route no-ops and process reuse spawns a duplicate every turn. That is
78
+ // exactly what happened to `grok-session-worker`, which the per-worker list
79
+ // this replaces never named.
80
+ //
81
+ // Matching the naming convention instead of four literals means a new node
82
+ // worker is discoverable the moment it exists. The tmux exclusions above
83
+ // still run first, so the shared tmux server cannot be adopted through this
84
+ // arm by carrying a worker's argv.
85
+ if (cmd.includes('-session-worker')) return true;
86
+ return cmd.includes('copilot.cmd')
87
+ || cmd.includes('copilot-win32')
88
+ || cmd.includes('@github\\copilot')
89
+ || cmd.includes('@aykahshi/copilot-mcp-server')
90
+ || cmd.includes('--allow-all')
91
+ || cmd.includes('--resume');
92
+ }
93
+
94
+ export function createSessionWorkerProcessInspector({
95
+ platform = process.platform,
96
+ execFileSyncImpl = execFileSync,
97
+ } = {}) {
98
+ function getPosixProcessSnapshot() {
99
+ if (platform === 'win32') return [];
100
+ const output = execFileSyncImpl('ps', ['-eo', 'pid=,ppid=,comm=,args=', '-ww'], {
101
+ stdio: ['ignore', 'pipe', 'ignore'],
102
+ });
103
+ const text = String(output || '').trim();
104
+ if (!text) return [];
105
+ return text
106
+ .split(/\r?\n/)
107
+ .map((line) => {
108
+ const match = String(line || '').match(/^\s*(\d+)\s+(\d+)\s+(\S+)\s+(.*)$/);
109
+ if (!match) return null;
110
+ return {
111
+ processId: Number.parseInt(match[1], 10),
112
+ parentProcessId: Number.parseInt(match[2], 10),
113
+ name: String(match[3] || ''),
114
+ commandLine: String(match[4] || ''),
115
+ };
116
+ })
117
+ .filter(Boolean);
118
+ }
119
+
120
+ function getWindowsProcessSnapshot() {
121
+ if (platform !== 'win32') return [];
122
+ const script = [
123
+ '$list = Get-CimInstance Win32_Process | ForEach-Object {',
124
+ ' [pscustomobject]@{',
125
+ ' processId = [int]$_.ProcessId;',
126
+ ' parentProcessId = [int]$_.ParentProcessId;',
127
+ ' name = [string]$_.Name;',
128
+ ' commandLine = [string]$_.CommandLine;',
129
+ ' }',
130
+ '};',
131
+ '$list | ConvertTo-Json -Depth 3 -Compress',
132
+ ].join(' ');
133
+ const output = execFileSyncImpl('powershell.exe', ['-NoProfile', '-Command', script], {
134
+ stdio: ['ignore', 'pipe', 'ignore'],
135
+ });
136
+ const text = String(output || '').trim();
137
+ if (!text) return [];
138
+ const parsed = JSON.parse(text);
139
+ return Array.isArray(parsed) ? parsed : [parsed];
140
+ }
141
+
142
+ function isWindowsSessionMatch(proc, target, targetPattern) {
143
+ if (!proc?.processId) return false;
144
+ if (!looksLikeCopilotWorkerProcess(proc)) return false;
145
+ const parsedSessionId = parseSessionIdFromCommandLine(proc.commandLine);
146
+ if (parsedSessionId) return parsedSessionId === target;
147
+ return Boolean(targetPattern?.test(proc.commandLine));
148
+ }
149
+
150
+ function findWindowsProcessesForSession(targetSessionId) {
151
+ const target = normalizeSessionId(targetSessionId);
152
+ if (platform !== 'win32' || !target) return [];
153
+ const targetPattern = buildSessionArgPattern(target, { includeResume: false });
154
+ return getWindowsProcessSnapshot()
155
+ .map(normalizeWindowsProcess)
156
+ .filter((proc) => proc.processId)
157
+ .filter((proc) => isWindowsSessionMatch(proc, target, targetPattern))
158
+ .sort((left, right) => {
159
+ const scoreDelta = scoreWindowsWorkerCandidate(right) - scoreWindowsWorkerCandidate(left);
160
+ if (scoreDelta !== 0) return scoreDelta;
161
+ return Number(right.processId || 0) - Number(left.processId || 0);
162
+ });
163
+ }
164
+
165
+ function findWindowsProcessForSession(targetSessionId) {
166
+ const candidates = findWindowsProcessesForSession(targetSessionId);
167
+ return candidates.find((proc) => !isWindowsWrapperProcess(proc)) || null;
168
+ }
169
+
170
+ function findWindowsProcessTreeForSession(targetSessionId) {
171
+ const target = normalizeSessionId(targetSessionId);
172
+ if (platform !== 'win32' || !target) return [];
173
+ const targetPattern = buildSessionArgPattern(target, { includeResume: false });
174
+ const snapshot = getWindowsProcessSnapshot()
175
+ .map(normalizeWindowsProcess)
176
+ .filter((proc) => proc.processId);
177
+ const byPid = new Map(snapshot.map((proc) => [proc.processId, proc]));
178
+ const childrenByParent = new Map();
179
+ for (const proc of snapshot) {
180
+ if (!proc.parentProcessId) continue;
181
+ const children = childrenByParent.get(proc.parentProcessId) || [];
182
+ children.push(proc);
183
+ childrenByParent.set(proc.parentProcessId, children);
184
+ }
185
+
186
+ const related = new Map();
187
+ const addProcess = (proc) => {
188
+ if (proc?.processId) related.set(proc.processId, proc);
189
+ };
190
+ const addDescendants = (pid) => {
191
+ for (const child of childrenByParent.get(pid) || []) {
192
+ if (related.has(child.processId)) continue;
193
+ addProcess(child);
194
+ addDescendants(child.processId);
195
+ }
196
+ };
197
+ const addWrapperAncestors = (proc) => {
198
+ let current = proc;
199
+ const seen = new Set();
200
+ while (current?.parentProcessId && !seen.has(current.parentProcessId)) {
201
+ seen.add(current.parentProcessId);
202
+ const parent = byPid.get(current.parentProcessId);
203
+ if (!parent || !isWindowsWrapperProcess(parent)) return;
204
+ addProcess(parent);
205
+ current = parent;
206
+ }
207
+ };
208
+
209
+ for (const proc of snapshot.filter((candidate) => isWindowsSessionMatch(candidate, target, targetPattern))) {
210
+ addProcess(proc);
211
+ addDescendants(proc.processId);
212
+ addWrapperAncestors(proc);
213
+ }
214
+
215
+ return Array.from(related.values()).sort((left, right) => {
216
+ const scoreDelta = scoreWindowsWorkerCandidate(right) - scoreWindowsWorkerCandidate(left);
217
+ if (scoreDelta !== 0) return scoreDelta;
218
+ return Number(right.processId || 0) - Number(left.processId || 0);
219
+ });
220
+ }
221
+
222
+ function findPosixProcessesForSession(targetSessionId) {
223
+ const target = normalizeSessionId(targetSessionId);
224
+ if (platform === 'win32' || !target) return [];
225
+ const targetPattern = buildSessionArgPattern(target, { includeResume: true });
226
+ return getPosixProcessSnapshot()
227
+ .map((proc) => ({
228
+ processId: Number.isInteger(Number(proc?.processId)) ? Number(proc.processId) : null,
229
+ parentProcessId: Number.isInteger(Number(proc?.parentProcessId)) ? Number(proc.parentProcessId) : null,
230
+ name: String(proc?.name || ''),
231
+ commandLine: String(proc?.commandLine || ''),
232
+ }))
233
+ .filter((proc) => proc.processId)
234
+ .filter((proc) => looksLikeCopilotWorkerProcess(proc))
235
+ .filter((proc) => {
236
+ const parsedSessionId = parseSessionIdFromCommandLine(proc.commandLine);
237
+ if (parsedSessionId) return parsedSessionId === target;
238
+ return targetPattern?.test(proc.commandLine);
239
+ });
240
+ }
241
+
242
+ function findPosixProcessForSession(targetSessionId) {
243
+ return findPosixProcessesForSession(targetSessionId)[0] || null;
244
+ }
245
+
246
+ function parsePositiveInt(value) {
247
+ const num = Number.parseInt(String(value || ''), 10);
248
+ return Number.isInteger(num) && num > 0 ? num : null;
249
+ }
250
+
251
+ function stopWindowsPids(pids) {
252
+ const ids = Array.from(new Set(
253
+ (Array.isArray(pids) ? pids : [pids])
254
+ .map((value) => parsePositiveInt(value))
255
+ .filter(Boolean),
256
+ ));
257
+ if (!ids.length) return [];
258
+ const script = [
259
+ '$ErrorActionPreference = "Continue"',
260
+ '$ids = @(' + ids.join(',') + ')',
261
+ 'try {',
262
+ ' $snapshot = @(Get-CimInstance Win32_Process -ErrorAction Stop | ForEach-Object { [pscustomobject]@{ processId = [int]$_.ProcessId; parentProcessId = [int]$_.ParentProcessId } })',
263
+ '} catch {',
264
+ ' Write-Error $_',
265
+ ' exit 2',
266
+ '}',
267
+ '$targets = [System.Collections.Generic.HashSet[int]]::new()',
268
+ 'foreach ($id in $ids) { [void]$targets.Add([int]$id) }',
269
+ '$changed = $true',
270
+ 'while ($changed) {',
271
+ ' $changed = $false',
272
+ ' foreach ($proc in $snapshot) {',
273
+ ' if ($targets.Contains([int]$proc.parentProcessId) -and -not $targets.Contains([int]$proc.processId)) {',
274
+ ' [void]$targets.Add([int]$proc.processId)',
275
+ ' $changed = $true',
276
+ ' }',
277
+ ' }',
278
+ '}',
279
+ '$ordered = @($snapshot | Where-Object { $targets.Contains([int]$_.processId) } | Sort-Object parentProcessId -Descending | ForEach-Object { [int]$_.processId })',
280
+ '$seen = [System.Collections.Generic.HashSet[int]]::new()',
281
+ '$ids = @($ordered + $ids | Where-Object { $seen.Add([int]$_) })',
282
+ 'foreach ($id in $ids) {',
283
+ ' try { Stop-Process -Id $id -Force -ErrorAction SilentlyContinue } catch {}',
284
+ '}',
285
+ 'exit 0',
286
+ ].join('; ');
287
+ execFileSyncImpl('powershell.exe', ['-NoProfile', '-Command', script], {
288
+ stdio: ['ignore', 'ignore', 'ignore'],
289
+ });
290
+ return ids;
291
+ }
292
+
293
+ function findProcessesForSession(targetSessionId) {
294
+ return platform === 'win32'
295
+ ? findWindowsProcessesForSession(targetSessionId)
296
+ : findPosixProcessesForSession(targetSessionId);
297
+ }
298
+
299
+ function findProcessForSession(targetSessionId) {
300
+ return platform === 'win32'
301
+ ? findWindowsProcessForSession(targetSessionId)
302
+ : findPosixProcessForSession(targetSessionId);
303
+ }
304
+
305
+ return {
306
+ normalizeSessionId,
307
+ parseSessionIdFromCommandLine,
308
+ findProcessForSession,
309
+ findProcessesForSession,
310
+ findPosixProcessForSession,
311
+ findPosixProcessesForSession,
312
+ getPosixProcessSnapshot,
313
+ findWindowsProcessesForSession,
314
+ findWindowsProcessForSession,
315
+ findWindowsProcessTreeForSession,
316
+ getWindowsProcessSnapshot,
317
+ stopWindowsPids,
318
+ };
319
+ }
@@ -0,0 +1,194 @@
1
+ 'use strict';
2
+
3
+ function normalizeSessionId(value) {
4
+ const text = String(value || '').trim();
5
+ return text || null;
6
+ }
7
+
8
+ function normalizeWorkerId(value) {
9
+ const text = String(value || '').trim();
10
+ return text || null;
11
+ }
12
+
13
+ function normalizeConversationId(value) {
14
+ const text = String(value || '').trim();
15
+ return text || null;
16
+ }
17
+
18
+ function normalizeRuntimeSessionId(value) {
19
+ const text = String(value || '').trim();
20
+ return text || null;
21
+ }
22
+
23
+ function toNonNegativeInt(value, fallback = 0) {
24
+ const n = Number(value);
25
+ if (!Number.isFinite(n)) return fallback;
26
+ return Math.max(0, Math.trunc(n));
27
+ }
28
+
29
+ function toPositivePid(value) {
30
+ if (value === null || value === undefined || value === '') return null;
31
+ if (typeof value === 'number') {
32
+ if (!Number.isInteger(value) || value <= 0) return null;
33
+ return value;
34
+ }
35
+ const text = String(value).trim();
36
+ if (!/^\d+$/.test(text)) return null;
37
+ const parsed = Number(text);
38
+ return Number.isInteger(parsed) && parsed > 0 ? parsed : null;
39
+ }
40
+
41
+ function normalizeTimestamp(value, fallbackIso) {
42
+ const text = String(value || '').trim();
43
+ if (!text) return fallbackIso;
44
+ const parsed = new Date(text);
45
+ if (Number.isNaN(parsed.getTime())) return fallbackIso;
46
+ return parsed.toISOString();
47
+ }
48
+
49
+ const STATUS_ALIASES = Object.freeze({
50
+ idle: 'ready',
51
+ queued: 'processing',
52
+ failed: 'error',
53
+ });
54
+
55
+ const ALLOWED_STATUSES = new Set(['new', 'starting', 'ready', 'processing', 'error', 'stopped']);
56
+
57
+ function normalizeStatus(value) {
58
+ const normalized = String(value || 'new').trim().toLowerCase();
59
+ const mapped = STATUS_ALIASES[normalized] || normalized || 'new';
60
+ return ALLOWED_STATUSES.has(mapped) ? mapped : 'new';
61
+ }
62
+
63
+ function sanitizeState(raw = null) {
64
+ const nowIso = new Date().toISOString();
65
+ const base = raw && typeof raw === 'object' ? raw : {};
66
+ const createdAt = normalizeTimestamp(base.createdAt, nowIso);
67
+ return {
68
+ workerId: normalizeWorkerId(base.workerId),
69
+ conversationId: normalizeConversationId(base.conversationId),
70
+ runtimeSessionId: normalizeRuntimeSessionId(base.runtimeSessionId),
71
+ sdkSessionId: normalizeSessionId(base.sdkSessionId),
72
+ status: normalizeStatus(base.status),
73
+ pid: toPositivePid(base.pid),
74
+ queueDepth: toNonNegativeInt(base.queueDepth, 0),
75
+ retryCount: toNonNegativeInt(base.retryCount, 0),
76
+ lastError: String(base.lastError || '').trim() || null,
77
+ updatedAt: normalizeTimestamp(base.updatedAt, nowIso),
78
+ createdAt,
79
+ };
80
+ }
81
+
82
+ function toSnapshot(entry) {
83
+ return Object.freeze({ ...entry });
84
+ }
85
+
86
+ export function createSessionWorkerRegistry() {
87
+ const bySession = new Map();
88
+ const sessionByWorkerId = new Map();
89
+ const sessionByConversationId = new Map();
90
+ const sessionByRuntimeSessionId = new Map();
91
+
92
+ function clearIndexesForSession(entry) {
93
+ if (!entry) return;
94
+ if (entry.workerId) sessionByWorkerId.delete(entry.workerId);
95
+ if (entry.conversationId) sessionByConversationId.delete(entry.conversationId);
96
+ if (entry.runtimeSessionId) sessionByRuntimeSessionId.delete(entry.runtimeSessionId);
97
+ }
98
+
99
+ function updateIndexesForSession(entry) {
100
+ if (!entry) return;
101
+ if (entry.workerId) sessionByWorkerId.set(entry.workerId, entry.sdkSessionId);
102
+ if (entry.conversationId) sessionByConversationId.set(entry.conversationId, entry.sdkSessionId);
103
+ if (entry.runtimeSessionId) sessionByRuntimeSessionId.set(entry.runtimeSessionId, entry.sdkSessionId);
104
+ }
105
+
106
+ function getWorker(sdkSessionId) {
107
+ const sessionId = normalizeSessionId(sdkSessionId);
108
+ if (!sessionId) return null;
109
+ const entry = bySession.get(sessionId);
110
+ return entry ? toSnapshot(entry) : null;
111
+ }
112
+
113
+ function getWorkerByWorkerId(workerId) {
114
+ const normalized = normalizeWorkerId(workerId);
115
+ if (!normalized) return null;
116
+ const sessionId = sessionByWorkerId.get(normalized);
117
+ if (!sessionId) return null;
118
+ return getWorker(sessionId);
119
+ }
120
+
121
+ function getWorkerByConversationId(conversationId) {
122
+ const normalized = normalizeConversationId(conversationId);
123
+ if (!normalized) return null;
124
+ const sessionId = sessionByConversationId.get(normalized);
125
+ if (!sessionId) return null;
126
+ return getWorker(sessionId);
127
+ }
128
+
129
+ function getWorkerByRuntimeSessionId(runtimeSessionId) {
130
+ const normalized = normalizeRuntimeSessionId(runtimeSessionId);
131
+ if (!normalized) return null;
132
+ const sessionId = sessionByRuntimeSessionId.get(normalized);
133
+ if (!sessionId) return null;
134
+ return getWorker(sessionId);
135
+ }
136
+
137
+ function listWorkers() {
138
+ return Object.freeze(Array.from(bySession.values()).map((entry) => toSnapshot(entry)));
139
+ }
140
+
141
+ function upsertWorker(rawState = {}) {
142
+ if (!rawState || typeof rawState !== 'object' || Array.isArray(rawState)) {
143
+ throw new TypeError('upsertWorker requires a state object');
144
+ }
145
+ const state = sanitizeState(rawState);
146
+ if (!state.sdkSessionId) {
147
+ throw new Error('upsertWorker requires sdkSessionId');
148
+ }
149
+ const existing = bySession.get(state.sdkSessionId);
150
+ const next = sanitizeState({
151
+ ...(existing || {}),
152
+ ...state,
153
+ sdkSessionId: state.sdkSessionId,
154
+ createdAt: existing?.createdAt || state.createdAt,
155
+ updatedAt: new Date().toISOString(),
156
+ });
157
+ clearIndexesForSession(existing);
158
+ bySession.set(state.sdkSessionId, next);
159
+ updateIndexesForSession(next);
160
+ return toSnapshot(next);
161
+ }
162
+
163
+ function removeWorker(sdkSessionId) {
164
+ const sessionId = normalizeSessionId(sdkSessionId);
165
+ if (!sessionId) return false;
166
+ const existing = bySession.get(sessionId);
167
+ if (!existing) return false;
168
+ clearIndexesForSession(existing);
169
+ return bySession.delete(sessionId);
170
+ }
171
+
172
+ function clearAll() {
173
+ bySession.clear();
174
+ sessionByWorkerId.clear();
175
+ sessionByConversationId.clear();
176
+ sessionByRuntimeSessionId.clear();
177
+ }
178
+
179
+ return {
180
+ normalizeSessionId,
181
+ normalizeWorkerId,
182
+ normalizeConversationId,
183
+ normalizeRuntimeSessionId,
184
+ getWorker,
185
+ getWorkerByWorkerId,
186
+ getWorkerByConversationId,
187
+ getWorkerByRuntimeSessionId,
188
+ listWorkers,
189
+ upsertWorker,
190
+ removeWorker,
191
+ clearAll,
192
+ };
193
+ }
194
+
@@ -0,0 +1,141 @@
1
+ // ---------------------------------------------------------------------------
2
+ // Deterministic session-worker stop
3
+ //
4
+ // Kills every process belonging to a session and then *verifies* it is gone,
5
+ // escalating once if the graceful pass did not take. Callers must be able to
6
+ // distinguish "the old CLI is definitely dead" from "we asked it to die" —
7
+ // relaunching into a still-live process is how a session ends up with two CLIs,
8
+ // or with one CLI silently still in the old working directory.
9
+ //
10
+ // Never throws: the caller decides what a timeout means.
11
+ // ---------------------------------------------------------------------------
12
+
13
+ const DEFAULT_GRACEFUL_TIMEOUT_MS = 3_000;
14
+ const DEFAULT_ESCALATION_TIMEOUT_MS = 2_000;
15
+ const DEFAULT_POLL_INTERVAL_MS = 50;
16
+
17
+ function toPidList(rows, extraPid) {
18
+ const fromRows = (Array.isArray(rows) ? rows : []).map((row) => Number(row?.processId));
19
+ return [...new Set([...fromRows, Number(extraPid)])]
20
+ .filter((pid) => Number.isInteger(pid) && pid > 0);
21
+ }
22
+
23
+ function enumeratePids({ sdkSessionId, worker, processInspector, platform }) {
24
+ const rows = platform === 'win32'
25
+ ? (
26
+ processInspector?.findWindowsProcessTreeForSession?.(sdkSessionId)
27
+ || processInspector?.findWindowsProcessesForSession?.(sdkSessionId)
28
+ || processInspector?.findProcessesForSession?.(sdkSessionId)
29
+ || []
30
+ )
31
+ : (processInspector?.findProcessesForSession?.(sdkSessionId) || []);
32
+ return toPidList(rows, worker?.pid);
33
+ }
34
+
35
+ export async function stopSessionWorkerProcesses({
36
+ sdkSessionId,
37
+ worker = null,
38
+ processInspector = null,
39
+ platform = process.platform,
40
+ killImpl = (pid, signal) => process.kill(pid, signal),
41
+ isPidAliveImpl = () => false,
42
+ killTmuxSessionImpl = null,
43
+ sleepImpl = (ms) => new Promise((resolve) => setTimeout(resolve, ms)),
44
+ nowMs = Date.now,
45
+ gracefulTimeoutMs = DEFAULT_GRACEFUL_TIMEOUT_MS,
46
+ escalationTimeoutMs = DEFAULT_ESCALATION_TIMEOUT_MS,
47
+ pollIntervalMs = DEFAULT_POLL_INTERVAL_MS,
48
+ } = {}) {
49
+ const sid = String(sdkSessionId || '').trim();
50
+ const startedAt = nowMs();
51
+ const done = (extra) => ({
52
+ ok: true,
53
+ pids: [],
54
+ remainingPids: [],
55
+ escalated: false,
56
+ timedOut: false,
57
+ durationMs: nowMs() - startedAt,
58
+ error: null,
59
+ ...extra,
60
+ });
61
+
62
+ if (!sid) return done({ ok: false, error: 'Missing session id' });
63
+
64
+ const pids = enumeratePids({ sdkSessionId: sid, worker, processInspector, platform });
65
+ if (!pids.length) return done({ pids: [] });
66
+
67
+ const survivors = (list) => list.filter((pid) => isPidAliveImpl(pid));
68
+
69
+ const waitFor = async (list, timeoutMs) => {
70
+ const deadline = nowMs() + Math.max(0, Number(timeoutMs) || 0);
71
+ let remaining = survivors(list);
72
+ while (remaining.length && nowMs() < deadline) {
73
+ await sleepImpl(pollIntervalMs);
74
+ remaining = survivors(list);
75
+ }
76
+ return remaining;
77
+ };
78
+
79
+ try {
80
+ if (platform === 'win32') {
81
+ // stopWindowsPids already walks the descendant tree leaf-first and uses
82
+ // Stop-Process -Force, so there is no gentler first pass to attempt.
83
+ processInspector?.stopWindowsPids?.(pids);
84
+ } else {
85
+ for (const pid of pids) {
86
+ try {
87
+ killImpl(pid, 'SIGTERM');
88
+ } catch (error) {
89
+ if (String(error?.code || '') !== 'ESRCH') throw error;
90
+ }
91
+ }
92
+ // The tmux session can outlive its child after SIGTERM.
93
+ killTmuxSessionImpl?.(sid);
94
+ }
95
+ } catch (error) {
96
+ return done({ ok: false, pids, remainingPids: survivors(pids), error: error?.message || 'Failed to stop the CLI' });
97
+ }
98
+
99
+ let remainingPids = await waitFor(pids, gracefulTimeoutMs);
100
+ if (!remainingPids.length) return done({ pids });
101
+
102
+ // Escalate once.
103
+ let escalationTargets = remainingPids;
104
+ try {
105
+ if (platform === 'win32') {
106
+ // Re-enumerate: children spawned after the first snapshot are invisible to it.
107
+ const rediscovered = enumeratePids({ sdkSessionId: sid, worker, processInspector, platform });
108
+ escalationTargets = [...new Set([...remainingPids, ...rediscovered.filter((pid) => isPidAliveImpl(pid))])];
109
+ processInspector?.stopWindowsPids?.(escalationTargets);
110
+ } else {
111
+ for (const pid of escalationTargets) {
112
+ try {
113
+ killImpl(pid, 'SIGKILL');
114
+ } catch (error) {
115
+ if (String(error?.code || '') !== 'ESRCH') throw error;
116
+ }
117
+ }
118
+ }
119
+ } catch (error) {
120
+ return done({
121
+ ok: false,
122
+ pids,
123
+ remainingPids: survivors(escalationTargets),
124
+ escalated: true,
125
+ error: error?.message || 'Failed to force-stop the CLI',
126
+ });
127
+ }
128
+
129
+ remainingPids = await waitFor(escalationTargets, escalationTimeoutMs);
130
+ if (remainingPids.length) {
131
+ return done({
132
+ ok: false,
133
+ pids,
134
+ remainingPids,
135
+ escalated: true,
136
+ timedOut: true,
137
+ error: `worker-stop-timeout:${remainingPids.join(',')}`,
138
+ });
139
+ }
140
+ return done({ pids, escalated: true });
141
+ }