byterover-cli 1.0.4 → 1.1.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 (172) hide show
  1. package/README.md +24 -11
  2. package/dist/commands/curate.js +1 -1
  3. package/dist/commands/hook-prompt-submit.d.ts +27 -0
  4. package/dist/commands/hook-prompt-submit.js +39 -0
  5. package/dist/commands/main.d.ts +13 -0
  6. package/dist/commands/main.js +53 -2
  7. package/dist/commands/query.js +1 -1
  8. package/dist/commands/status.js +8 -3
  9. package/dist/constants.d.ts +2 -2
  10. package/dist/constants.js +2 -2
  11. package/dist/core/domain/cipher/llm/registry.js +53 -2
  12. package/dist/core/domain/cipher/llm/types.d.ts +2 -0
  13. package/dist/core/domain/cipher/process/types.d.ts +7 -0
  14. package/dist/core/domain/cipher/session/session-metadata.d.ts +178 -0
  15. package/dist/core/domain/cipher/session/session-metadata.js +147 -0
  16. package/dist/core/domain/cipher/tools/constants.d.ts +1 -0
  17. package/dist/core/domain/cipher/tools/constants.js +1 -0
  18. package/dist/core/domain/entities/agent.d.ts +16 -0
  19. package/dist/core/domain/entities/agent.js +24 -0
  20. package/dist/core/domain/entities/connector-type.d.ts +9 -0
  21. package/dist/core/domain/entities/connector-type.js +8 -0
  22. package/dist/core/domain/entities/event.d.ts +1 -1
  23. package/dist/core/domain/entities/event.js +2 -0
  24. package/dist/core/domain/errors/task-error.d.ts +4 -0
  25. package/dist/core/domain/errors/task-error.js +7 -0
  26. package/dist/core/domain/knowledge/markdown-writer.d.ts +15 -18
  27. package/dist/core/domain/knowledge/markdown-writer.js +232 -34
  28. package/dist/core/domain/knowledge/relation-parser.d.ts +25 -39
  29. package/dist/core/domain/knowledge/relation-parser.js +39 -61
  30. package/dist/core/domain/transport/schemas.d.ts +77 -2
  31. package/dist/core/domain/transport/schemas.js +51 -2
  32. package/dist/core/interfaces/cipher/i-session-persistence.d.ts +133 -0
  33. package/dist/core/interfaces/cipher/i-session-persistence.js +7 -0
  34. package/dist/core/interfaces/cipher/message-types.d.ts +6 -0
  35. package/dist/core/interfaces/connectors/connector-types.d.ts +57 -0
  36. package/dist/core/interfaces/connectors/i-connector-manager.d.ts +72 -0
  37. package/dist/core/interfaces/connectors/i-connector.d.ts +54 -0
  38. package/dist/core/interfaces/connectors/i-connector.js +1 -0
  39. package/dist/core/interfaces/executor/i-curate-executor.d.ts +2 -2
  40. package/dist/core/interfaces/i-context-file-reader.d.ts +3 -0
  41. package/dist/core/interfaces/i-file-service.d.ts +7 -0
  42. package/dist/core/interfaces/usecase/i-connectors-use-case.d.ts +3 -0
  43. package/dist/core/interfaces/usecase/i-connectors-use-case.js +1 -0
  44. package/dist/core/interfaces/usecase/{i-clear-use-case.d.ts → i-reset-use-case.d.ts} +1 -1
  45. package/dist/core/interfaces/usecase/i-reset-use-case.js +1 -0
  46. package/dist/hooks/init/update-notifier.d.ts +1 -0
  47. package/dist/hooks/init/update-notifier.js +10 -1
  48. package/dist/infra/cipher/agent/agent-schemas.d.ts +6 -6
  49. package/dist/infra/cipher/agent/service-initializer.js +4 -4
  50. package/dist/infra/cipher/file-system/binary-utils.d.ts +7 -12
  51. package/dist/infra/cipher/file-system/binary-utils.js +46 -31
  52. package/dist/infra/cipher/file-system/context-tree-file-system-factory.js +3 -2
  53. package/dist/infra/cipher/file-system/file-system-service.js +1 -0
  54. package/dist/infra/cipher/http/internal-llm-http-service.js +3 -5
  55. package/dist/infra/cipher/interactive-loop.js +3 -1
  56. package/dist/infra/cipher/llm/context/context-manager.d.ts +2 -2
  57. package/dist/infra/cipher/llm/context/context-manager.js +63 -18
  58. package/dist/infra/cipher/llm/formatters/gemini-formatter.d.ts +13 -0
  59. package/dist/infra/cipher/llm/formatters/gemini-formatter.js +146 -15
  60. package/dist/infra/cipher/llm/generators/byterover-content-generator.js +6 -2
  61. package/dist/infra/cipher/llm/internal-llm-service.js +2 -2
  62. package/dist/infra/cipher/llm/thought-parser.d.ts +21 -0
  63. package/dist/infra/cipher/llm/thought-parser.js +27 -0
  64. package/dist/infra/cipher/llm/tool-output-processor.d.ts +10 -0
  65. package/dist/infra/cipher/llm/tool-output-processor.js +80 -7
  66. package/dist/infra/cipher/process/process-service.js +11 -3
  67. package/dist/infra/cipher/session/chat-session.d.ts +7 -2
  68. package/dist/infra/cipher/session/chat-session.js +90 -52
  69. package/dist/infra/cipher/session/session-metadata-store.d.ts +52 -0
  70. package/dist/infra/cipher/session/session-metadata-store.js +406 -0
  71. package/dist/infra/cipher/system-prompt/contributors/context-tree-structure-contributor.d.ts +6 -7
  72. package/dist/infra/cipher/system-prompt/contributors/context-tree-structure-contributor.js +57 -18
  73. package/dist/infra/cipher/tools/implementations/curate-tool.js +132 -36
  74. package/dist/infra/cipher/tools/implementations/read-file-tool.js +38 -17
  75. package/dist/infra/cipher/tools/implementations/search-knowledge-tool.d.ts +7 -0
  76. package/dist/infra/cipher/tools/implementations/search-knowledge-tool.js +303 -0
  77. package/dist/infra/cipher/tools/implementations/task-tool.js +1 -0
  78. package/dist/infra/cipher/tools/index.d.ts +1 -0
  79. package/dist/infra/cipher/tools/index.js +1 -0
  80. package/dist/infra/cipher/tools/tool-manager.js +1 -0
  81. package/dist/infra/cipher/tools/tool-registry.js +7 -0
  82. package/dist/infra/connectors/connector-manager.d.ts +32 -0
  83. package/dist/infra/connectors/connector-manager.js +156 -0
  84. package/dist/infra/connectors/hook/hook-connector-config.d.ts +52 -0
  85. package/dist/infra/connectors/hook/hook-connector-config.js +41 -0
  86. package/dist/infra/connectors/hook/hook-connector.d.ts +46 -0
  87. package/dist/infra/connectors/hook/hook-connector.js +231 -0
  88. package/dist/infra/{rule → connectors/rules}/legacy-rule-detector.d.ts +2 -2
  89. package/dist/infra/{rule → connectors/rules}/legacy-rule-detector.js +1 -1
  90. package/dist/infra/connectors/rules/rules-connector-config.d.ts +95 -0
  91. package/dist/infra/{rule/agent-rule-config.js → connectors/rules/rules-connector-config.js} +10 -10
  92. package/dist/infra/connectors/rules/rules-connector.d.ts +41 -0
  93. package/dist/infra/connectors/rules/rules-connector.js +204 -0
  94. package/dist/infra/{rule/rule-template-service.d.ts → connectors/shared/template-service.d.ts} +3 -3
  95. package/dist/infra/{rule/rule-template-service.js → connectors/shared/template-service.js} +1 -1
  96. package/dist/infra/context-tree/file-context-file-reader.js +4 -0
  97. package/dist/infra/context-tree/file-context-tree-writer-service.d.ts +5 -2
  98. package/dist/infra/context-tree/file-context-tree-writer-service.js +20 -5
  99. package/dist/infra/core/executors/curate-executor.d.ts +2 -2
  100. package/dist/infra/core/executors/curate-executor.js +7 -7
  101. package/dist/infra/core/executors/query-executor.d.ts +12 -0
  102. package/dist/infra/core/executors/query-executor.js +62 -1
  103. package/dist/infra/core/task-processor.d.ts +2 -2
  104. package/dist/infra/file/fs-file-service.d.ts +7 -0
  105. package/dist/infra/file/fs-file-service.js +15 -1
  106. package/dist/infra/process/agent-worker.d.ts +2 -2
  107. package/dist/infra/process/agent-worker.js +626 -142
  108. package/dist/infra/process/constants.d.ts +1 -1
  109. package/dist/infra/process/constants.js +1 -1
  110. package/dist/infra/process/ipc-types.d.ts +17 -4
  111. package/dist/infra/process/ipc-types.js +3 -3
  112. package/dist/infra/process/parent-heartbeat.d.ts +47 -0
  113. package/dist/infra/process/parent-heartbeat.js +118 -0
  114. package/dist/infra/process/process-manager.d.ts +89 -1
  115. package/dist/infra/process/process-manager.js +293 -9
  116. package/dist/infra/process/task-queue-manager.d.ts +13 -0
  117. package/dist/infra/process/task-queue-manager.js +19 -0
  118. package/dist/infra/process/transport-handlers.d.ts +3 -0
  119. package/dist/infra/process/transport-handlers.js +82 -5
  120. package/dist/infra/process/transport-worker.js +9 -69
  121. package/dist/infra/repl/commands/connectors-command.d.ts +8 -0
  122. package/dist/infra/repl/commands/{gen-rules-command.js → connectors-command.js} +21 -10
  123. package/dist/infra/repl/commands/index.js +8 -4
  124. package/dist/infra/repl/commands/init-command.js +11 -7
  125. package/dist/infra/repl/commands/new-command.d.ts +14 -0
  126. package/dist/infra/repl/commands/new-command.js +61 -0
  127. package/dist/infra/repl/commands/query-command.js +22 -2
  128. package/dist/infra/repl/commands/{clear-command.d.ts → reset-command.d.ts} +2 -2
  129. package/dist/infra/repl/commands/{clear-command.js → reset-command.js} +11 -11
  130. package/dist/infra/transport/socket-io-transport-client.d.ts +68 -0
  131. package/dist/infra/transport/socket-io-transport-client.js +283 -7
  132. package/dist/infra/usecase/connectors-use-case.d.ts +59 -0
  133. package/dist/infra/usecase/connectors-use-case.js +203 -0
  134. package/dist/infra/usecase/init-use-case.d.ts +8 -43
  135. package/dist/infra/usecase/init-use-case.js +29 -253
  136. package/dist/infra/usecase/logout-use-case.js +2 -2
  137. package/dist/infra/usecase/pull-use-case.js +5 -5
  138. package/dist/infra/usecase/push-use-case.js +5 -5
  139. package/dist/infra/usecase/{clear-use-case.d.ts → reset-use-case.d.ts} +5 -5
  140. package/dist/infra/usecase/{clear-use-case.js → reset-use-case.js} +7 -8
  141. package/dist/infra/usecase/space-list-use-case.js +3 -3
  142. package/dist/infra/usecase/space-switch-use-case.js +3 -3
  143. package/dist/resources/prompts/curate.yml +75 -13
  144. package/dist/resources/prompts/explore.yml +34 -0
  145. package/dist/resources/prompts/query-orchestrator.yml +112 -0
  146. package/dist/resources/prompts/system-prompt.yml +12 -2
  147. package/dist/resources/tools/curate.txt +60 -15
  148. package/dist/resources/tools/search_knowledge.txt +32 -0
  149. package/dist/templates/sections/brv-instructions.md +98 -0
  150. package/dist/tui/components/inline-prompts/inline-confirm.js +2 -2
  151. package/dist/tui/components/onboarding/onboarding-flow.js +14 -10
  152. package/dist/tui/components/onboarding/welcome-box.js +1 -1
  153. package/dist/tui/contexts/onboarding-context.d.ts +4 -0
  154. package/dist/tui/contexts/onboarding-context.js +14 -2
  155. package/dist/tui/views/command-view.js +19 -0
  156. package/dist/utils/file-validator.d.ts +1 -1
  157. package/dist/utils/file-validator.js +34 -35
  158. package/dist/utils/type-guards.d.ts +5 -0
  159. package/dist/utils/type-guards.js +7 -0
  160. package/oclif.manifest.json +32 -6
  161. package/package.json +4 -1
  162. package/dist/config/context-tree-domains.d.ts +0 -29
  163. package/dist/config/context-tree-domains.js +0 -29
  164. package/dist/core/interfaces/usecase/i-generate-rules-use-case.d.ts +0 -3
  165. package/dist/infra/repl/commands/gen-rules-command.d.ts +0 -7
  166. package/dist/infra/rule/agent-rule-config.d.ts +0 -19
  167. package/dist/infra/usecase/generate-rules-use-case.d.ts +0 -61
  168. package/dist/infra/usecase/generate-rules-use-case.js +0 -285
  169. /package/dist/core/interfaces/{usecase/i-clear-use-case.js → connectors/connector-types.js} +0 -0
  170. /package/dist/core/interfaces/{usecase/i-generate-rules-use-case.js → connectors/i-connector-manager.js} +0 -0
  171. /package/dist/infra/{rule → connectors/shared}/constants.d.ts +0 -0
  172. /package/dist/infra/{rule → connectors/shared}/constants.js +0 -0
