@clinebot/core 0.0.36 → 0.0.37

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 (228) hide show
  1. package/dist/ClineCore.d.ts +312 -3
  2. package/dist/ClineCore.d.ts.map +1 -1
  3. package/dist/account/cline-account-service.d.ts.map +1 -1
  4. package/dist/cron/cron-event-ingress.d.ts +38 -0
  5. package/dist/cron/cron-event-ingress.d.ts.map +1 -0
  6. package/dist/cron/cron-materializer.d.ts +36 -0
  7. package/dist/cron/cron-materializer.d.ts.map +1 -0
  8. package/dist/cron/cron-reconciler.d.ts +62 -0
  9. package/dist/cron/cron-reconciler.d.ts.map +1 -0
  10. package/dist/cron/cron-report-writer.d.ts +41 -0
  11. package/dist/cron/cron-report-writer.d.ts.map +1 -0
  12. package/dist/cron/cron-runner.d.ts +43 -0
  13. package/dist/cron/cron-runner.d.ts.map +1 -0
  14. package/dist/cron/cron-schema.d.ts +3 -0
  15. package/dist/cron/cron-schema.d.ts.map +1 -0
  16. package/dist/cron/cron-service.d.ts +57 -0
  17. package/dist/cron/cron-service.d.ts.map +1 -0
  18. package/dist/cron/cron-spec-parser.d.ts +27 -0
  19. package/dist/cron/cron-spec-parser.d.ts.map +1 -0
  20. package/dist/cron/cron-watcher.d.ts +23 -0
  21. package/dist/cron/cron-watcher.d.ts.map +1 -0
  22. package/dist/cron/scheduler.d.ts +3 -1
  23. package/dist/cron/scheduler.d.ts.map +1 -1
  24. package/dist/cron/sqlite-cron-store.d.ts +230 -0
  25. package/dist/cron/sqlite-cron-store.d.ts.map +1 -0
  26. package/dist/extensions/plugin/plugin-config-loader.d.ts +7 -1
  27. package/dist/extensions/plugin/plugin-config-loader.d.ts.map +1 -1
  28. package/dist/extensions/plugin/plugin-loader.d.ts +10 -6
  29. package/dist/extensions/plugin/plugin-loader.d.ts.map +1 -1
  30. package/dist/extensions/plugin/plugin-sandbox.d.ts +7 -1
  31. package/dist/extensions/plugin/plugin-sandbox.d.ts.map +1 -1
  32. package/dist/extensions/plugin-sandbox-bootstrap.js +236 -275
  33. package/dist/extensions/tools/constants.d.ts +1 -0
  34. package/dist/extensions/tools/constants.d.ts.map +1 -1
  35. package/dist/extensions/tools/definitions.d.ts +2 -3
  36. package/dist/extensions/tools/definitions.d.ts.map +1 -1
  37. package/dist/extensions/tools/executors/editor.d.ts.map +1 -1
  38. package/dist/extensions/tools/helpers.d.ts +1 -0
  39. package/dist/extensions/tools/helpers.d.ts.map +1 -1
  40. package/dist/extensions/tools/index.d.ts +1 -2
  41. package/dist/extensions/tools/index.d.ts.map +1 -1
  42. package/dist/extensions/tools/presets.d.ts +1 -1
  43. package/dist/extensions/tools/schemas.d.ts +25 -3
  44. package/dist/extensions/tools/schemas.d.ts.map +1 -1
  45. package/dist/extensions/tools/team/delegated-agent.d.ts +2 -2
  46. package/dist/extensions/tools/team/delegated-agent.d.ts.map +1 -1
  47. package/dist/extensions/tools/team/multi-agent.d.ts +7 -3
  48. package/dist/extensions/tools/team/multi-agent.d.ts.map +1 -1
  49. package/dist/extensions/tools/team/team-tools.d.ts.map +1 -1
  50. package/dist/extensions/tools/types.d.ts +0 -5
  51. package/dist/extensions/tools/types.d.ts.map +1 -1
  52. package/dist/hooks/hook-bridge.d.ts +118 -0
  53. package/dist/hooks/hook-bridge.d.ts.map +1 -0
  54. package/dist/hooks/hook-file-hooks.d.ts +2 -1
  55. package/dist/hooks/hook-file-hooks.d.ts.map +1 -1
  56. package/dist/hooks/hook-registry.d.ts +16 -0
  57. package/dist/hooks/hook-registry.d.ts.map +1 -0
  58. package/dist/hub/browser-websocket.d.ts.map +1 -1
  59. package/dist/hub/client.d.ts +7 -1
  60. package/dist/hub/client.d.ts.map +1 -1
  61. package/dist/hub/daemon-entry.js +721 -461
  62. package/dist/hub/daemon.d.ts.map +1 -1
  63. package/dist/hub/defaults.d.ts +8 -4
  64. package/dist/hub/defaults.d.ts.map +1 -1
  65. package/dist/hub/index.js +665 -415
  66. package/dist/hub/runtime-handlers.d.ts.map +1 -1
  67. package/dist/hub/server.d.ts +18 -0
  68. package/dist/hub/server.d.ts.map +1 -1
  69. package/dist/hub/session-client.d.ts +3 -0
  70. package/dist/hub/session-client.d.ts.map +1 -1
  71. package/dist/hub/start-shared-server.d.ts.map +1 -1
  72. package/dist/hub/ui-client.d.ts +1 -0
  73. package/dist/hub/ui-client.d.ts.map +1 -1
  74. package/dist/index.d.ts +9 -7
  75. package/dist/index.d.ts.map +1 -1
  76. package/dist/index.js +756 -467
  77. package/dist/llms/cline-recommended-models.d.ts +20 -0
  78. package/dist/llms/cline-recommended-models.d.ts.map +1 -0
  79. package/dist/llms/handler-factory.d.ts +16 -0
  80. package/dist/llms/handler-factory.d.ts.map +1 -0
  81. package/dist/llms/provider-defaults.d.ts.map +1 -1
  82. package/dist/llms/provider-settings.d.ts +45 -2
  83. package/dist/llms/provider-settings.d.ts.map +1 -1
  84. package/dist/llms/runtime-registry.d.ts.map +1 -1
  85. package/dist/runtime/agent-config-adapter.d.ts +148 -0
  86. package/dist/runtime/agent-config-adapter.d.ts.map +1 -0
  87. package/dist/runtime/agent-runtime-config-builder.d.ts +96 -0
  88. package/dist/runtime/agent-runtime-config-builder.d.ts.map +1 -0
  89. package/dist/runtime/history.d.ts +6 -0
  90. package/dist/runtime/history.d.ts.map +1 -1
  91. package/dist/runtime/host.d.ts.map +1 -1
  92. package/dist/runtime/loop-detection.d.ts +59 -0
  93. package/dist/runtime/loop-detection.d.ts.map +1 -0
  94. package/dist/runtime/mistake-tracker.d.ts +69 -0
  95. package/dist/runtime/mistake-tracker.d.ts.map +1 -0
  96. package/dist/runtime/runtime-builder.d.ts.map +1 -1
  97. package/dist/runtime/runtime-event-adapter.d.ts +102 -0
  98. package/dist/runtime/runtime-event-adapter.d.ts.map +1 -0
  99. package/dist/runtime/runtime-host.d.ts +28 -3
  100. package/dist/runtime/runtime-host.d.ts.map +1 -1
  101. package/dist/runtime/session-runtime-orchestrator.d.ts +261 -0
  102. package/dist/runtime/session-runtime-orchestrator.d.ts.map +1 -0
  103. package/dist/runtime/session-runtime.d.ts +16 -3
  104. package/dist/runtime/session-runtime.d.ts.map +1 -1
  105. package/dist/runtime/user-input-builder.d.ts +24 -0
  106. package/dist/runtime/user-input-builder.d.ts.map +1 -0
  107. package/dist/services/index.js +28 -0
  108. package/dist/services/local-runtime-bootstrap.d.ts.map +1 -1
  109. package/dist/services/plugin-tools.d.ts.map +1 -1
  110. package/dist/services/providers/local-provider-registry.d.ts +197 -21
  111. package/dist/services/providers/local-provider-registry.d.ts.map +1 -1
  112. package/dist/services/providers/local-provider-service.d.ts +3 -1
  113. package/dist/services/providers/local-provider-service.d.ts.map +1 -1
  114. package/dist/services/session-data.d.ts.map +1 -1
  115. package/dist/services/session-telemetry.d.ts +7 -2
  116. package/dist/services/session-telemetry.d.ts.map +1 -1
  117. package/dist/services/storage/file-team-store.d.ts.map +1 -1
  118. package/dist/services/storage/provider-settings-legacy-migration.d.ts.map +1 -1
  119. package/dist/services/storage/provider-settings-manager.d.ts +1 -0
  120. package/dist/services/storage/provider-settings-manager.d.ts.map +1 -1
  121. package/dist/services/storage/sqlite-team-store.d.ts.map +1 -1
  122. package/dist/session/conversation-store.d.ts +30 -0
  123. package/dist/session/conversation-store.d.ts.map +1 -0
  124. package/dist/session/message-builder.d.ts +65 -0
  125. package/dist/session/message-builder.d.ts.map +1 -0
  126. package/dist/session/session-manifest.d.ts +1 -1
  127. package/dist/transports/hub.d.ts +14 -3
  128. package/dist/transports/hub.d.ts.map +1 -1
  129. package/dist/transports/local.d.ts +14 -4
  130. package/dist/transports/local.d.ts.map +1 -1
  131. package/dist/transports/remote.d.ts.map +1 -1
  132. package/dist/types/chat-schema.d.ts +5 -5
  133. package/dist/types/config.d.ts +9 -0
  134. package/dist/types/config.d.ts.map +1 -1
  135. package/dist/types/events.d.ts +7 -6
  136. package/dist/types/events.d.ts.map +1 -1
  137. package/dist/types/provider-settings.d.ts +2 -2
  138. package/dist/types/provider-settings.d.ts.map +1 -1
  139. package/dist/types/session.d.ts +5 -2
  140. package/dist/types/session.d.ts.map +1 -1
  141. package/dist/types.d.ts +4 -4
  142. package/dist/types.d.ts.map +1 -1
  143. package/package.json +4 -4
  144. package/src/ClineCore.ts +691 -6
  145. package/src/account/cline-account-service.ts +44 -6
  146. package/src/cron/cron-event-ingress.ts +357 -0
  147. package/src/cron/cron-materializer.ts +97 -0
  148. package/src/cron/cron-reconciler.ts +241 -0
  149. package/src/cron/cron-report-writer.ts +153 -0
  150. package/src/cron/cron-runner.ts +495 -0
  151. package/src/cron/cron-schema.ts +127 -0
  152. package/src/cron/cron-service.ts +163 -0
  153. package/src/cron/cron-spec-parser.ts +489 -0
  154. package/src/cron/cron-watcher.ts +102 -0
  155. package/src/cron/index.ts +10 -0
  156. package/src/cron/scheduler.ts +141 -6
  157. package/src/cron/sqlite-cron-store.ts +1286 -0
  158. package/src/extensions/plugin/plugin-config-loader.ts +21 -1
  159. package/src/extensions/plugin/plugin-loader.ts +25 -9
  160. package/src/extensions/plugin/plugin-sandbox-bootstrap.ts +151 -1
  161. package/src/extensions/plugin/plugin-sandbox.ts +131 -7
  162. package/src/extensions/tools/constants.ts +2 -0
  163. package/src/extensions/tools/definitions.ts +31 -22
  164. package/src/extensions/tools/executors/editor.ts +4 -3
  165. package/src/extensions/tools/helpers.ts +24 -0
  166. package/src/extensions/tools/index.ts +1 -2
  167. package/src/extensions/tools/presets.ts +1 -1
  168. package/src/extensions/tools/schemas.ts +13 -18
  169. package/src/extensions/tools/team/delegated-agent.ts +8 -3
  170. package/src/extensions/tools/team/multi-agent.ts +135 -19
  171. package/src/extensions/tools/team/team-tools.ts +151 -91
  172. package/src/extensions/tools/types.ts +0 -6
  173. package/src/hooks/hook-bridge.ts +489 -0
  174. package/src/hooks/hook-file-hooks.ts +58 -3
  175. package/src/hooks/hook-registry.ts +257 -0
  176. package/src/hub/browser-websocket.ts +26 -4
  177. package/src/hub/client.ts +72 -13
  178. package/src/hub/daemon-entry.ts +35 -0
  179. package/src/hub/daemon.ts +117 -14
  180. package/src/hub/defaults.ts +39 -12
  181. package/src/hub/runtime-handlers.ts +4 -3
  182. package/src/hub/server.ts +506 -77
  183. package/src/hub/session-client.ts +43 -1
  184. package/src/hub/start-shared-server.ts +3 -0
  185. package/src/hub/ui-client.ts +4 -0
  186. package/src/index.ts +46 -1
  187. package/src/llms/cline-recommended-models.ts +167 -0
  188. package/src/llms/handler-factory.ts +56 -0
  189. package/src/llms/provider-defaults.ts +17 -1
  190. package/src/llms/provider-settings.ts +48 -1
  191. package/src/llms/runtime-registry.ts +1 -0
  192. package/src/runtime/agent-config-adapter.ts +636 -0
  193. package/src/runtime/agent-runtime-config-builder.ts +205 -0
  194. package/src/runtime/error-feedback.ts +142 -0
  195. package/src/runtime/history.ts +137 -0
  196. package/src/runtime/host.ts +22 -0
  197. package/src/runtime/loop-detection.ts +162 -0
  198. package/src/runtime/mistake-tracker.ts +221 -0
  199. package/src/runtime/runtime-builder.ts +61 -5
  200. package/src/runtime/runtime-event-adapter.ts +412 -0
  201. package/src/runtime/runtime-host.ts +45 -1
  202. package/src/runtime/session-runtime-orchestrator.ts +1253 -0
  203. package/src/runtime/session-runtime.ts +16 -2
  204. package/src/runtime/user-input-builder.ts +167 -0
  205. package/src/services/local-runtime-bootstrap.ts +128 -22
  206. package/src/services/plugin-tools.ts +1 -0
  207. package/src/services/providers/local-provider-registry.ts +273 -57
  208. package/src/services/providers/local-provider-service.ts +67 -7
  209. package/src/services/session-data.ts +16 -14
  210. package/src/services/session-telemetry.ts +6 -15
  211. package/src/services/storage/file-team-store.ts +1 -5
  212. package/src/services/storage/provider-settings-legacy-migration.ts +8 -47
  213. package/src/services/storage/provider-settings-manager.ts +16 -1
  214. package/src/services/storage/sqlite-team-store.ts +1 -5
  215. package/src/session/conversation-store.ts +77 -0
  216. package/src/session/message-builder.ts +941 -0
  217. package/src/transports/hub.ts +458 -33
  218. package/src/transports/local.ts +296 -65
  219. package/src/transports/remote.ts +1 -0
  220. package/src/types/config.ts +9 -0
  221. package/src/types/events.ts +8 -6
  222. package/src/types/index.ts +3 -0
  223. package/src/types/provider-settings.ts +8 -1
  224. package/src/types/session.ts +5 -2
  225. package/src/types.ts +15 -1
  226. package/dist/cron/index.d.ts +0 -6
  227. package/dist/cron/index.d.ts.map +0 -1
  228. package/dist/services/telemetry/index.js +0 -28
