byterover-cli 1.0.5 → 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 +12 -10
- package/dist/commands/hook-prompt-submit.d.ts +27 -0
- package/dist/commands/hook-prompt-submit.js +39 -0
- package/dist/commands/status.js +8 -3
- package/dist/constants.d.ts +1 -1
- package/dist/constants.js +1 -1
- 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/transport/schemas.d.ts +40 -0
- package/dist/core/domain/transport/schemas.js +28 -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-manager.js +1 -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/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/hooks/init/update-notifier.d.ts +1 -0
- package/dist/hooks/init/update-notifier.js +10 -1
- 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/llm/context/context-manager.d.ts +2 -2
- package/dist/infra/cipher/llm/context/context-manager.js +23 -2
- package/dist/infra/cipher/llm/formatters/gemini-formatter.js +48 -9
- package/dist/infra/cipher/llm/internal-llm-service.js +2 -2
- 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 +20 -2
- 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/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-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/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 +79 -0
- package/dist/infra/process/process-manager.js +277 -3
- 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 +51 -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 +3 -2
- package/dist/infra/repl/commands/init-command.js +11 -7
- package/dist/infra/repl/commands/query-command.js +22 -2
- package/dist/infra/repl/commands/reset-command.js +1 -1
- 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 +27 -251
- package/dist/infra/usecase/logout-use-case.js +1 -1
- package/dist/infra/usecase/pull-use-case.js +5 -5
- package/dist/infra/usecase/push-use-case.js +4 -4
- package/dist/infra/usecase/reset-use-case.js +3 -4
- 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 +7 -0
- 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/search_knowledge.txt +32 -0
- package/dist/templates/sections/brv-instructions.md +98 -0
- package/dist/tui/components/onboarding/onboarding-flow.js +14 -11
- 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 +4 -0
- package/dist/utils/file-validator.d.ts +1 -1
- package/dist/utils/file-validator.js +25 -28
- package/dist/utils/type-guards.d.ts +5 -0
- package/dist/utils/type-guards.js +7 -0
- package/oclif.manifest.json +30 -4
- package/package.json +4 -1
- 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-generate-rules-use-case.js → connectors/connector-types.js} +0 -0
- /package/dist/infra/{rule → connectors/shared}/constants.d.ts +0 -0
- /package/dist/infra/{rule → connectors/shared}/constants.js +0 -0
|
@@ -26,8 +26,11 @@ import { fileURLToPath } from 'node:url';
|
|
|
26
26
|
import { crashLog, getSessionLogPath, processManagerLog } from '../../utils/process-logger.js';
|
|
27
27
|
const DEFAULT_SHUTDOWN_TIMEOUT_MS = 5000;
|
|
28
28
|
const DEFAULT_STARTUP_TIMEOUT_MS = 30_000;
|
|
29
|
-
const HEALTH_CHECK_INTERVAL_MS = 5000; // Check every 5 seconds
|
|
29
|
+
const HEALTH_CHECK_INTERVAL_MS = 5000; // Check every 5 seconds for sleep detection
|
|
30
30
|
const SLEEP_DETECTION_THRESHOLD_MS = 30_000; // If 30s passed when expecting 5s, likely slept
|
|
31
|
+
const TRANSPORT_PING_TIMEOUT_MS = 5000; // Timeout for Transport ping response
|
|
32
|
+
const AGENT_HEALTH_CHECK_TIMEOUT_MS = 5000; // Timeout for Agent health-check response
|
|
33
|
+
const PERIODIC_HEALTH_CHECK_INTERVAL_MS = 30_000; // Periodic health check every 30s
|
|
31
34
|
/**
|
|
32
35
|
* Creates a system error with crash log.
|
|
33
36
|
* Logs error details to session log and returns user-friendly message.
|
|
@@ -46,9 +49,21 @@ function createSystemError(error, context) {
|
|
|
46
49
|
* - Crash recovery: respawn on exit
|
|
47
50
|
*/
|
|
48
51
|
export class ProcessManager {
|
|
52
|
+
/** Whether an Agent health-check is pending response */
|
|
53
|
+
agentHealthCheckPending = false;
|
|
54
|
+
/** Timeout for Agent health-check response */
|
|
55
|
+
agentHealthCheckTimeout;
|
|
49
56
|
currentSessionId;
|
|
50
57
|
healthCheckInterval;
|
|
58
|
+
/** Guard to prevent concurrent Agent restarts */
|
|
59
|
+
isRestartingAgent = false;
|
|
60
|
+
/** Guard to prevent concurrent Transport restarts */
|
|
61
|
+
isRestartingTransport = false;
|
|
51
62
|
lastHealthCheckTime = Date.now();
|
|
63
|
+
/** Periodic health check interval (30s) */
|
|
64
|
+
periodicHealthCheckInterval;
|
|
65
|
+
/** Stored handler ref for idempotent listener setup (prevents accumulation on respawn) */
|
|
66
|
+
runtimeMessageHandler;
|
|
52
67
|
shutdownTimeoutMs;
|
|
53
68
|
startupTimeoutMs;
|
|
54
69
|
state = {
|
|
@@ -56,6 +71,12 @@ export class ProcessManager {
|
|
|
56
71
|
running: false,
|
|
57
72
|
transportReady: false,
|
|
58
73
|
};
|
|
74
|
+
/** Stored handler ref for Transport runtime messages (prevents accumulation on respawn) */
|
|
75
|
+
transportMessageHandler;
|
|
76
|
+
/** Whether a Transport ping is pending response */
|
|
77
|
+
transportPingPending = false;
|
|
78
|
+
/** Timeout for Transport ping response */
|
|
79
|
+
transportPingTimeout;
|
|
59
80
|
constructor(config) {
|
|
60
81
|
this.startupTimeoutMs = config?.startupTimeoutMs ?? DEFAULT_STARTUP_TIMEOUT_MS;
|
|
61
82
|
this.shutdownTimeoutMs = config?.shutdownTimeoutMs ?? DEFAULT_SHUTDOWN_TIMEOUT_MS;
|
|
@@ -106,6 +127,8 @@ export class ProcessManager {
|
|
|
106
127
|
this.state.running = true;
|
|
107
128
|
// Step 3: Start health check for sleep/wake detection
|
|
108
129
|
this.startHealthCheck();
|
|
130
|
+
// Step 4: Start periodic health check (30s) for zombie detection mid-session
|
|
131
|
+
this.startPeriodicHealthCheck();
|
|
109
132
|
}
|
|
110
133
|
/**
|
|
111
134
|
* Stop all processes gracefully.
|
|
@@ -119,8 +142,9 @@ export class ProcessManager {
|
|
|
119
142
|
return;
|
|
120
143
|
}
|
|
121
144
|
this.state.running = false;
|
|
122
|
-
// Stop health
|
|
145
|
+
// Stop health checks first
|
|
123
146
|
this.stopHealthCheck();
|
|
147
|
+
this.stopPeriodicHealthCheck();
|
|
124
148
|
// Step 1: Stop Agent Process
|
|
125
149
|
await this.stopAgentProcess();
|
|
126
150
|
this.state.agentReady = false;
|
|
@@ -141,9 +165,34 @@ export class ProcessManager {
|
|
|
141
165
|
}
|
|
142
166
|
return currentDir;
|
|
143
167
|
}
|
|
168
|
+
/**
|
|
169
|
+
* Handle runtime IPC messages from Agent process.
|
|
170
|
+
* Called by the message event handler set up in setupAgentRuntimeHandlers.
|
|
171
|
+
*/
|
|
172
|
+
handleAgentRuntimeMessage(message) {
|
|
173
|
+
if (message.type === 'health-check-result') {
|
|
174
|
+
// Clear pending flag and timeout (for periodic health check)
|
|
175
|
+
if (this.agentHealthCheckTimeout) {
|
|
176
|
+
clearTimeout(this.agentHealthCheckTimeout);
|
|
177
|
+
this.agentHealthCheckTimeout = undefined;
|
|
178
|
+
}
|
|
179
|
+
this.agentHealthCheckPending = false;
|
|
180
|
+
if (message.success) {
|
|
181
|
+
processManagerLog('Agent health-check passed');
|
|
182
|
+
}
|
|
183
|
+
else {
|
|
184
|
+
processManagerLog('Agent health-check FAILED - Socket.IO connection stale, restarting agent');
|
|
185
|
+
// Restart agent to force reconnection
|
|
186
|
+
this.restartAgent().catch((error) => {
|
|
187
|
+
processManagerLog(`Failed to restart agent after health-check failure: ${error}`);
|
|
188
|
+
});
|
|
189
|
+
}
|
|
190
|
+
}
|
|
191
|
+
}
|
|
144
192
|
/**
|
|
145
193
|
* Handle system wake from sleep.
|
|
146
194
|
* Verify processes are still alive and restart if needed.
|
|
195
|
+
* Triggers immediate health checks on both Transport and Agent.
|
|
147
196
|
*/
|
|
148
197
|
handleSystemWake() {
|
|
149
198
|
const { agentProcess, transportProcess } = this.state;
|
|
@@ -165,7 +214,139 @@ export class ProcessManager {
|
|
|
165
214
|
}
|
|
166
215
|
}
|
|
167
216
|
else {
|
|
168
|
-
processManagerLog('Processes healthy after wake');
|
|
217
|
+
processManagerLog('Processes healthy after wake - triggering immediate health check');
|
|
218
|
+
// Trigger immediate health checks (reuse the timeout-based methods)
|
|
219
|
+
// Check Transport (if not already pinging)
|
|
220
|
+
if (this.state.transportReady && !this.transportPingPending) {
|
|
221
|
+
this.pingTransportWithTimeout();
|
|
222
|
+
}
|
|
223
|
+
// Check Agent (if not already checking)
|
|
224
|
+
if (this.state.agentReady && !this.agentHealthCheckPending) {
|
|
225
|
+
this.healthCheckAgentWithTimeout();
|
|
226
|
+
}
|
|
227
|
+
}
|
|
228
|
+
}
|
|
229
|
+
/**
|
|
230
|
+
* Handle runtime IPC messages from Transport process.
|
|
231
|
+
* Called by the message event handler set up in setupTransportRuntimeHandlers.
|
|
232
|
+
*/
|
|
233
|
+
handleTransportRuntimeMessage(message) {
|
|
234
|
+
if (message.type === 'pong' && this.transportPingPending) {
|
|
235
|
+
// Clear timeout and flag
|
|
236
|
+
if (this.transportPingTimeout) {
|
|
237
|
+
clearTimeout(this.transportPingTimeout);
|
|
238
|
+
this.transportPingTimeout = undefined;
|
|
239
|
+
}
|
|
240
|
+
this.transportPingPending = false;
|
|
241
|
+
processManagerLog('Transport ping successful - process healthy');
|
|
242
|
+
}
|
|
243
|
+
}
|
|
244
|
+
/**
|
|
245
|
+
* Send health-check to Agent with timeout.
|
|
246
|
+
* If no response within timeout, Agent is considered stuck and restarted.
|
|
247
|
+
* Used by periodic health check and after system wake.
|
|
248
|
+
*/
|
|
249
|
+
healthCheckAgentWithTimeout() {
|
|
250
|
+
const { agentProcess } = this.state;
|
|
251
|
+
if (!agentProcess || this.agentHealthCheckPending)
|
|
252
|
+
return;
|
|
253
|
+
this.agentHealthCheckPending = true;
|
|
254
|
+
processManagerLog('Sending health-check to agent');
|
|
255
|
+
this.sendToChild(agentProcess, { type: 'health-check' });
|
|
256
|
+
this.agentHealthCheckTimeout = setTimeout(() => {
|
|
257
|
+
if (this.agentHealthCheckPending) {
|
|
258
|
+
processManagerLog('Agent health-check timeout - process may be stuck, restarting');
|
|
259
|
+
this.agentHealthCheckPending = false;
|
|
260
|
+
this.agentHealthCheckTimeout = undefined;
|
|
261
|
+
this.restartAgent().catch((error) => {
|
|
262
|
+
processManagerLog(`Failed to restart agent after health-check timeout: ${error}`);
|
|
263
|
+
});
|
|
264
|
+
}
|
|
265
|
+
}, AGENT_HEALTH_CHECK_TIMEOUT_MS);
|
|
266
|
+
// Don't block process exit
|
|
267
|
+
this.agentHealthCheckTimeout.unref();
|
|
268
|
+
}
|
|
269
|
+
/**
|
|
270
|
+
* Send ping to Transport with timeout.
|
|
271
|
+
* If no pong received within timeout, Transport is considered zombie and restarted.
|
|
272
|
+
*/
|
|
273
|
+
pingTransportWithTimeout() {
|
|
274
|
+
const { transportProcess } = this.state;
|
|
275
|
+
if (!transportProcess || this.transportPingPending)
|
|
276
|
+
return;
|
|
277
|
+
this.transportPingPending = true;
|
|
278
|
+
processManagerLog('Sending ping to transport after wake');
|
|
279
|
+
this.sendToChild(transportProcess, { type: 'ping' });
|
|
280
|
+
this.transportPingTimeout = setTimeout(() => {
|
|
281
|
+
if (this.transportPingPending) {
|
|
282
|
+
processManagerLog('Transport ping timeout - process may be zombie, restarting');
|
|
283
|
+
this.transportPingPending = false;
|
|
284
|
+
this.transportPingTimeout = undefined;
|
|
285
|
+
this.restartTransport().catch((error) => {
|
|
286
|
+
processManagerLog(`Failed to restart transport after ping timeout: ${error}`);
|
|
287
|
+
});
|
|
288
|
+
}
|
|
289
|
+
}, TRANSPORT_PING_TIMEOUT_MS);
|
|
290
|
+
// Don't block process exit
|
|
291
|
+
this.transportPingTimeout.unref();
|
|
292
|
+
}
|
|
293
|
+
/**
|
|
294
|
+
* Restart Agent process gracefully.
|
|
295
|
+
* Used when health-check fails after sleep/wake or periodic check.
|
|
296
|
+
* Guarded to prevent concurrent restart race conditions.
|
|
297
|
+
*/
|
|
298
|
+
async restartAgent() {
|
|
299
|
+
// Guard: prevent concurrent restarts
|
|
300
|
+
if (!this.state.running || !this.state.port || this.isRestartingAgent)
|
|
301
|
+
return;
|
|
302
|
+
this.isRestartingAgent = true;
|
|
303
|
+
try {
|
|
304
|
+
processManagerLog('Restarting agent process...');
|
|
305
|
+
// Stop existing agent
|
|
306
|
+
await this.stopAgentProcess();
|
|
307
|
+
this.state.agentReady = false;
|
|
308
|
+
// Start new agent
|
|
309
|
+
await this.startAgentProcess(this.state.port);
|
|
310
|
+
this.state.agentReady = true;
|
|
311
|
+
processManagerLog('Agent process restarted successfully');
|
|
312
|
+
}
|
|
313
|
+
finally {
|
|
314
|
+
this.isRestartingAgent = false;
|
|
315
|
+
}
|
|
316
|
+
}
|
|
317
|
+
/**
|
|
318
|
+
* Restart Transport process gracefully.
|
|
319
|
+
* Used when ping timeout detects zombie process after sleep/wake or periodic check.
|
|
320
|
+
* Note: Agent must also be restarted since Transport port changes.
|
|
321
|
+
* Guarded to prevent concurrent restart race conditions.
|
|
322
|
+
*/
|
|
323
|
+
async restartTransport() {
|
|
324
|
+
// Guard: prevent concurrent restarts
|
|
325
|
+
if (!this.state.running || this.isRestartingTransport)
|
|
326
|
+
return;
|
|
327
|
+
this.isRestartingTransport = true;
|
|
328
|
+
try {
|
|
329
|
+
processManagerLog('Restarting transport process...');
|
|
330
|
+
// Stop existing transport
|
|
331
|
+
await this.stopTransportProcess();
|
|
332
|
+
this.state.transportReady = false;
|
|
333
|
+
// Start new transport (gets new port)
|
|
334
|
+
const newPort = await this.startTransportProcess();
|
|
335
|
+
this.state.port = newPort;
|
|
336
|
+
this.state.transportReady = true;
|
|
337
|
+
processManagerLog(`Transport process restarted on port ${newPort}`);
|
|
338
|
+
// Agent needs to reconnect to new port - restart Agent too
|
|
339
|
+
if (this.state.agentProcess) {
|
|
340
|
+
await this.stopAgentProcess();
|
|
341
|
+
this.state.agentReady = false;
|
|
342
|
+
await this.startAgentProcess(newPort);
|
|
343
|
+
this.state.agentReady = true;
|
|
344
|
+
processManagerLog('Agent reconnected to new Transport');
|
|
345
|
+
}
|
|
346
|
+
processManagerLog('Transport process restarted successfully');
|
|
347
|
+
}
|
|
348
|
+
finally {
|
|
349
|
+
this.isRestartingTransport = false;
|
|
169
350
|
}
|
|
170
351
|
}
|
|
171
352
|
/**
|
|
@@ -199,6 +380,29 @@ export class ProcessManager {
|
|
|
199
380
|
});
|
|
200
381
|
}
|
|
201
382
|
});
|
|
383
|
+
// Setup runtime message handler for health-check results
|
|
384
|
+
this.setupAgentRuntimeHandlers();
|
|
385
|
+
}
|
|
386
|
+
/**
|
|
387
|
+
* Setup runtime message handlers for Agent process.
|
|
388
|
+
* Handles IPC messages that arrive during normal operation (not just startup).
|
|
389
|
+
*
|
|
390
|
+
* IMPORTANT: Uses stored handler reference to prevent listener accumulation.
|
|
391
|
+
* Each respawn calls this method, so we must remove the old listener first.
|
|
392
|
+
*/
|
|
393
|
+
setupAgentRuntimeHandlers() {
|
|
394
|
+
const { agentProcess } = this.state;
|
|
395
|
+
if (!agentProcess)
|
|
396
|
+
return;
|
|
397
|
+
// Remove old listener FIRST (prevents accumulation on respawn)
|
|
398
|
+
if (this.runtimeMessageHandler) {
|
|
399
|
+
agentProcess.off('message', this.runtimeMessageHandler);
|
|
400
|
+
}
|
|
401
|
+
// Create and store new handler
|
|
402
|
+
this.runtimeMessageHandler = (message) => {
|
|
403
|
+
this.handleAgentRuntimeMessage(message);
|
|
404
|
+
};
|
|
405
|
+
agentProcess.on('message', this.runtimeMessageHandler);
|
|
202
406
|
}
|
|
203
407
|
/**
|
|
204
408
|
* Setup Transport crash recovery.
|
|
@@ -232,6 +436,29 @@ export class ProcessManager {
|
|
|
232
436
|
processManagerLog(`Failed to respawn Transport: ${error}`);
|
|
233
437
|
});
|
|
234
438
|
});
|
|
439
|
+
// Setup runtime message handler for ping/pong health checks
|
|
440
|
+
this.setupTransportRuntimeHandlers();
|
|
441
|
+
}
|
|
442
|
+
/**
|
|
443
|
+
* Setup runtime message handlers for Transport process.
|
|
444
|
+
* Handles IPC messages that arrive during normal operation (pong for health check).
|
|
445
|
+
*
|
|
446
|
+
* IMPORTANT: Uses stored handler reference to prevent listener accumulation.
|
|
447
|
+
* Each respawn calls this method, so we must remove the old listener first.
|
|
448
|
+
*/
|
|
449
|
+
setupTransportRuntimeHandlers() {
|
|
450
|
+
const { transportProcess } = this.state;
|
|
451
|
+
if (!transportProcess)
|
|
452
|
+
return;
|
|
453
|
+
// Remove old listener FIRST (prevents accumulation on respawn)
|
|
454
|
+
if (this.transportMessageHandler) {
|
|
455
|
+
transportProcess.off('message', this.transportMessageHandler);
|
|
456
|
+
}
|
|
457
|
+
// Create and store new handler
|
|
458
|
+
this.transportMessageHandler = (message) => {
|
|
459
|
+
this.handleTransportRuntimeMessage(message);
|
|
460
|
+
};
|
|
461
|
+
transportProcess.on('message', this.transportMessageHandler);
|
|
235
462
|
}
|
|
236
463
|
/**
|
|
237
464
|
* Start Agent Process.
|
|
@@ -312,6 +539,27 @@ export class ProcessManager {
|
|
|
312
539
|
// Don't prevent process exit
|
|
313
540
|
this.healthCheckInterval.unref();
|
|
314
541
|
}
|
|
542
|
+
/**
|
|
543
|
+
* Start periodic health check for Transport and Agent.
|
|
544
|
+
* Runs every 30s to detect zombie processes mid-session (not just after wake).
|
|
545
|
+
*/
|
|
546
|
+
startPeriodicHealthCheck() {
|
|
547
|
+
this.periodicHealthCheckInterval = setInterval(() => {
|
|
548
|
+
if (!this.state.running)
|
|
549
|
+
return;
|
|
550
|
+
// Check Transport (if ready and not already pinging)
|
|
551
|
+
if (this.state.transportReady && !this.transportPingPending) {
|
|
552
|
+
this.pingTransportWithTimeout();
|
|
553
|
+
}
|
|
554
|
+
// Check Agent (if ready and not already checking)
|
|
555
|
+
if (this.state.agentReady && !this.agentHealthCheckPending) {
|
|
556
|
+
this.healthCheckAgentWithTimeout();
|
|
557
|
+
}
|
|
558
|
+
}, PERIODIC_HEALTH_CHECK_INTERVAL_MS);
|
|
559
|
+
// Don't prevent process exit
|
|
560
|
+
this.periodicHealthCheckInterval.unref();
|
|
561
|
+
processManagerLog('Periodic health check started (30s interval)');
|
|
562
|
+
}
|
|
315
563
|
/**
|
|
316
564
|
* Start Transport Process.
|
|
317
565
|
* @returns The port Transport is listening on
|
|
@@ -385,6 +633,8 @@ export class ProcessManager {
|
|
|
385
633
|
clearTimeout(timeout);
|
|
386
634
|
agentProcess.off('message', onMessage);
|
|
387
635
|
agentProcess.off('exit', onExit);
|
|
636
|
+
// Clear stored handler reference (prevents stale refs on respawn)
|
|
637
|
+
this.runtimeMessageHandler = undefined;
|
|
388
638
|
};
|
|
389
639
|
const timeout = setTimeout(() => {
|
|
390
640
|
cleanup();
|
|
@@ -419,6 +669,22 @@ export class ProcessManager {
|
|
|
419
669
|
this.healthCheckInterval = undefined;
|
|
420
670
|
}
|
|
421
671
|
}
|
|
672
|
+
/**
|
|
673
|
+
* Stop periodic health check interval.
|
|
674
|
+
* Also clears any pending agent health check timeout.
|
|
675
|
+
*/
|
|
676
|
+
stopPeriodicHealthCheck() {
|
|
677
|
+
if (this.periodicHealthCheckInterval) {
|
|
678
|
+
clearInterval(this.periodicHealthCheckInterval);
|
|
679
|
+
this.periodicHealthCheckInterval = undefined;
|
|
680
|
+
}
|
|
681
|
+
// Clear any pending agent health check
|
|
682
|
+
if (this.agentHealthCheckTimeout) {
|
|
683
|
+
clearTimeout(this.agentHealthCheckTimeout);
|
|
684
|
+
this.agentHealthCheckTimeout = undefined;
|
|
685
|
+
}
|
|
686
|
+
this.agentHealthCheckPending = false;
|
|
687
|
+
}
|
|
422
688
|
/**
|
|
423
689
|
* Stop Transport Process.
|
|
424
690
|
*/
|
|
@@ -431,6 +697,14 @@ export class ProcessManager {
|
|
|
431
697
|
clearTimeout(timeout);
|
|
432
698
|
transportProcess.off('message', onMessage);
|
|
433
699
|
transportProcess.off('exit', onExit);
|
|
700
|
+
// Clear stored handler reference (prevents stale refs on respawn)
|
|
701
|
+
this.transportMessageHandler = undefined;
|
|
702
|
+
// Clear ping state
|
|
703
|
+
if (this.transportPingTimeout) {
|
|
704
|
+
clearTimeout(this.transportPingTimeout);
|
|
705
|
+
this.transportPingTimeout = undefined;
|
|
706
|
+
}
|
|
707
|
+
this.transportPingPending = false;
|
|
434
708
|
};
|
|
435
709
|
const timeout = setTimeout(() => {
|
|
436
710
|
cleanup();
|
|
@@ -81,14 +81,27 @@ export declare class TaskQueueManager {
|
|
|
81
81
|
* Returns success with queue position, or failure reason.
|
|
82
82
|
*/
|
|
83
83
|
enqueue(task: TaskExecute): EnqueueResult;
|
|
84
|
+
/**
|
|
85
|
+
* Get total active task count across all queues.
|
|
86
|
+
*/
|
|
87
|
+
getActiveCount(): number;
|
|
84
88
|
/**
|
|
85
89
|
* Get all queue statistics.
|
|
86
90
|
*/
|
|
87
91
|
getAllStats(): Record<TaskType, TaskQueueStats>;
|
|
92
|
+
/**
|
|
93
|
+
* Get total queued task count across all queues.
|
|
94
|
+
*/
|
|
95
|
+
getQueuedCount(): number;
|
|
88
96
|
/**
|
|
89
97
|
* Get statistics for a specific queue.
|
|
90
98
|
*/
|
|
91
99
|
getStats(type: TaskType): TaskQueueStats;
|
|
100
|
+
/**
|
|
101
|
+
* Check if there are any active tasks (currently being processed).
|
|
102
|
+
* Used to prevent reinit during task execution.
|
|
103
|
+
*/
|
|
104
|
+
hasActiveTasks(): boolean;
|
|
92
105
|
/**
|
|
93
106
|
* Check if a taskId is known (queued or processing).
|
|
94
107
|
*/
|
|
@@ -91,6 +91,12 @@ export class TaskQueueManager {
|
|
|
91
91
|
this.tryProcessNext('query');
|
|
92
92
|
return { position: this.queryQueue.length, success: true };
|
|
93
93
|
}
|
|
94
|
+
/**
|
|
95
|
+
* Get total active task count across all queues.
|
|
96
|
+
*/
|
|
97
|
+
getActiveCount() {
|
|
98
|
+
return this.activeCurateTasks + this.activeQueryTasks;
|
|
99
|
+
}
|
|
94
100
|
/**
|
|
95
101
|
* Get all queue statistics.
|
|
96
102
|
*/
|
|
@@ -100,6 +106,12 @@ export class TaskQueueManager {
|
|
|
100
106
|
query: this.getStats('query'),
|
|
101
107
|
};
|
|
102
108
|
}
|
|
109
|
+
/**
|
|
110
|
+
* Get total queued task count across all queues.
|
|
111
|
+
*/
|
|
112
|
+
getQueuedCount() {
|
|
113
|
+
return this.curateQueue.length + this.queryQueue.length;
|
|
114
|
+
}
|
|
103
115
|
/**
|
|
104
116
|
* Get statistics for a specific queue.
|
|
105
117
|
*/
|
|
@@ -117,6 +129,13 @@ export class TaskQueueManager {
|
|
|
117
129
|
queued: this.queryQueue.length,
|
|
118
130
|
};
|
|
119
131
|
}
|
|
132
|
+
/**
|
|
133
|
+
* Check if there are any active tasks (currently being processed).
|
|
134
|
+
* Used to prevent reinit during task execution.
|
|
135
|
+
*/
|
|
136
|
+
hasActiveTasks() {
|
|
137
|
+
return this.activeCurateTasks > 0 || this.activeQueryTasks > 0;
|
|
138
|
+
}
|
|
120
139
|
/**
|
|
121
140
|
* Check if a taskId is known (queued or processing).
|
|
122
141
|
*/
|
|
@@ -40,6 +40,8 @@ import type { ITransportServer } from '../../core/interfaces/transport/i-transpo
|
|
|
40
40
|
export declare class TransportHandlers {
|
|
41
41
|
/** The Agent's client ID (set when Agent registers) */
|
|
42
42
|
private agentClientId;
|
|
43
|
+
/** Cached agent status from last status:changed broadcast */
|
|
44
|
+
private cachedAgentStatus;
|
|
43
45
|
/** Track active tasks */
|
|
44
46
|
private tasks;
|
|
45
47
|
/** Transport server reference */
|
|
@@ -56,6 +58,7 @@ export declare class TransportHandlers {
|
|
|
56
58
|
/**
|
|
57
59
|
* Handle Agent registration.
|
|
58
60
|
* Agent connects as Socket.IO client and sends 'agent:register'.
|
|
61
|
+
* Fix #4: Accepts optional status in payload to cache atomically with registration.
|
|
59
62
|
*/
|
|
60
63
|
private handleAgentRegister;
|
|
61
64
|
/**
|
|
@@ -30,9 +30,16 @@
|
|
|
30
30
|
* - agent:connected / agent:disconnected: Broadcast to all clients
|
|
31
31
|
* - broadcast-room: TUI joins this room to monitor all events
|
|
32
32
|
*/
|
|
33
|
-
import { AgentDisconnectedError, AgentNotAvailableError, serializeTaskError, } from '../../core/domain/errors/task-error.js';
|
|
34
|
-
import { LlmEventNames, TransportAgentEventNames, TransportLlmEventList, TransportTaskEventNames, } from '../../core/domain/transport/schemas.js';
|
|
33
|
+
import { AgentDisconnectedError, AgentNotAvailableError, AgentNotInitializedError, serializeTaskError, } from '../../core/domain/errors/task-error.js';
|
|
34
|
+
import { AgentStatusEventNames, LlmEventNames, TransportAgentEventNames, TransportLlmEventList, TransportTaskEventNames, } from '../../core/domain/transport/schemas.js';
|
|
35
35
|
import { eventLog, transportLog } from '../../utils/process-logger.js';
|
|
36
|
+
/**
|
|
37
|
+
* Type guard for valid task types.
|
|
38
|
+
* Replaces unsafe `as` assertion per CLAUDE.md standards.
|
|
39
|
+
*/
|
|
40
|
+
function isValidTaskType(type) {
|
|
41
|
+
return type === 'curate' || type === 'query';
|
|
42
|
+
}
|
|
36
43
|
// All message types are imported from core/domain/transport/schemas.ts
|
|
37
44
|
// - TaskExecute: Transport → Agent (command)
|
|
38
45
|
// - TaskStartedEvent, TaskCompletedEvent, TaskErrorEvent: Agent → Transport (task lifecycle events)
|
|
@@ -49,6 +56,8 @@ import { eventLog, transportLog } from '../../utils/process-logger.js';
|
|
|
49
56
|
export class TransportHandlers {
|
|
50
57
|
/** The Agent's client ID (set when Agent registers) */
|
|
51
58
|
agentClientId;
|
|
59
|
+
/** Cached agent status from last status:changed broadcast */
|
|
60
|
+
cachedAgentStatus;
|
|
52
61
|
/** Track active tasks */
|
|
53
62
|
tasks = new Map();
|
|
54
63
|
/** Transport server reference */
|
|
@@ -62,6 +71,7 @@ export class TransportHandlers {
|
|
|
62
71
|
cleanup() {
|
|
63
72
|
this.tasks.clear();
|
|
64
73
|
this.agentClientId = undefined;
|
|
74
|
+
this.cachedAgentStatus = undefined;
|
|
65
75
|
}
|
|
66
76
|
/**
|
|
67
77
|
* Setup all message handlers.
|
|
@@ -75,10 +85,15 @@ export class TransportHandlers {
|
|
|
75
85
|
/**
|
|
76
86
|
* Handle Agent registration.
|
|
77
87
|
* Agent connects as Socket.IO client and sends 'agent:register'.
|
|
88
|
+
* Fix #4: Accepts optional status in payload to cache atomically with registration.
|
|
78
89
|
*/
|
|
79
|
-
handleAgentRegister(clientId) {
|
|
90
|
+
handleAgentRegister(clientId, data) {
|
|
80
91
|
transportLog(`Agent registered: ${clientId}`);
|
|
81
92
|
this.agentClientId = clientId;
|
|
93
|
+
// Cache status if provided (prevents race window between register and status broadcast)
|
|
94
|
+
if (data?.status) {
|
|
95
|
+
this.cachedAgentStatus = data.status;
|
|
96
|
+
}
|
|
82
97
|
// Broadcast to all clients that Agent is online
|
|
83
98
|
this.transport.broadcast(TransportAgentEventNames.CONNECTED, {});
|
|
84
99
|
}
|
|
@@ -169,6 +184,26 @@ export class TransportHandlers {
|
|
|
169
184
|
});
|
|
170
185
|
// Forward to Agent
|
|
171
186
|
if (this.agentClientId) {
|
|
187
|
+
// Pre-task check: verify cipher is initialized before forwarding
|
|
188
|
+
// Reject if: (1) no status cached yet, OR (2) status shows not initialized
|
|
189
|
+
// This prevents race condition where task arrives before agent broadcasts initial status
|
|
190
|
+
if (!this.cachedAgentStatus || !this.cachedAgentStatus.isInitialized) {
|
|
191
|
+
transportLog(`Agent not initialized, cannot process task ${taskId}`);
|
|
192
|
+
const error = serializeTaskError(new AgentNotInitializedError(this.cachedAgentStatus?.lastError ?? 'Agent status unknown'));
|
|
193
|
+
this.transport.sendTo(clientId, TransportTaskEventNames.ERROR, { error, taskId });
|
|
194
|
+
this.transport.broadcastTo('broadcast-room', TransportTaskEventNames.ERROR, { error, taskId });
|
|
195
|
+
this.tasks.delete(taskId);
|
|
196
|
+
return { taskId };
|
|
197
|
+
}
|
|
198
|
+
// Validate task type before forwarding (type guard replaces unsafe `as` assertion)
|
|
199
|
+
if (!isValidTaskType(data.type)) {
|
|
200
|
+
transportLog(`Invalid task type: ${data.type}`);
|
|
201
|
+
const error = serializeTaskError(new Error(`Invalid task type: ${data.type}`));
|
|
202
|
+
this.transport.sendTo(clientId, TransportTaskEventNames.ERROR, { error, taskId });
|
|
203
|
+
this.transport.broadcastTo('broadcast-room', TransportTaskEventNames.ERROR, { error, taskId });
|
|
204
|
+
this.tasks.delete(taskId);
|
|
205
|
+
return { taskId };
|
|
206
|
+
}
|
|
172
207
|
const executeMsg = {
|
|
173
208
|
clientId,
|
|
174
209
|
content: data.content,
|
|
@@ -323,8 +358,9 @@ export class TransportHandlers {
|
|
|
323
358
|
*/
|
|
324
359
|
setupAgentHandlers() {
|
|
325
360
|
// Agent registration
|
|
326
|
-
|
|
327
|
-
|
|
361
|
+
// Fix #4: Accept optional status in payload for atomic caching
|
|
362
|
+
this.transport.onRequest(TransportAgentEventNames.REGISTER, (data, clientId) => {
|
|
363
|
+
this.handleAgentRegister(clientId, data);
|
|
328
364
|
return { success: true };
|
|
329
365
|
});
|
|
330
366
|
// Task lifecycle events (Transport-generated names)
|
|
@@ -344,6 +380,15 @@ export class TransportHandlers {
|
|
|
344
380
|
for (const eventName of TransportLlmEventList) {
|
|
345
381
|
this.registerLlmEvent(eventName);
|
|
346
382
|
}
|
|
383
|
+
// Agent status events
|
|
384
|
+
// agent:status:changed - Agent broadcasts status changes
|
|
385
|
+
this.transport.onRequest(AgentStatusEventNames.STATUS_CHANGED, (data) => {
|
|
386
|
+
transportLog(`Agent status changed: initialized=${data.isInitialized}, auth=${data.hasAuth}, config=${data.hasConfig}`);
|
|
387
|
+
// Cache status for pre-task check
|
|
388
|
+
this.cachedAgentStatus = data;
|
|
389
|
+
// Broadcast status change to all clients
|
|
390
|
+
this.transport.broadcast(AgentStatusEventNames.STATUS_CHANGED, data);
|
|
391
|
+
});
|
|
347
392
|
}
|
|
348
393
|
/**
|
|
349
394
|
* Setup client-related handlers.
|
|
@@ -368,6 +413,7 @@ export class TransportHandlers {
|
|
|
368
413
|
if (clientId === this.agentClientId) {
|
|
369
414
|
transportLog('Agent disconnected!');
|
|
370
415
|
this.agentClientId = undefined;
|
|
416
|
+
this.cachedAgentStatus = undefined;
|
|
371
417
|
// Broadcast to all clients
|
|
372
418
|
this.transport.broadcast(TransportAgentEventNames.DISCONNECTED, {});
|
|
373
419
|
// Fail all pending tasks - send to client AND broadcast-room for TUI monitoring
|
|
@@ -24,6 +24,7 @@ import { transportLog } from '../../utils/process-logger.js';
|
|
|
24
24
|
import { FileInstanceManager } from '../instance/file-instance-manager.js';
|
|
25
25
|
import { findAvailablePort } from '../transport/port-utils.js';
|
|
26
26
|
import { createTransportServer } from '../transport/transport-factory.js';
|
|
27
|
+
import { createParentHeartbeat } from './parent-heartbeat.js';
|
|
27
28
|
import { TransportHandlers } from './transport-handlers.js';
|
|
28
29
|
// IPC types imported from ./ipc-types.ts
|
|
29
30
|
function sendToParent(message) {
|
|
@@ -35,13 +36,10 @@ function sendToParent(message) {
|
|
|
35
36
|
let transportServer;
|
|
36
37
|
let transportHandlers;
|
|
37
38
|
let instancePollingInterval;
|
|
38
|
-
let
|
|
39
|
-
let parentPid;
|
|
39
|
+
let parentHeartbeat;
|
|
40
40
|
const instanceManager = new FileInstanceManager();
|
|
41
41
|
/** Polling interval in milliseconds */
|
|
42
42
|
const INSTANCE_POLLING_INTERVAL_MS = 2000;
|
|
43
|
-
/** Parent heartbeat check interval in milliseconds */
|
|
44
|
-
const PARENT_HEARTBEAT_INTERVAL_MS = 2000;
|
|
45
43
|
/**
|
|
46
44
|
* Setup polling to detect instance.json deletion and recreate it.
|
|
47
45
|
*
|
|
@@ -83,69 +81,6 @@ function stopInstancePolling() {
|
|
|
83
81
|
instancePollingInterval = undefined;
|
|
84
82
|
}
|
|
85
83
|
}
|
|
86
|
-
/**
|
|
87
|
-
* Setup parent process heartbeat monitoring.
|
|
88
|
-
*
|
|
89
|
-
* Why this is needed:
|
|
90
|
-
* - When main process receives SIGKILL, it dies immediately
|
|
91
|
-
* - SIGKILL cannot be caught, so no cleanup happens
|
|
92
|
-
* - IPC 'disconnect' event may not fire
|
|
93
|
-
* - Child processes become orphans (PPID = 1)
|
|
94
|
-
*
|
|
95
|
-
* This function periodically checks if parent is still alive.
|
|
96
|
-
* If parent dies, child self-terminates to prevent zombie processes.
|
|
97
|
-
*/
|
|
98
|
-
function setupParentHeartbeat() {
|
|
99
|
-
// Already running - don't start another
|
|
100
|
-
if (parentHeartbeatRunning)
|
|
101
|
-
return;
|
|
102
|
-
parentHeartbeatRunning = true;
|
|
103
|
-
parentPid = process.ppid;
|
|
104
|
-
/**
|
|
105
|
-
* Recursive setTimeout pattern - safer than setInterval:
|
|
106
|
-
* - No callback overlap possible
|
|
107
|
-
* - Clean cancellation (just set flag = false)
|
|
108
|
-
* - No orphan timers
|
|
109
|
-
*/
|
|
110
|
-
const checkParent = () => {
|
|
111
|
-
// Stopped - don't schedule next check
|
|
112
|
-
if (!parentHeartbeatRunning || !parentPid)
|
|
113
|
-
return;
|
|
114
|
-
// Check if parent is still alive using signal 0
|
|
115
|
-
// Signal 0 doesn't send any signal, just checks if process exists
|
|
116
|
-
try {
|
|
117
|
-
process.kill(parentPid, 0);
|
|
118
|
-
}
|
|
119
|
-
catch {
|
|
120
|
-
// Parent is dead - self-terminate
|
|
121
|
-
transportLog(`Parent process (${parentPid}) died - shutting down to prevent zombie`);
|
|
122
|
-
parentHeartbeatRunning = false;
|
|
123
|
-
stopInstancePolling();
|
|
124
|
-
// Release instance lock and exit
|
|
125
|
-
stopTransport()
|
|
126
|
-
.catch(() => { })
|
|
127
|
-
.finally(() => {
|
|
128
|
-
// eslint-disable-next-line n/no-process-exit, unicorn/no-process-exit
|
|
129
|
-
process.exit(0);
|
|
130
|
-
});
|
|
131
|
-
return;
|
|
132
|
-
}
|
|
133
|
-
// Schedule next check (only if still running)
|
|
134
|
-
if (parentHeartbeatRunning) {
|
|
135
|
-
setTimeout(checkParent, PARENT_HEARTBEAT_INTERVAL_MS);
|
|
136
|
-
}
|
|
137
|
-
};
|
|
138
|
-
// Start first check after delay
|
|
139
|
-
setTimeout(checkParent, PARENT_HEARTBEAT_INTERVAL_MS);
|
|
140
|
-
transportLog(`Parent heartbeat monitoring started (PPID: ${parentPid})`);
|
|
141
|
-
}
|
|
142
|
-
/**
|
|
143
|
-
* Stop the parent heartbeat monitoring.
|
|
144
|
-
* With recursive setTimeout, just set flag to false - next check won't schedule.
|
|
145
|
-
*/
|
|
146
|
-
function stopParentHeartbeat() {
|
|
147
|
-
parentHeartbeatRunning = false;
|
|
148
|
-
}
|
|
149
84
|
async function startTransport() {
|
|
150
85
|
// Create Socket.IO server
|
|
151
86
|
transportServer = createTransportServer();
|
|
@@ -171,7 +106,7 @@ async function startTransport() {
|
|
|
171
106
|
}
|
|
172
107
|
async function stopTransport() {
|
|
173
108
|
// Stop heartbeat and polling first
|
|
174
|
-
|
|
109
|
+
parentHeartbeat?.stop();
|
|
175
110
|
stopInstancePolling();
|
|
176
111
|
// Release instance.json
|
|
177
112
|
const projectRoot = process.cwd();
|
|
@@ -195,7 +130,12 @@ async function runWorker() {
|
|
|
195
130
|
sendToParent({ port, type: 'ready' });
|
|
196
131
|
// Start parent heartbeat monitoring after ready
|
|
197
132
|
// This ensures we self-terminate if parent dies (SIGKILL scenario)
|
|
198
|
-
|
|
133
|
+
parentHeartbeat = createParentHeartbeat({
|
|
134
|
+
cleanup: stopTransport,
|
|
135
|
+
log: transportLog,
|
|
136
|
+
preCleanup: stopInstancePolling,
|
|
137
|
+
});
|
|
138
|
+
parentHeartbeat.start();
|
|
199
139
|
}
|
|
200
140
|
catch (error) {
|
|
201
141
|
const message = error instanceof Error ? error.message : String(error);
|