@@ -1,6 +1,33 @@
1
1
  import { io } from 'socket.io-client';
2
2
  import { TRANSPORT_CONNECT_TIMEOUT_MS, TRANSPORT_DEFAULT_TRANSPORTS, TRANSPORT_RECONNECTION_ATTEMPTS, TRANSPORT_RECONNECTION_DELAY_MAX_MS, TRANSPORT_RECONNECTION_DELAY_MS, TRANSPORT_REQUEST_TIMEOUT_MS, TRANSPORT_ROOM_TIMEOUT_MS, } from '../../constants.js';
3
3
  import { TransportConnectionError, TransportNotConnectedError, TransportRequestError, TransportRequestTimeoutError, TransportRoomError, TransportRoomTimeoutError, } from '../../core/domain/errors/transport-error.js';
4
+ import { processLog } from '../../utils/process-logger.js';
5
+ /**
6
+ * Log transport client events for debugging reconnection issues.
7
+ */
8
+ function clientLog(message) {
9
+ processLog(`[TransportClient] ${message}`);
10
+ }
11
+ /**
12
+ * Force reconnect delays after Socket.IO gives up (exponential backoff).
13
+ * Used when all built-in reconnection attempts fail.
14
+ */
15
+ const FORCE_RECONNECT_DELAYS = [5000, 10_000, 20_000, 30_000, 60_000]; // 5s, 10s, 20s, 30s, 60s cap
16
+ /**
17
+ * Maximum number of force reconnect attempts before giving up.
18
+ * After this, ProcessManager's periodic health check will detect the failure and restart.
19
+ */
20
+ const MAX_FORCE_RECONNECT_ATTEMPTS = 10;
21
+ /**
22
+ * Interval for checking time jumps that indicate system wake from sleep.
23
+ * Uses a short interval so we detect wake quickly.
24
+ */
25
+ const WAKE_DETECTION_INTERVAL_MS = 5000;
26
+ /**
27
+ * Time jump threshold to detect wake from sleep/hibernate.
28
+ * If the actual elapsed time exceeds expected by this amount, system likely woke from sleep.
29
+ */
30
+ const WAKE_TIME_JUMP_THRESHOLD_MS = 10_000;
4
31
  /**
5
32
  * Socket.IO implementation of ITransportClient.
6
33
  *
@@ -8,15 +35,29 @@ import { TransportConnectionError, TransportNotConnectedError, TransportRequestE
8
35
  * - Auto-reconnects with exponential backoff
9
36
  * - Request/response uses Socket.IO acknowledgements (callbacks)
10
37
  * - Connection state is tracked and exposed via onStateChange
38
+ * - Force reconnect after all built-in attempts fail (sleep/wake recovery)
39
+ * - Auto-rejoins rooms after reconnect
11
40
  */