package/src/hub/server.ts CHANGED
@@ -9,6 +9,7 @@ import type {
9
9
  SessionRecord as HubSessionRecord,
10
10
  HubToolExecutorName,
11
11
  JsonValue,
12
+ RuntimeConfigExtensionKind,
12
13
  SessionParticipant,
13
14
  TeamProgressProjectionEvent,
14
15
  ToolApprovalRequest,
@@ -16,6 +17,7 @@ import type {
16
17
  } from "@clinebot/shared";
17
18
  import { createSessionId } from "@clinebot/shared";
18
19
  import { WebSocketServer } from "ws";
20
+ import { CronService, type CronServiceOptions } from "../cron/cron-service";
19
21
  import { HubScheduleCommandService } from "../cron/schedule-command-service";
20
22
  import {
21
23
  type HubScheduleRuntimeHandlers,
@@ -32,10 +34,11 @@ import { SqliteSessionStore } from "../services/storage/sqlite-session-store";
32
34
  import { CoreSessionService } from "../session/session-service";
33
35
  import { LocalRuntimeHost } from "../transports/local";
34
36
  import { readPersistedMessagesFile } from "../transports/runtime-host-support";
35
- import type { CoreSessionEvent } from "../types/events";
37
+ import type { CoreSessionEvent, SessionPendingPrompt } from "../types/events";
36
38
  import type { SessionRecord as LocalSessionRecord } from "../types/sessions";
37
39
  import { BrowserWebSocketHubAdapter } from "./browser-websocket";
38
40
  import { verifyHubConnection } from "./client";
41
+ import { resolveDefaultHubPort } from "./defaults";
39
42
  import {
40
43
  clearHubDiscovery,
41
44
  createHubServerUrl,
@@ -60,8 +63,15 @@ type NodeWebSocketLike = {
60
63
  once(event: "close", listener: () => void): void;
61
64
  };
62
65
 
66
+ type NodeUpgradeSocketLike = {
67
+ destroy(error?: Error): void;
68
+ write(chunk: string): boolean;
69
+ end(): void;
70
+ };
71
+
63
72
  type HubSessionState = {
64
73
  createdByClientId: string;
74
+ interactive: boolean;
65
75
  participants: Map<string, SessionParticipant>;
66
76
  };
67
77
 
@@ -104,6 +114,37 @@ function wrapWsSocket(socket: NodeWebSocketLike) {
104
114
  };
105
115
  }
106
116
 
117
+ const RUNTIME_CONFIG_EXTENSION_KINDS = new Set<RuntimeConfigExtensionKind>([
118
+ "rules",
119
+ "skills",
120
+ "plugins",
121
+ ]);
122
+
123
+ function parseRuntimeConfigExtensions(
124
+ value: unknown,
125
+ ): RuntimeConfigExtensionKind[] | undefined {
126
+ if (!Array.isArray(value)) {
127
+ return undefined;
128
+ }
129
+ const extensions = value
130
+ .map((item) => (typeof item === "string" ? item.trim() : ""))
131
+ .filter((item): item is RuntimeConfigExtensionKind =>
132
+ RUNTIME_CONFIG_EXTENSION_KINDS.has(item as RuntimeConfigExtensionKind),
133
+ );
134
+ return [...new Set(extensions)];
135
+ }
136
+
137
+ function rejectUpgradeSocket(socket: NodeUpgradeSocketLike): void {
138
+ try {
139
+ socket.write(
140
+ "HTTP/1.1 400 Bad Request\r\nConnection: close\r\nContent-Length: 0\r\n\r\n",
141
+ );
142
+ socket.end();
143
+ } catch {
144
+ socket.destroy();
145
+ }
146
+ }
147
+
107
148
  function formatHubUptime(ms: number): string {
108
149
  const totalSeconds = Math.max(0, Math.floor(ms / 1000));
109
150
  const days = Math.floor(totalSeconds / 86_400);
@@ -157,6 +198,10 @@ function cloneSessionMetadata(
157
198
  if (session.messagesPath?.trim())
158
199
  metadata.messagesPath = session.messagesPath;
159
200
  if (session.prompt?.trim()) metadata.prompt = session.prompt;
201
+ if (session.provider?.trim()) metadata.provider = session.provider;
202
+ if (session.model?.trim()) metadata.model = session.model;
203
+ if (session.source?.trim()) metadata.source = session.source;
204
+ if (typeof session.pid === "number") metadata.pid = session.pid;
160
205
  return Object.keys(metadata).length > 0 ? metadata : undefined;
161
206
  }
162
207
 
@@ -354,6 +399,14 @@ function formatHubStartupError(
354
399
  return wrapped;
355
400
  }
356
401
 
402
+ function isAddressInUseError(error: unknown): boolean {
403
+ return (
404
+ error instanceof Error &&
405
+ "code" in error &&
406
+ (error as Error & { code?: string }).code === "EADDRINUSE"
407
+ );
408
+ }
409
+
357
410
  function serializeToolContext(context: ToolContext): Record<string, unknown> {
358
411
  return {
359
412
  agentId: context.agentId,
@@ -483,8 +536,13 @@ export class HubServerTransport implements NativeHubTransport {
483
536
  }) => void;
484
537
  }
485
538
  >();
539
+ private readonly suppressNextTerminalEventBySession = new Map<
540
+ string,
541
+ string
542
+ >();
486
543
  private readonly schedules: HubScheduleService;
487
544
  private readonly scheduleCommands: HubScheduleCommandService;
545
+ private readonly cronService?: CronService;
488
546
  private readonly sessionHost: RuntimeHost;
489
547
  private readonly hubId = createSessionId("hub_");
490
548
  private readonly startedAtMs = Date.now();
@@ -522,27 +580,45 @@ export class HubServerTransport implements NativeHubTransport {
522
580
  },
523
581
  });
524
582
  this.scheduleCommands = new HubScheduleCommandService(this.schedules);
583
+ if (options.cronOptions) {
584
+ this.cronService = new CronService({
585
+ runtimeHandlers: options.runtimeHandlers,
586
+ ...options.cronOptions,
587
+ });
588
+ }
525
589
  this.sessionHost.subscribe((event) => {
526
- void this.handleSessionEvent(event);
590
+ void this.handleSessionEvent(event).catch((error) => {
591
+ logHubBoundaryError("session event handling failed", error);
592
+ });
527
593
  });
528
594
  }
