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.
- package/README.md +24 -11
- package/dist/commands/curate.js +1 -1
- package/dist/commands/hook-prompt-submit.d.ts +27 -0
- package/dist/commands/hook-prompt-submit.js +39 -0
- package/dist/commands/main.d.ts +13 -0
- package/dist/commands/main.js +53 -2
- package/dist/commands/query.js +1 -1
- package/dist/commands/status.js +8 -3
- package/dist/constants.d.ts +2 -2
- package/dist/constants.js +2 -2
- package/dist/core/domain/cipher/llm/registry.js +53 -2
- package/dist/core/domain/cipher/llm/types.d.ts +2 -0
- package/dist/core/domain/cipher/process/types.d.ts +7 -0
- package/dist/core/domain/cipher/session/session-metadata.d.ts +178 -0
- package/dist/core/domain/cipher/session/session-metadata.js +147 -0
- package/dist/core/domain/cipher/tools/constants.d.ts +1 -0
- package/dist/core/domain/cipher/tools/constants.js +1 -0
- package/dist/core/domain/entities/agent.d.ts +16 -0
- package/dist/core/domain/entities/agent.js +24 -0
- package/dist/core/domain/entities/connector-type.d.ts +9 -0
- package/dist/core/domain/entities/connector-type.js +8 -0
- package/dist/core/domain/entities/event.d.ts +1 -1
- package/dist/core/domain/entities/event.js +2 -0
- package/dist/core/domain/errors/task-error.d.ts +4 -0
- package/dist/core/domain/errors/task-error.js +7 -0
- package/dist/core/domain/knowledge/markdown-writer.d.ts +15 -18
- package/dist/core/domain/knowledge/markdown-writer.js +232 -34
- package/dist/core/domain/knowledge/relation-parser.d.ts +25 -39
- package/dist/core/domain/knowledge/relation-parser.js +39 -61
- package/dist/core/domain/transport/schemas.d.ts +77 -2
- package/dist/core/domain/transport/schemas.js +51 -2
- package/dist/core/interfaces/cipher/i-session-persistence.d.ts +133 -0
- package/dist/core/interfaces/cipher/i-session-persistence.js +7 -0
- package/dist/core/interfaces/cipher/message-types.d.ts +6 -0
- package/dist/core/interfaces/connectors/connector-types.d.ts +57 -0
- package/dist/core/interfaces/connectors/i-connector-manager.d.ts +72 -0
- package/dist/core/interfaces/connectors/i-connector.d.ts +54 -0
- package/dist/core/interfaces/connectors/i-connector.js +1 -0
- package/dist/core/interfaces/executor/i-curate-executor.d.ts +2 -2
- package/dist/core/interfaces/i-context-file-reader.d.ts +3 -0
- package/dist/core/interfaces/i-file-service.d.ts +7 -0
- package/dist/core/interfaces/usecase/i-connectors-use-case.d.ts +3 -0
- package/dist/core/interfaces/usecase/i-connectors-use-case.js +1 -0
- package/dist/core/interfaces/usecase/{i-clear-use-case.d.ts → i-reset-use-case.d.ts} +1 -1
- package/dist/core/interfaces/usecase/i-reset-use-case.js +1 -0
- package/dist/hooks/init/update-notifier.d.ts +1 -0
- package/dist/hooks/init/update-notifier.js +10 -1
- package/dist/infra/cipher/agent/agent-schemas.d.ts +6 -6
- package/dist/infra/cipher/agent/service-initializer.js +4 -4
- package/dist/infra/cipher/file-system/binary-utils.d.ts +7 -12
- package/dist/infra/cipher/file-system/binary-utils.js +46 -31
- package/dist/infra/cipher/file-system/context-tree-file-system-factory.js +3 -2
- package/dist/infra/cipher/file-system/file-system-service.js +1 -0
- package/dist/infra/cipher/http/internal-llm-http-service.js +3 -5
- package/dist/infra/cipher/interactive-loop.js +3 -1
- package/dist/infra/cipher/llm/context/context-manager.d.ts +2 -2
- package/dist/infra/cipher/llm/context/context-manager.js +63 -18
- package/dist/infra/cipher/llm/formatters/gemini-formatter.d.ts +13 -0
- package/dist/infra/cipher/llm/formatters/gemini-formatter.js +146 -15
- package/dist/infra/cipher/llm/generators/byterover-content-generator.js +6 -2
- package/dist/infra/cipher/llm/internal-llm-service.js +2 -2
- package/dist/infra/cipher/llm/thought-parser.d.ts +21 -0
- package/dist/infra/cipher/llm/thought-parser.js +27 -0
- package/dist/infra/cipher/llm/tool-output-processor.d.ts +10 -0
- package/dist/infra/cipher/llm/tool-output-processor.js +80 -7
- package/dist/infra/cipher/process/process-service.js +11 -3
- package/dist/infra/cipher/session/chat-session.d.ts +7 -2
- package/dist/infra/cipher/session/chat-session.js +90 -52
- package/dist/infra/cipher/session/session-metadata-store.d.ts +52 -0
- package/dist/infra/cipher/session/session-metadata-store.js +406 -0
- package/dist/infra/cipher/system-prompt/contributors/context-tree-structure-contributor.d.ts +6 -7
- package/dist/infra/cipher/system-prompt/contributors/context-tree-structure-contributor.js +57 -18
- package/dist/infra/cipher/tools/implementations/curate-tool.js +132 -36
- package/dist/infra/cipher/tools/implementations/read-file-tool.js +38 -17
- package/dist/infra/cipher/tools/implementations/search-knowledge-tool.d.ts +7 -0
- package/dist/infra/cipher/tools/implementations/search-knowledge-tool.js +303 -0
- package/dist/infra/cipher/tools/implementations/task-tool.js +1 -0
- package/dist/infra/cipher/tools/index.d.ts +1 -0
- package/dist/infra/cipher/tools/index.js +1 -0
- package/dist/infra/cipher/tools/tool-manager.js +1 -0
- package/dist/infra/cipher/tools/tool-registry.js +7 -0
- package/dist/infra/connectors/connector-manager.d.ts +32 -0
- package/dist/infra/connectors/connector-manager.js +156 -0
- package/dist/infra/connectors/hook/hook-connector-config.d.ts +52 -0
- package/dist/infra/connectors/hook/hook-connector-config.js +41 -0
- package/dist/infra/connectors/hook/hook-connector.d.ts +46 -0
- package/dist/infra/connectors/hook/hook-connector.js +231 -0
- package/dist/infra/{rule → connectors/rules}/legacy-rule-detector.d.ts +2 -2
- package/dist/infra/{rule → connectors/rules}/legacy-rule-detector.js +1 -1
- package/dist/infra/connectors/rules/rules-connector-config.d.ts +95 -0
- package/dist/infra/{rule/agent-rule-config.js → connectors/rules/rules-connector-config.js} +10 -10
- package/dist/infra/connectors/rules/rules-connector.d.ts +41 -0
- package/dist/infra/connectors/rules/rules-connector.js +204 -0
- package/dist/infra/{rule/rule-template-service.d.ts → connectors/shared/template-service.d.ts} +3 -3
- package/dist/infra/{rule/rule-template-service.js → connectors/shared/template-service.js} +1 -1
- package/dist/infra/context-tree/file-context-file-reader.js +4 -0
- package/dist/infra/context-tree/file-context-tree-writer-service.d.ts +5 -2
- package/dist/infra/context-tree/file-context-tree-writer-service.js +20 -5
- package/dist/infra/core/executors/curate-executor.d.ts +2 -2
- package/dist/infra/core/executors/curate-executor.js +7 -7
- package/dist/infra/core/executors/query-executor.d.ts +12 -0
- package/dist/infra/core/executors/query-executor.js +62 -1
- package/dist/infra/core/task-processor.d.ts +2 -2
- package/dist/infra/file/fs-file-service.d.ts +7 -0
- package/dist/infra/file/fs-file-service.js +15 -1
- package/dist/infra/process/agent-worker.d.ts +2 -2
- package/dist/infra/process/agent-worker.js +626 -142
- package/dist/infra/process/constants.d.ts +1 -1
- package/dist/infra/process/constants.js +1 -1
- package/dist/infra/process/ipc-types.d.ts +17 -4
- package/dist/infra/process/ipc-types.js +3 -3
- package/dist/infra/process/parent-heartbeat.d.ts +47 -0
- package/dist/infra/process/parent-heartbeat.js +118 -0
- package/dist/infra/process/process-manager.d.ts +89 -1
- package/dist/infra/process/process-manager.js +293 -9
- package/dist/infra/process/task-queue-manager.d.ts +13 -0
- package/dist/infra/process/task-queue-manager.js +19 -0
- package/dist/infra/process/transport-handlers.d.ts +3 -0
- package/dist/infra/process/transport-handlers.js +82 -5
- package/dist/infra/process/transport-worker.js +9 -69
- package/dist/infra/repl/commands/connectors-command.d.ts +8 -0
- package/dist/infra/repl/commands/{gen-rules-command.js → connectors-command.js} +21 -10
- package/dist/infra/repl/commands/index.js +8 -4
- package/dist/infra/repl/commands/init-command.js +11 -7
- package/dist/infra/repl/commands/new-command.d.ts +14 -0
- package/dist/infra/repl/commands/new-command.js +61 -0
- package/dist/infra/repl/commands/query-command.js +22 -2
- package/dist/infra/repl/commands/{clear-command.d.ts → reset-command.d.ts} +2 -2
- package/dist/infra/repl/commands/{clear-command.js → reset-command.js} +11 -11
- package/dist/infra/transport/socket-io-transport-client.d.ts +68 -0
- package/dist/infra/transport/socket-io-transport-client.js +283 -7
- package/dist/infra/usecase/connectors-use-case.d.ts +59 -0
- package/dist/infra/usecase/connectors-use-case.js +203 -0
- package/dist/infra/usecase/init-use-case.d.ts +8 -43
- package/dist/infra/usecase/init-use-case.js +29 -253
- package/dist/infra/usecase/logout-use-case.js +2 -2
- package/dist/infra/usecase/pull-use-case.js +5 -5
- package/dist/infra/usecase/push-use-case.js +5 -5
- package/dist/infra/usecase/{clear-use-case.d.ts → reset-use-case.d.ts} +5 -5
- package/dist/infra/usecase/{clear-use-case.js → reset-use-case.js} +7 -8
- package/dist/infra/usecase/space-list-use-case.js +3 -3
- package/dist/infra/usecase/space-switch-use-case.js +3 -3
- package/dist/resources/prompts/curate.yml +75 -13
- package/dist/resources/prompts/explore.yml +34 -0
- package/dist/resources/prompts/query-orchestrator.yml +112 -0
- package/dist/resources/prompts/system-prompt.yml +12 -2
- package/dist/resources/tools/curate.txt +60 -15
- package/dist/resources/tools/search_knowledge.txt +32 -0
- package/dist/templates/sections/brv-instructions.md +98 -0
- package/dist/tui/components/inline-prompts/inline-confirm.js +2 -2
- package/dist/tui/components/onboarding/onboarding-flow.js +14 -10
- package/dist/tui/components/onboarding/welcome-box.js +1 -1
- package/dist/tui/contexts/onboarding-context.d.ts +4 -0
- package/dist/tui/contexts/onboarding-context.js +14 -2
- package/dist/tui/views/command-view.js +19 -0
- package/dist/utils/file-validator.d.ts +1 -1
- package/dist/utils/file-validator.js +34 -35
- package/dist/utils/type-guards.d.ts +5 -0
- package/dist/utils/type-guards.js +7 -0
- package/oclif.manifest.json +32 -6
- package/package.json +4 -1
- package/dist/config/context-tree-domains.d.ts +0 -29
- package/dist/config/context-tree-domains.js +0 -29
- package/dist/core/interfaces/usecase/i-generate-rules-use-case.d.ts +0 -3
- package/dist/infra/repl/commands/gen-rules-command.d.ts +0 -7
- package/dist/infra/rule/agent-rule-config.d.ts +0 -19
- package/dist/infra/usecase/generate-rules-use-case.d.ts +0 -61
- package/dist/infra/usecase/generate-rules-use-case.js +0 -285
- /package/dist/core/interfaces/{usecase/i-clear-use-case.js → connectors/connector-types.js} +0 -0
- /package/dist/core/interfaces/{usecase/i-generate-rules-use-case.js → connectors/i-connector-manager.js} +0 -0
- /package/dist/infra/{rule → connectors/shared}/constants.d.ts +0 -0
- /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
|
|
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.
|
|
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
|
|
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
|
-
//
|
|
214
|
-
//
|
|
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
|
+
}
|