12
41
  export class SocketIOTransportClient {
13
42
  config;
14
43
  eventHandlers = new Map();
44
+ /** Track force reconnect attempt count for backoff calculation */
45
+ forceReconnectAttempt = 0;
46
+ /** Timer for scheduled force reconnect */
47
+ forceReconnectTimer;
48
+ /** Track joined rooms for auto-rejoin after reconnect */
49
+ joinedRooms = new Set();
50
+ /** Last time we checked for wake (for detecting time jumps) */
51
+ lastWakeCheckTime = Date.now();
15
52
  /** Track which events have socket listeners registered (prevents duplicates on reconnect) */
16
53
  registeredSocketEvents = new Set();
54
+ /** Store server URL for force reconnect */
55
+ serverUrl;
17
56
  socket;
18
57
  state = 'disconnected';
19
58
  stateHandlers = new Set();
59
+ /** Timer for wake detection */
60
+ wakeDetectionTimer;
20
61
  constructor(config) {
21
62
  this.config = {
22
63
  connectTimeoutMs: config?.connectTimeoutMs ?? TRANSPORT_CONNECT_TIMEOUT_MS,
@@ -32,6 +73,20 @@ export class SocketIOTransportClient {
32
73
  if (this.socket?.connected) {
33
74
  return;
34
75
  }
76
+ // Cleanup existing socket if present but not connected
77
+ // This prevents resource leaks when connect() is called again after a failed connection
78
+ if (this.socket) {
79
+ this.socket.removeAllListeners();
80
+ this.socket.io.removeAllListeners();
81
+ this.socket.disconnect();
82
+ this.socket = undefined;
83
+ this.registeredSocketEvents.clear();
84
+ }
85
+ // Store URL for force reconnect
86
+ this.serverUrl = url;
87
+ // Clear any pending force reconnect timer (we're connecting now)
88
+ // Do NOT reset counter - connection may still fail, need to preserve backoff
89
+ this.clearForceReconnectTimer();
35
90
  return new Promise((resolve, reject) => {
36
91
  this.setState('connecting');
37
92
  this.socket = io(url, {
@@ -51,6 +106,8 @@ export class SocketIOTransportClient {
51
106
  // This fixes the issue where on() called before connect() would not
52
107
  // actually register handlers on the socket.
53
108
  this.registerPendingEventHandlers();
109
+ // Start wake detection to handle sleep/hibernate recovery
110
+ this.startWakeDetection();
54
111
  resolve();
55
112
  };
56
113
  const onConnectError = (error) => {
@@ -74,7 +131,8 @@ export class SocketIOTransportClient {
74
131
  this.socket.on('connect', onConnect);
75
132
  this.socket.once('connect_error', onConnectError);
76
133
  // Set up persistent event handlers
77
- this.socket.on('disconnect', () => {
134
+ this.socket.on('disconnect', (reason) => {
135
+ clientLog(`Socket disconnected, reason: ${reason}, active: ${this.socket?.active}`);
78
136
  if (this.socket?.active) {
79
137
  // Socket.IO is attempting to reconnect
80
138
  this.setState('reconnecting');
@@ -83,19 +141,31 @@ export class SocketIOTransportClient {
83
141
  this.setState('disconnected');
84
142
  }
85
143
  });
86
- this.socket.io.on('reconnect', () => {
144
+ this.socket.io.on('reconnect', (attemptNumber) => {
145
+ clientLog(`Socket.IO built-in reconnect succeeded after ${attemptNumber} attempts`);
87
146
  this.setState('connected');
88
147
  // Re-register event handlers after reconnect
89
148
  // Clear tracking first since socket listeners were reset during reconnect
90
149
  this.registeredSocketEvents.clear();
91
150
  this.registerPendingEventHandlers();
151
+ // Auto-rejoin rooms after reconnect
152
+ // Use process.nextTick to ensure socket.connected is true
153
+ // (reconnect event fires before socket.connected is updated)
154
+ process.nextTick(() => this.rejoinRooms());
92
155
  });
93
156
  this.socket.io.on('reconnect_failed', () => {
157
+ clientLog('Socket.IO built-in reconnection failed after all attempts, starting force reconnect');
94
158
  this.setState('disconnected');
159
+ // Start force reconnect loop after Socket.IO gives up
160
+ this.scheduleForceReconnect();
95
161
  });
96
162
  });
97
163
  }
98
164
  async disconnect() {
165
+ // Cancel any pending force reconnect
166
+ this.cancelForceReconnect();
167
+ // Stop wake detection
168
+ this.stopWakeDetection();
99
169
  const { socket } = this;
100
170
  if (!socket) {
101
171
  return;
@@ -107,10 +177,12 @@ export class SocketIOTransportClient {
107
177
  socket.disconnect();
108
178
  this.socket = undefined;
109
179
  this.setState('disconnected');
110
- // Clear all tracking state to prevent leaks
180
+ // Clear socket-specific tracking state to prevent leaks
181
+ // Note: stateHandlers are NOT cleared - subscriptions persist across disconnect/connect cycles
182
+ // Users can unsubscribe via the returned function from onStateChange()
111
183
  this.eventHandlers.clear();
112
184
  this.registeredSocketEvents.clear();
113
- this.stateHandlers.clear();
185
+ this.joinedRooms.clear();
114
186
  resolve();
115
187
  });
116
188
  }
@@ -126,12 +198,21 @@ export class SocketIOTransportClient {
126
198
  throw new TransportNotConnectedError('joinRoom');
127
199
  }
128
200
  return new Promise((resolve, reject) => {
201
+ // Flag to prevent callback from executing after timeout
202
+ let resolved = false;
129
203
  const timer = setTimeout(() => {
204
+ resolved = true;
130
205
  reject(new TransportRoomTimeoutError(room, 'join', this.config.roomTimeoutMs));
131
206
  }, this.config.roomTimeoutMs);
132
207
  socket.emit('room:join', room, (response) => {
208
+ // Don't process if timeout already fired
209
+ if (resolved)
210
+ return;
211
+ resolved = true;
133
212
  clearTimeout(timer);
134
213
  if (response.success) {
214
+ // Track joined room for auto-rejoin after reconnect
215
+ this.joinedRooms.add(room);
135
216
  resolve();
136
217
  }
137
218
  else {
@@ -146,12 +227,21 @@ export class SocketIOTransportClient {
146
227
  throw new TransportNotConnectedError('leaveRoom');
147
228
  }
148
229
  return new Promise((resolve, reject) => {
230
+ // Flag to prevent callback from executing after timeout
231
+ let resolved = false;
149
232
  const timer = setTimeout(() => {
233
+ resolved = true;
150
234
  reject(new TransportRoomTimeoutError(room, 'leave', this.config.roomTimeoutMs));
151
235
  }, this.config.roomTimeoutMs);
152
236
  socket.emit('room:leave', room, (response) => {
237
+ // Don't process if timeout already fired
238
+ if (resolved)
239
+ return;
240
+ resolved = true;
153
241
  clearTimeout(timer);
154
242
  if (response.success) {
243
+ // Remove from tracked rooms
244
+ this.joinedRooms.delete(room);
155
245
  resolve();
156
246
  }
157
247
  else {
@@ -167,7 +257,9 @@ export class SocketIOTransportClient {
167
257
  // Register socket listener if connected and not already registered
168
258
  // Use registeredSocketEvents to prevent duplicates across reconnects
169
259
  this.registerSocketEventIfNeeded(event);
170
- // Wrap handler to store without type assertion
260
+ // Wrap handler to match StoredEventHandler signature.
261
+ // BOUNDARY CAST: Socket.IO delivers unknown data; caller specifies T via generic.
262
+ // Type guard not possible for generic T at runtime.
171
263
  const wrappedHandler = (data) => handler(data);
172
264
  const handlers = this.eventHandlers.get(event);
173
265
  handlers?.add(wrappedHandler);
@@ -188,6 +280,14 @@ export class SocketIOTransportClient {
188
280
  }
189
281
  socket.once(event, handler);
190
282
  }
283
+ /**
284
+ * Subscribe to connection state changes.
285
+ *
286
+ * Important: Subscriptions persist across disconnect/connect cycles. Call the returned
287
+ * function to unsubscribe when no longer needed to prevent memory leaks.
288
+ *
289
+ * @returns Unsubscribe function - call to remove the handler
290
+ */
191
291
  onStateChange(handler) {
192
292
  this.stateHandlers.add(handler);
193
293
  return () => {
@@ -210,8 +310,9 @@ export class SocketIOTransportClient {
210
310
  resolve(response.data);
211
311
  }
212
312
  else if (response.success) {
213
- // Response success but data is undefined - resolve with undefined cast
214
- // This is a boundary case where server returns void
313
+ // BOUNDARY CAST: Server returned success without data (void response).
314
+ // Caller must specify TResponse=void for void endpoints.
315
+ // Type guard not applicable - void vs non-void is a type-level distinction.
215
316
  resolve(undefined);
216
317
  }
217
318
  else {
@@ -220,6 +321,88 @@ export class SocketIOTransportClient {
220
321
  });
221
322
  });
222
323
  }
324
+ /**
325
+ * Attempt force reconnect after Socket.IO gives up.
326
+ * Creates a new socket and connects.
327
+ * Gives up after MAX_FORCE_RECONNECT_ATTEMPTS (ProcessManager will detect via periodic health check).
328
+ */
329
+ async attemptForceReconnect() {
330
+ if (!this.serverUrl || this.state === 'connected') {
331
+ return;
332
+ }
333
+ // Check max attempts - give up if exceeded
334
+ // ProcessManager's periodic health check (30s) will detect and restart
335
+ if (this.forceReconnectAttempt >= MAX_FORCE_RECONNECT_ATTEMPTS) {
336
+ clientLog(`Force reconnect gave up after ${MAX_FORCE_RECONNECT_ATTEMPTS} attempts`);
337
+ this.setState('disconnected');
338
+ return;
339
+ }
340
+ clientLog(`Force reconnect attempt ${this.forceReconnectAttempt + 1}/${MAX_FORCE_RECONNECT_ATTEMPTS}`);
341
+ try {
342
+ // Cleanup old socket completely
343
+ if (this.socket) {
344
+ this.socket.removeAllListeners();
345
+ this.socket.io.removeAllListeners();
346
+ this.socket.disconnect();
347
+ this.socket = undefined;
348
+ }
349
+ // Reset tracking (will be re-populated on connect)
350
+ this.registeredSocketEvents.clear();
351
+ // Attempt to connect
352
+ await this.connect(this.serverUrl);
353
+ // Rejoin rooms after force reconnect (same as built-in reconnect)
354
+ // Without this, TUI REPL won't receive events from broadcast-room
355
+ // Use process.nextTick for consistency with built-in reconnect handler
356
+ clientLog(`Force reconnect succeeded, rejoining ${this.joinedRooms.size} rooms`);
357
+ process.nextTick(() => this.rejoinRooms());
358
+ // Success - reset attempt counter
359
+ this.forceReconnectAttempt = 0;
360
+ }
361
+ catch (error) {
362
+ // Failed - schedule next attempt
363
+ const errorMsg = error instanceof Error ? error.message : String(error);
364
+ clientLog(`Force reconnect failed: ${errorMsg}`);
365
+ this.scheduleForceReconnect();
366
+ }
367
+ }
368
+ /**
369
+ * Cancel force reconnect completely (clears timer AND resets counter).
370
+ * Used when connection is stable or on disconnect().
371
+ */
372
+ cancelForceReconnect() {
373
+ this.clearForceReconnectTimer();
374
+ this.forceReconnectAttempt = 0;
375
+ }
376
+ /**
377
+ * Clear force reconnect timer only (does NOT reset counter).
378
+ * Used when initiating new connection attempt - counter preserved for backoff continuity.
379
+ */
380
+ clearForceReconnectTimer() {
381
+ if (this.forceReconnectTimer) {
382
+ clearTimeout(this.forceReconnectTimer);
383
+ this.forceReconnectTimer = undefined;
384
+ }
385
+ }
386
+ /**
387
+ * Handle system wake from sleep/hibernate.
388
+ * Re-triggers reconnection if not connected and force reconnect has given up.
389
+ */
390
+ handleWakeFromSleep() {
391
+ // Only take action if disconnected (force reconnect may have given up before sleep)
392
+ if (this.state === 'disconnected' && this.serverUrl) {
393
+ clientLog('handleWakeFromSleep: state is disconnected, restarting force reconnect');
394
+ // Reset attempt counter to get fresh tries after wake
395
+ this.forceReconnectAttempt = 0;
396
+ this.scheduleForceReconnect();
397
+ }
398
+ else if (this.state === 'connected' && this.socket && !this.socket.connected) {
399
+ // State says connected but socket isn't - stale state after wake
400
+ clientLog('handleWakeFromSleep: state mismatch (connected but socket disconnected), triggering reconnect');
401
+ this.setState('disconnected');
402
+ this.forceReconnectAttempt = 0;
403
+ this.scheduleForceReconnect();
404
+ }
405
+ }
223
406
  /**
224
407
  * Register all pending event handlers on the socket.
225
408
  * Called after successful connection to handle handlers added before connect().
@@ -249,6 +432,52 @@ export class SocketIOTransportClient {
249
432
  });
250
433
  this.registeredSocketEvents.add(event);
251
434
  }
435
+ /**
436
+ * Re-join all tracked rooms after reconnect.
437
+ * Retries failed room joins with exponential backoff.
438
+ */
439
+ rejoinRooms() {
440
+ clientLog(`rejoinRooms: rejoining ${this.joinedRooms.size} rooms: [${[...this.joinedRooms].join(', ')}]`);
441
+ for (const room of this.joinedRooms) {
442
+ this.rejoinRoomWithRetry(room, 0);
443
+ }
444
+ }
445
+ /**
446
+ * Rejoin a single room with retry logic.
447
+ * Uses exponential backoff: 50ms, 100ms, 200ms, 400ms, 800ms (max 5 attempts).
448
+ * Retries BOTH when socket not connected AND when room:join fails.
449
+ */
450
+ rejoinRoomWithRetry(room, attempt) {
451
+ const MAX_REJOIN_ATTEMPTS = 5;
452
+ const REJOIN_BASE_DELAY_MS = 50;
453
+ if (attempt >= MAX_REJOIN_ATTEMPTS) {
454
+ clientLog(`rejoinRoomWithRetry: gave up rejoining '${room}' after ${MAX_REJOIN_ATTEMPTS} attempts`);
455
+ return;
456
+ }
457
+ // If socket not connected yet, retry with backoff (don't silent return!)
458
+ // This handles race condition where reconnect event fires before socket.connected is true
459
+ if (!this.socket?.connected) {
460
+ const delay = REJOIN_BASE_DELAY_MS * 2 ** attempt;
461
+ clientLog(`rejoinRoomWithRetry: socket not connected, retrying '${room}' in ${delay}ms (attempt ${attempt + 1}/${MAX_REJOIN_ATTEMPTS})`);
462
+ setTimeout(() => this.rejoinRoomWithRetry(room, attempt + 1), delay);
463
+ return;
464
+ }
465
+ clientLog(`rejoinRoomWithRetry: attempting to rejoin '${room}' (attempt ${attempt + 1}/${MAX_REJOIN_ATTEMPTS})`);
466
+ this.socket.emit('room:join', room, (response) => {
467
+ if (response?.success) {
468
+ clientLog(`rejoinRoomWithRetry: successfully rejoined '${room}'`);
469
+ }
470
+ else if (attempt < MAX_REJOIN_ATTEMPTS - 1) {
471
+ // Retry with exponential backoff
472
+ const delay = REJOIN_BASE_DELAY_MS * 2 ** attempt;
473
+ clientLog(`rejoinRoomWithRetry: room:join failed for '${room}', retrying in ${delay}ms`);
474
+ setTimeout(() => this.rejoinRoomWithRetry(room, attempt + 1), delay);
475
+ }
476
+ else {
477
+ clientLog(`rejoinRoomWithRetry: failed to rejoin '${room}' on final attempt`);
478
+ }
479
+ });
480
+ }
252
481
  /**
253
482
  * Remove socket listener for an event and clear tracking.
254
483
  */
@@ -259,12 +488,59 @@ export class SocketIOTransportClient {
259
488
  this.registeredSocketEvents.delete(event);
260
489
  }
261
490
  }
491
+ /**
492
+ * Schedule force reconnect with exponential backoff.
493
+ * Called after Socket.IO's built-in reconnection gives up.
494
+ */
495
+ scheduleForceReconnect() {
496
+ if (!this.serverUrl) {
497
+ clientLog('scheduleForceReconnect: no serverUrl, skipping');
498
+ return;
499
+ }
500
+ // Clear existing timer to prevent duplicate timers (fixes concurrent reconnect race)
501
+ // This can happen when both 'connect_error' and 'reconnect_failed' schedule timers
502
+ this.clearForceReconnectTimer();
503
+ const delay = FORCE_RECONNECT_DELAYS[Math.min(this.forceReconnectAttempt, FORCE_RECONNECT_DELAYS.length - 1)];
504
+ clientLog(`scheduleForceReconnect: scheduling attempt ${this.forceReconnectAttempt + 1} in ${delay}ms`);
505
+ this.forceReconnectTimer = setTimeout(() => {
506
+ this.forceReconnectAttempt++;
507
+ this.attemptForceReconnect();
508
+ }, delay);
509
+ }
262
510
  setState(state) {
263
511
  if (this.state !== state) {
512
+ clientLog(`State change: ${this.state} -> ${state}`);
264
513
  this.state = state;
265
514
  for (const handler of this.stateHandlers) {
266
515
  handler(state);
267
516
  }
268
517
  }
269
518
  }
519
+ /**
520
+ * Start wake detection timer.
521
+ * Periodically checks for time jumps that indicate system woke from sleep.
522
+ */
523
+ startWakeDetection() {
524
+ this.stopWakeDetection();
525
+ this.lastWakeCheckTime = Date.now();
526
+ this.wakeDetectionTimer = setInterval(() => {
527
+ const now = Date.now();
528
+ const elapsed = now - this.lastWakeCheckTime;
529
+ this.lastWakeCheckTime = now;
530
+ // If elapsed time is much greater than interval, system likely woke from sleep
531
+ if (elapsed > WAKE_DETECTION_INTERVAL_MS + WAKE_TIME_JUMP_THRESHOLD_MS) {
532
+ clientLog(`Wake detected: time jump of ${elapsed}ms (expected ~${WAKE_DETECTION_INTERVAL_MS}ms)`);
533
+ this.handleWakeFromSleep();
534
+ }
535
+ }, WAKE_DETECTION_INTERVAL_MS);
536
+ }
537
+ /**
538
+ * Stop wake detection timer.
539
+ */
540
+ stopWakeDetection() {
541
+ if (this.wakeDetectionTimer) {
542
+ clearInterval(this.wakeDetectionTimer);
543
+ this.wakeDetectionTimer = undefined;
544
+ }
545
+ }
270
546
  }
@@ -0,0 +1,59 @@
1
+ import type { IConnectorManager } from '../../core/interfaces/connectors/i-connector-manager.js';
2
+ import type { ITerminal } from '../../core/interfaces/i-terminal.js';
3
+ import type { ITrackingService } from '../../core/interfaces/i-tracking-service.js';
4
+ import { IConnectorsUseCase } from '../../core/interfaces/usecase/i-connectors-use-case.js';
5
+ /**
6
+ * Options for constructing ConnectorsUseCase.
7
+ */
8
+ export interface ConnectorsUseCaseOptions {
9
+ connectorManager: IConnectorManager;
10
+ terminal: ITerminal;
11
+ trackingService: ITrackingService;
12
+ }
13
+ /**
14
+ * Use case for managing connectors.
15
+ * Shows list of connected agents and allows managing or adding new connections.
16
+ */
17
+ export declare class ConnectorsUseCase implements IConnectorsUseCase {
18
+ private readonly connectorManager;
19
+ private readonly terminal;
20
+ private readonly trackingService;
21
+ constructor(options: ConnectorsUseCaseOptions);
22
+ run(): Promise<void>;
23
+ /**
24
+ * Gets a description for a connector type.
25
+ */
26
+ private getConnectorDescription;
27
+ /**
28
+ * Gets a human-readable label for a connector type.
29
+ */
30
+ private getConnectorLabel;
31
+ /**
32
+ * Handles the case when connectors are already installed.
33
+ */
34
+ private handleExistingConnectors;
35
+ /**
36
+ * Handles the case when no connectors are installed.
37
+ */
38
+ private handleNoConnectorsInstalled;
39
+ /**
40
+ * Installs the selected connector and displays result.
41
+ */
42
+ private installConnector;
43
+ /**
44
+ * Prompts user to select from connected agents or add new.
45
+ */
46
+ private promptForAgentToManage;
47
+ /**
48
+ * Prompts the user to select a connector type.
49
+ */
50
+ private promptForConnectorType;
51
+ /**
52
+ * Prompts user to select a new agent (excludes already connected agents).
53
+ */
54
+ private promptForNewAgentSelection;
55
+ /**
56
+ * Prompts the user to confirm switching connectors.
57
+ */
58
+ private promptForSwitchConfirmation;
59
+ }