529
595
 
596
+ getCronService(): CronService | undefined {
597
+ return this.cronService;
598
+ }
599
+
530
600
  getHubId(): string {
531
601
  return this.hubId;
532
602
  }
533
603
 
534
604
  async start(): Promise<void> {
535
605
  await this.schedules.start();
606
+ if (this.cronService) {
607
+ try {
608
+ await this.cronService.start();
609
+ } catch (err) {
610
+ console.error("[hub] cron service start failed", err);
611
+ }
612
+ }
536
613
  }
537
614
 
538
615
  async stop(): Promise<void> {
539
- for (const approval of this.pendingApprovals.values()) {
540
- approval.resolve({
616
+ for (const approvalId of this.pendingApprovals.keys()) {
617
+ this.resolvePendingApproval(approvalId, {
541
618
  approved: false,
542
619
  reason: "Hub shutting down before approval was resolved.",
543
620
  });
544
621
  }
545
- this.pendingApprovals.clear();
546
622
  for (const pending of this.pendingCapabilityRequests.values()) {
547
623
  pending.resolve({
548
624
  ok: false,
@@ -552,6 +628,13 @@ export class HubServerTransport implements NativeHubTransport {
552
628
  this.pendingCapabilityRequests.clear();
553
629
  await this.sessionHost.dispose("hub_server_stop");
554
630
  await this.schedules.dispose();
631
+ if (this.cronService) {
632
+ try {
633
+ await this.cronService.dispose();
634
+ } catch (err) {
635
+ console.error("[hub] cron service stop failed", err);
636
+ }
637
+ }
555
638
  }
556
639
 
557
640
  async handleCommand(envelope: HubCommandEnvelope): Promise<HubReplyEnvelope> {
@@ -581,10 +664,18 @@ export class HubServerTransport implements NativeHubTransport {
581
664
  return await this.handleSessionDetach(envelope);
582
665
  case "session.get":
583
666
  return await this.handleSessionGet(envelope);
667
+ case "session.messages":
668
+ return await this.handleSessionMessages(envelope);
584
669
  case "session.list":
585
670
  return await this.handleSessionList(envelope);
586
671
  case "session.update":
587
672
  return await this.handleSessionUpdate(envelope);
673
+ case "session.pending_prompts":
674
+ return await this.handleSessionPendingPrompts(envelope);
675
+ case "session.update_pending_prompt":
676
+ return await this.handleSessionUpdatePendingPrompt(envelope);
677
+ case "session.remove_pending_prompt":
678
+ return await this.handleSessionRemovePendingPrompt(envelope);
588
679
  case "session.delete":
589
680
  return await this.handleSessionDelete(envelope);
590
681
  case "session.hook":
@@ -748,9 +839,13 @@ export class HubServerTransport implements NativeHubTransport {
748
839
  sessionId: string,
749
840
  clientId: string,
750
841
  role: SessionParticipant["role"],
842
+ options: { interactive?: boolean } = {},
751
843
  ): HubSessionState {
752
844
  const existing = this.sessionState.get(sessionId);
753
845
  if (existing) {
846
+ if (options.interactive !== undefined) {
847
+ existing.interactive = options.interactive;
848
+ }
754
849
  if (!existing.participants.has(clientId)) {
755
850
  existing.participants.set(clientId, {
756
851
  clientId,
@@ -762,6 +857,7 @@ export class HubServerTransport implements NativeHubTransport {
762
857
  }
763
858
  const state: HubSessionState = {
764
859
  createdByClientId: clientId,
860
+ interactive: options.interactive ?? true,
765
861
  participants: new Map([
766
862
  [
767
863
  clientId,
@@ -877,6 +973,9 @@ export class HubServerTransport implements NativeHubTransport {
877
973
  const advertisedToolExecutors = Array.isArray(runtimeOptions.toolExecutors)
878
974
  ? runtimeOptions.toolExecutors.filter(isHubToolExecutorName)
879
975
  : [];
976
+ const configExtensions = parseRuntimeConfigExtensions(
977
+ runtimeOptions.configExtensions,
978
+ );
880
979
  const started = await this.sessionHost.start({
881
980
  source: typeof metadata.source === "string" ? metadata.source : undefined,
882
981
  interactive: metadata.interactive !== false,
@@ -892,6 +991,7 @@ export class HubServerTransport implements NativeHubTransport {
892
991
  loadLatestOnInit: true,
893
992
  loadPrivateOnAuth: true,
894
993
  },
994
+ configExtensions,
895
995
  defaultToolExecutors: createCapabilityBackedToolExecutors(
896
996
  clientId,
897
997
  advertisedToolExecutors,
@@ -985,7 +1085,9 @@ export class HubServerTransport implements NativeHubTransport {
985
1085
  ? { "*": { autoApprove: true } }
986
1086
  : undefined,
987
1087
  });
988
- this.ensureSessionState(started.sessionId, clientId, "creator");
1088
+ this.ensureSessionState(started.sessionId, clientId, "creator", {
1089
+ interactive: metadata.interactive !== false,
1090
+ });
989
1091
  const session = await this.readHubSessionRecord(started.sessionId);
990
1092
  if (session) {
991
1093
  this.publish(
@@ -1106,6 +1208,45 @@ export class HubServerTransport implements NativeHubTransport {
1106
1208
  };
1107
1209
  }
1108
1210
 
1211
+ private async handleSessionMessages(
1212
+ envelope: HubCommandEnvelope,
1213
+ ): Promise<HubReplyEnvelope> {
1214
+ const sessionId =
1215
+ typeof envelope.payload?.sessionId === "string"
1216
+ ? envelope.payload.sessionId.trim()
1217
+ : envelope.sessionId?.trim() || "";
1218
+ if (!sessionId) {
1219
+ return {
1220
+ version: envelope.version,
1221
+ requestId: envelope.requestId,
1222
+ ok: false,
1223
+ error: {
1224
+ code: "invalid_session_id",
1225
+ message: "session.messages requires a session id",
1226
+ },
1227
+ };
1228
+ }
1229
+ const session = await this.readHubSessionRecord(sessionId);
1230
+ if (!session) {
1231
+ return {
1232
+ version: envelope.version,
1233
+ requestId: envelope.requestId,
1234
+ ok: false,
1235
+ error: {
1236
+ code: "session_not_found",
1237
+ message: `Unknown session: ${sessionId}`,
1238
+ },
1239
+ };
1240
+ }
1241
+ const messages = await this.sessionHost.readMessages(sessionId);
1242
+ return {
1243
+ version: envelope.version,
1244
+ requestId: envelope.requestId,
1245
+ ok: true,
1246
+ payload: { sessionId, messages },
1247
+ };
1248
+ }
1249
+
1109
1250
  private async handleSessionList(
1110
1251
  envelope: HubCommandEnvelope,
1111
1252
  ): Promise<HubReplyEnvelope> {
@@ -1150,6 +1291,81 @@ export class HubServerTransport implements NativeHubTransport {
1150
1291
  };
1151
1292
  }
1152
1293
 
1294
+ private async handleSessionPendingPrompts(
1295
+ envelope: HubCommandEnvelope,
1296
+ ): Promise<HubReplyEnvelope> {
1297
+ const sessionId =
1298
+ typeof envelope.payload?.sessionId === "string"
1299
+ ? envelope.payload.sessionId.trim()
1300
+ : envelope.sessionId?.trim() || "";
1301
+ const prompts = await this.sessionHost.pendingPrompts("list", {
1302
+ sessionId,
1303
+ });
1304
+ return {
1305
+ version: envelope.version,
1306
+ requestId: envelope.requestId,
1307
+ ok: true,
1308
+ payload: { sessionId, prompts },
1309
+ };
1310
+ }
1311
+
1312
+ private async handleSessionUpdatePendingPrompt(
1313
+ envelope: HubCommandEnvelope,
1314
+ ): Promise<HubReplyEnvelope> {
1315
+ const sessionId =
1316
+ typeof envelope.payload?.sessionId === "string"
1317
+ ? envelope.payload.sessionId.trim()
1318
+ : envelope.sessionId?.trim() || "";
1319
+ const promptId =
1320
+ typeof envelope.payload?.promptId === "string"
1321
+ ? envelope.payload.promptId.trim()
1322
+ : "";
1323
+ const prompt =
1324
+ typeof envelope.payload?.prompt === "string"
1325
+ ? envelope.payload.prompt
1326
+ : undefined;
1327
+ const delivery =
1328
+ envelope.payload?.delivery === "queue" ||
1329
+ envelope.payload?.delivery === "steer"
1330
+ ? envelope.payload.delivery
1331
+ : undefined;
1332
+ const result = await this.sessionHost.pendingPrompts("update", {
1333
+ sessionId,
1334
+ promptId,
1335
+ prompt,
1336
+ delivery,
1337
+ });
1338
+ return {
1339
+ version: envelope.version,
1340
+ requestId: envelope.requestId,
1341
+ ok: true,
1342
+ payload: result as unknown as Record<string, JsonValue | undefined>,
1343
+ };
1344
+ }
1345
+
1346
+ private async handleSessionRemovePendingPrompt(
1347
+ envelope: HubCommandEnvelope,
1348
+ ): Promise<HubReplyEnvelope> {
1349
+ const sessionId =
1350
+ typeof envelope.payload?.sessionId === "string"
1351
+ ? envelope.payload.sessionId.trim()
1352
+ : envelope.sessionId?.trim() || "";
1353
+ const promptId =
1354
+ typeof envelope.payload?.promptId === "string"
1355
+ ? envelope.payload.promptId.trim()
1356
+ : "";
1357
+ const result = await this.sessionHost.pendingPrompts("delete", {
1358
+ sessionId,
1359
+ promptId,
1360
+ });
1361
+ return {
1362
+ version: envelope.version,
1363
+ requestId: envelope.requestId,
1364
+ ok: true,
1365
+ payload: result as unknown as Record<string, JsonValue | undefined>,
1366
+ };
1367
+ }
1368
+
1153
1369
  private async handleSessionDelete(
1154
1370
  envelope: HubCommandEnvelope,
1155
1371
  ): Promise<HubReplyEnvelope> {
@@ -1202,6 +1418,9 @@ export class HubServerTransport implements NativeHubTransport {
1202
1418
  !Array.isArray(payload.attachments)
1203
1419
  ? (payload.attachments as Record<string, unknown>)
1204
1420
  : undefined;
1421
+ const userFiles = Array.isArray(attachments?.userFiles)
1422
+ ? attachments.userFiles.filter((filePath) => typeof filePath === "string")
1423
+ : undefined;
1205
1424
  const result = await this.sessionHost.send({
1206
1425
  sessionId,
1207
1426
  prompt,
@@ -1212,8 +1431,13 @@ export class HubServerTransport implements NativeHubTransport {
1212
1431
  userImages: Array.isArray(attachments?.userImages)
1213
1432
  ? (attachments.userImages as string[])
1214
1433
  : undefined,
1434
+ userFiles,
1215
1435
  });
1216
1436
  if (result) {
1437
+ this.suppressNextTerminalEventBySession.set(
1438
+ sessionId,
1439
+ result.finishReason,
1440
+ );
1217
1441
  this.publish(
1218
1442
  this.buildEvent(
1219
1443
  "run.completed",
@@ -1283,9 +1507,18 @@ export class HubServerTransport implements NativeHubTransport {
1283
1507
  request: ToolApprovalRequest,
1284
1508
  ): Promise<{ approved: boolean; reason?: string }> {
1285
1509
  const approvalId = createSessionId("approval_");
1510
+ const sessionId = request.sessionId;
1511
+ const state = this.sessionState.get(sessionId);
1512
+ if (state?.interactive === false) {
1513
+ return {
1514
+ approved: false,
1515
+ reason:
1516
+ "Tool approval requires an interactive session, but this session is non-interactive.",
1517
+ };
1518
+ }
1286
1519
  return await new Promise((resolve) => {
1287
1520
  this.pendingApprovals.set(approvalId, {
1288
- sessionId: request.conversationId,
1521
+ sessionId,
1289
1522
  resolve,
1290
1523
  });
1291
1524
  this.publish(
@@ -1293,16 +1526,34 @@ export class HubServerTransport implements NativeHubTransport {
1293
1526
  "approval.requested",
1294
1527
  {
1295
1528
  approvalId,
1529
+ sessionId: request.sessionId,
1530
+ agentId: request.agentId,
1531
+ conversationId: request.conversationId,
1532
+ iteration: request.iteration,
1296
1533
  toolCallId: request.toolCallId,
1297
1534
  toolName: request.toolName,
1298
1535
  inputJson: JSON.stringify(request.input ?? null),
1536
+ policy: request.policy,
1299
1537
  },
1300
- request.conversationId,
1538
+ sessionId,
1301
1539
  ),
1302
1540
  );
1303
1541
  });
1304
1542
  }
1305
1543
 
1544
+ private resolvePendingApproval(
1545
+ approvalId: string,
1546
+ result: { approved: boolean; reason?: string },
1547
+ ): { sessionId: string } | undefined {
1548
+ const pending = this.pendingApprovals.get(approvalId);
1549
+ if (!pending) {
1550
+ return undefined;
1551
+ }
1552
+ this.pendingApprovals.delete(approvalId);
1553
+ pending.resolve(result);
1554
+ return { sessionId: pending.sessionId };
1555
+ }
1556
+
1306
1557
  private async handleApprovalRespond(
1307
1558
  envelope: HubCommandEnvelope,
1308
1559
  ): Promise<HubReplyEnvelope> {
@@ -1322,25 +1573,37 @@ export class HubServerTransport implements NativeHubTransport {
1322
1573
  },
1323
1574
  };
1324
1575
  }
1325
- this.pendingApprovals.delete(approvalId);
1326
1576
  const reason =
1327
- envelope.payload?.payload &&
1328
- typeof envelope.payload.payload === "object" &&
1329
- !Array.isArray(envelope.payload.payload) &&
1330
- typeof (envelope.payload.payload as Record<string, unknown>).reason ===
1331
- "string"
1332
- ? ((envelope.payload.payload as Record<string, unknown>)
1333
- .reason as string)
1334
- : undefined;
1335
- pending.resolve({
1577
+ typeof envelope.payload?.reason === "string"
1578
+ ? envelope.payload.reason
1579
+ : envelope.payload?.payload &&
1580
+ typeof envelope.payload.payload === "object" &&
1581
+ !Array.isArray(envelope.payload.payload) &&
1582
+ typeof (envelope.payload.payload as Record<string, unknown>)
1583
+ .reason === "string"
1584
+ ? ((envelope.payload.payload as Record<string, unknown>)
1585
+ .reason as string)
1586
+ : undefined;
1587
+ const resolved = this.resolvePendingApproval(approvalId, {
1336
1588
  approved: envelope.payload?.approved === true,
1337
1589
  reason,
1338
1590
  });
1591
+ if (!resolved) {
1592
+ return {
1593
+ version: envelope.version,
1594
+ requestId: envelope.requestId,
1595
+ ok: false,
1596
+ error: {
1597
+ code: "approval_not_found",
1598
+ message: `Unknown approval: ${approvalId}`,
1599
+ },
1600
+ };
1601
+ }
1339
1602
  this.publish(
1340
1603
  this.buildEvent(
1341
1604
  "approval.resolved",
1342
1605
  { approvalId, approved: envelope.payload?.approved === true, reason },
1343
- pending.sessionId,
1606
+ resolved.sessionId,
1344
1607
  ),
1345
1608
  );
1346
1609
  return {
@@ -1473,6 +1736,30 @@ export class HubServerTransport implements NativeHubTransport {
1473
1736
  return;
1474
1737
  case "agent_event": {
1475
1738
  const { sessionId, event: agentEvent } = event.payload;
1739
+ if (agentEvent.type === "iteration_start") {
1740
+ this.publish(
1741
+ this.buildEvent(
1742
+ "iteration.started",
1743
+ { iteration: agentEvent.iteration },
1744
+ sessionId,
1745
+ ),
1746
+ );
1747
+ return;
1748
+ }
1749
+ if (agentEvent.type === "iteration_end") {
1750
+ this.publish(
1751
+ this.buildEvent(
1752
+ "iteration.finished",
1753
+ {
1754
+ iteration: agentEvent.iteration,
1755
+ hadToolCalls: agentEvent.hadToolCalls,
1756
+ toolCallCount: agentEvent.toolCallCount,
1757
+ },
1758
+ sessionId,
1759
+ ),
1760
+ );
1761
+ return;
1762
+ }
1476
1763
  if (agentEvent.type === "content_start") {
1477
1764
  if (
1478
1765
  agentEvent.contentType === "text" &&
@@ -1531,18 +1818,52 @@ export class HubServerTransport implements NativeHubTransport {
1531
1818
  return;
1532
1819
  }
1533
1820
  }
1534
- if (
1535
- agentEvent.type === "content_end" &&
1536
- agentEvent.contentType === "tool"
1537
- ) {
1821
+ if (agentEvent.type === "content_end") {
1822
+ switch (agentEvent.contentType) {
1823
+ case "text":
1824
+ this.publish(
1825
+ this.buildEvent(
1826
+ "assistant.finished",
1827
+ { text: agentEvent.text },
1828
+ sessionId,
1829
+ ),
1830
+ );
1831
+ break;
1832
+ case "reasoning":
1833
+ this.publish(
1834
+ this.buildEvent(
1835
+ "reasoning.finished",
1836
+ { reasoning: agentEvent.reasoning },
1837
+ sessionId,
1838
+ ),
1839
+ );
1840
+ break;
1841
+ case "tool":
1842
+ this.publish(
1843
+ this.buildEvent(
1844
+ "tool.finished",
1845
+ {
1846
+ toolCallId: agentEvent.toolCallId,
1847
+ toolName: agentEvent.toolName,
1848
+ output: agentEvent.output,
1849
+ error: agentEvent.error,
1850
+ },
1851
+ sessionId,
1852
+ ),
1853
+ );
1854
+ break;
1855
+ }
1856
+ return;
1857
+ }
1858
+ if (agentEvent.type === "done") {
1538
1859
  this.publish(
1539
1860
  this.buildEvent(
1540
- "tool.finished",
1861
+ "agent.done",
1541
1862
  {
1542
- toolCallId: agentEvent.toolCallId,
1543
- toolName: agentEvent.toolName,
1544
- output: agentEvent.output,
1545
- error: agentEvent.error,
1863
+ reason: agentEvent.reason,
1864
+ text: agentEvent.text,
1865
+ iterations: agentEvent.iterations,
1866
+ usage: agentEvent.usage,
1546
1867
  },
1547
1868
  sessionId,
1548
1869
  ),
@@ -1586,6 +1907,35 @@ export class HubServerTransport implements NativeHubTransport {
1586
1907
  );
1587
1908
  return;
1588
1909
  }
1910
+ case "pending_prompts": {
1911
+ this.publish(
1912
+ this.buildEvent(
1913
+ "session.pending_prompts",
1914
+ {
1915
+ sessionId: event.payload.sessionId,
1916
+ prompts: event.payload.prompts,
1917
+ },
1918
+ event.payload.sessionId,
1919
+ ),
1920
+ );
1921
+ return;
1922
+ }
1923
+ case "pending_prompt_submitted": {
1924
+ const prompt: SessionPendingPrompt = {
1925
+ id: event.payload.id,
1926
+ prompt: event.payload.prompt,
1927
+ delivery: event.payload.delivery,
1928
+ attachmentCount: event.payload.attachmentCount,
1929
+ };
1930
+ this.publish(
1931
+ this.buildEvent(
1932
+ "session.pending_prompt_submitted",
1933
+ { sessionId: event.payload.sessionId, prompt },
1934
+ event.payload.sessionId,
1935
+ ),
1936
+ );
1937
+ return;
1938
+ }
1589
1939
  case "status": {
1590
1940
  const session = await this.readHubSessionRecord(
1591
1941
  event.payload.sessionId,
@@ -1601,7 +1951,16 @@ export class HubServerTransport implements NativeHubTransport {
1601
1951
  }
1602
1952
  return;
1603
1953
  }
1604
- case "ended":
1954
+ case "ended": {
1955
+ const suppressDuplicateTerminalEvent =
1956
+ this.suppressNextTerminalEventBySession.get(
1957
+ event.payload.sessionId,
1958
+ ) === event.payload.reason;
1959
+ if (suppressDuplicateTerminalEvent) {
1960
+ this.suppressNextTerminalEventBySession.delete(
1961
+ event.payload.sessionId,
1962
+ );
1963
+ }
1605
1964
  if (event.payload.reason === "completed") {
1606
1965
  const session = await this.readHubSessionRecord(
1607
1966
  event.payload.sessionId,
@@ -1611,6 +1970,9 @@ export class HubServerTransport implements NativeHubTransport {
1611
1970
  this.buildEvent("ui.notify", notification, event.payload.sessionId),
1612
1971
  );
1613
1972
  }
1973
+ if (suppressDuplicateTerminalEvent) {
1974
+ return;
1975
+ }
1614
1976
  this.publish(
1615
1977
  this.buildEvent(
1616
1978
  event.payload.reason === "aborted"
@@ -1621,6 +1983,7 @@ export class HubServerTransport implements NativeHubTransport {
1621
1983
  ),
1622
1984
  );
1623
1985
  return;
1986
+ }
1624
1987
  default:
1625
1988
  return;
1626
1989
  }
@@ -1653,12 +2016,25 @@ export class HubServerTransport implements NativeHubTransport {
1653
2016
  if (entry.sessionId && entry.sessionId !== event.sessionId) {
1654
2017
  continue;
1655
2018
  }
1656
- entry.listener(event);
2019
+ try {
2020
+ entry.listener(event);
2021
+ } catch (error) {
2022
+ logHubBoundaryError(
2023
+ `listener threw while publishing ${event.event}`,
2024
+ error,
2025
+ );
2026
+ }
1657
2027
  }
1658
2028
  }
1659
2029
  }
1660
2030
  }
1661
2031
 
2032
+ function logHubBoundaryError(message: string, error: unknown): void {
2033
+ const details =
2034
+ error instanceof Error ? error.stack || error.message : String(error);
2035
+ console.error(`[hub] ${message}: ${details}`);
2036
+ }
2037
+
1662
2038
  export interface HubWebSocketServerOptions {
1663
2039
  host?: string;
1664
2040
  port?: number;
@@ -1667,6 +2043,14 @@ export interface HubWebSocketServerOptions {
1667
2043
  sessionHost?: RuntimeHost;
1668
2044
  runtimeHandlers: HubScheduleRuntimeHandlers;
1669
2045
  scheduleOptions?: Omit<HubScheduleServiceOptions, "runtimeHandlers">;
2046
+ /**
2047
+ * File-based cron automation options. When provided, the hub starts a
2048
+ * `CronService` that watches global `~/.cline/cron/` by default, reconciles
2049
+ * specs into `cron.db`, and executes queued runs through `runtimeHandlers`.
2050
+ * Pass `cronOptions.specs` to use a different source, including future
2051
+ * workspace-scoped specs.
2052
+ */
2053
+ cronOptions?: Omit<CronServiceOptions, "runtimeHandlers">;
1670
2054
  /**
1671
2055
  * Custom `fetch` implementation forwarded to the internally-constructed
1672
2056
  * `LocalRuntimeHost` that executes incoming `session.create` traffic.
@@ -1687,7 +2071,9 @@ export interface HubWebSocketServer {
1687
2071
  }
1688
2072
 
1689
2073
  export interface EnsureHubWebSocketServerOptions
1690
- extends HubWebSocketServerOptions {}
2074
+ extends HubWebSocketServerOptions {
2075
+ allowPortFallback?: boolean;
2076
+ }
1691
2077
 
1692
2078
  export interface EnsuredHubWebSocketServerResult {
1693
2079
  server?: HubWebSocketServer;
@@ -1703,7 +2089,7 @@ export async function startHubWebSocketServer(
1703
2089
  const owner = options.owner ?? resolveHubOwnerContext();
1704
2090
  const host = options.host ?? "127.0.0.1";
1705
2091
  const pathname = options.pathname ?? "/hub";
1706
- const requestedPort = options.port ?? 25463;
2092
+ const requestedPort = options.port ?? resolveDefaultHubPort();
1707
2093
  let port = requestedPort;
1708
2094
  let url = createHubServerUrl(host, requestedPort, pathname);
1709
2095
  const buildId = resolveHubBuildId();
@@ -1720,6 +2106,43 @@ export async function startHubWebSocketServer(
1720
2106
  pid: process.pid,
1721
2107
  startedAt,
1722
2108
  } as const;
2109
+ let closePromise: Promise<void> | undefined;
2110
+
2111
+ const closeServer = async (): Promise<void> => {
2112
+ if (closePromise) {
2113
+ return closePromise;
2114
+ }
2115
+ closePromise = (async () => {
2116
+ for (const detach of cleanup) {
2117
+ detach();
2118
+ }
2119
+ cleanup.clear();
2120
+ await new Promise<void>((resolve, reject) => {
2121
+ wss.close((error?: Error) => {
2122
+ if (error) {
2123
+ reject(error);
2124
+ return;
2125
+ }
2126
+ resolve();
2127
+ });
2128
+ });
2129
+ await new Promise<void>((resolve, reject) => {
2130
+ server.close((error) => {
2131
+ if (error) {
2132
+ reject(error);
2133
+ return;
2134
+ }
2135
+ resolve();
2136
+ });
2137
+ });
2138
+ await transport.stop();
2139
+ const current = await readHubDiscovery(owner.discoveryPath);
2140
+ if (current?.url === url) {
2141
+ await clearHubDiscovery(owner.discoveryPath);
2142
+ }
2143
+ })();
2144
+ return closePromise;
2145
+ };
1723
2146
 
1724
2147
  const server = http.createServer((req, res) => {
1725
2148
  if ((req.url ?? "/") === "/health") {
@@ -1742,6 +2165,15 @@ export async function startHubWebSocketServer(
1742
2165
  res.end(JSON.stringify(versionPayload));
1743
2166
  return;
1744
2167
  }
2168
+ if ((req.url ?? "/") === "/shutdown" && req.method === "POST") {
2169
+ res.statusCode = 202;
2170
+ res.setHeader("content-type", "application/json");
2171
+ res.end(JSON.stringify({ ok: true }));
2172
+ queueMicrotask(() => {
2173
+ void closeServer();
2174
+ });
2175
+ return;
2176
+ }
1745
2177
  res.statusCode = 404;
1746
2178
  res.end("Not found");
1747
2179
  });
@@ -1753,14 +2185,23 @@ export async function startHubWebSocketServer(
1753
2185
  socket.destroy();
1754
2186
  return;
1755
2187
  }
1756
- wss.handleUpgrade(request, socket, head, (websocket: NodeWebSocketLike) => {
1757
- const detach = adapter.attach(wrapWsSocket(websocket));
1758
- cleanup.add(detach);
1759
- websocket.once("close", () => {
1760
- detach();
1761
- cleanup.delete(detach);
1762
- });
1763
- });
2188
+ try {
2189
+ wss.handleUpgrade(
2190
+ request,
2191
+ socket,
2192
+ head,
2193
+ (websocket: NodeWebSocketLike) => {
2194
+ const detach = adapter.attach(wrapWsSocket(websocket));
2195
+ cleanup.add(detach);
2196
+ websocket.once("close", () => {
2197
+ detach();
2198
+ cleanup.delete(detach);
2199
+ });
2200
+ },
2201
+ );
2202
+ } catch {
2203
+ rejectUpgradeSocket(socket);
2204
+ }
1764
2205
  });
1765
2206
 
1766
2207
  await new Promise<void>((resolve, reject) => {
@@ -1807,35 +2248,7 @@ export async function startHubWebSocketServer(
1807
2248
  host,
1808
2249
  port,
1809
2250
  url,
1810
- close: async () => {
1811
- for (const detach of cleanup) {
1812
- detach();
1813
- }
1814
- cleanup.clear();
1815
- await new Promise<void>((resolve, reject) => {
1816
- wss.close((error?: Error) => {
1817
- if (error) {
1818
- reject(error);
1819
- return;
1820
- }
1821
- resolve();
1822
- });
1823
- });
1824
- await new Promise<void>((resolve, reject) => {
1825
- server.close((error) => {
1826
- if (error) {
1827
- reject(error);
1828
- return;
1829
- }
1830
- resolve();
1831
- });
1832
- });
1833
- await transport.stop();
1834
- const current = await readHubDiscovery(owner.discoveryPath);
1835
- if (current?.url === url) {
1836
- await clearHubDiscovery(owner.discoveryPath);
1837
- }
1838
- },
2251
+ close: closeServer,
1839
2252
  };
1840
2253
  }
1841
2254
 
@@ -1844,7 +2257,7 @@ export async function ensureHubWebSocketServer(
1844
2257
  ): Promise<EnsuredHubWebSocketServerResult> {
1845
2258
  const owner = options.owner ?? resolveHubOwnerContext();
1846
2259
  const host = options.host ?? "127.0.0.1";
1847
- const port = options.port ?? 25463;
2260
+ const port = options.port ?? resolveDefaultHubPort();
1848
2261
  const pathname = options.pathname ?? "/hub";
1849
2262
  const expectedUrl = createHubServerUrl(host, port, pathname);
1850
2263
  const sharedKey = owner.discoveryPath;
@@ -1858,7 +2271,10 @@ export async function ensureHubWebSocketServer(
1858
2271
 
1859
2272
  return await withHubStartupLock(owner.discoveryPath, async () => {
1860
2273
  const discovered = await readHubDiscovery(owner.discoveryPath);
1861
- if (discovered?.url === expectedUrl) {
2274
+ const canReuseDiscovered =
2275
+ discovered?.url &&
2276
+ (discovered.url === expectedUrl || options.allowPortFallback === true);
2277
+ if (canReuseDiscovered) {
1862
2278
  const healthy = await probeHubServer(discovered.url);
1863
2279
  if (healthy?.url && (await verifyHubConnection(healthy.url))) {
1864
2280
  return { url: healthy.url, action: "reuse" };
@@ -1875,14 +2291,27 @@ export async function ensureHubWebSocketServer(
1875
2291
  await clearHubDiscovery(owner.discoveryPath);
1876
2292
  }
1877
2293
 
1878
- const serverPromise = startHubWebSocketServer({ ...options, owner });
1879
- SHARED_SERVERS.set(sharedKey, serverPromise);
2294
+ const start = async (
2295
+ startOptions: HubWebSocketServerOptions,
2296
+ ): Promise<EnsuredHubWebSocketServerResult> => {
2297
+ const serverPromise = startHubWebSocketServer({ ...startOptions, owner });
2298
+ SHARED_SERVERS.set(sharedKey, serverPromise);
2299
+ try {
2300
+ const server = await serverPromise;
2301
+ return { server, url: server.url, action: "started" };
2302
+ } catch (error) {
2303
+ SHARED_SERVERS.delete(sharedKey);
2304
+ throw error;
2305
+ }
2306
+ };
2307
+
1880
2308
  try {
1881
- const server = await serverPromise;
1882
- return { server, url: server.url, action: "started" };
2309
+ return await start(options);
1883
2310
  } catch (error) {
1884
- SHARED_SERVERS.delete(sharedKey);
1885
- throw error;
2311
+ if (!options.allowPortFallback || !isAddressInUseError(error)) {
2312
+ throw error;
2313
+ }
2314
+ return await start({ ...options, port: 0 });
1886
2315
  }
1887
2316
  });
1888
2317
  }