byterover-cli 1.0.5 → 1.2.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 +19 -13
- package/dist/commands/hook-prompt-submit.d.ts +27 -0
- package/dist/commands/hook-prompt-submit.js +39 -0
- package/dist/commands/mcp.d.ts +13 -0
- package/dist/commands/mcp.js +61 -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/agent-events/types.d.ts +44 -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 +78 -0
- package/dist/core/domain/entities/connector-type.d.ts +10 -0
- package/dist/core/domain/entities/connector-type.js +9 -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 +70 -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/i-mcp-config-writer.d.ts +40 -0
- package/dist/core/interfaces/i-mcp-config-writer.js +1 -0
- package/dist/core/interfaces/i-rule-template-service.d.ts +4 -2
- package/dist/core/interfaces/transport/i-transport-client.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/agent/cipher-agent.d.ts +8 -0
- package/dist/infra/cipher/agent/cipher-agent.js +16 -0
- 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 +10 -2
- package/dist/infra/cipher/llm/context/context-manager.js +39 -2
- package/dist/infra/cipher/llm/formatters/gemini-formatter.js +48 -9
- package/dist/infra/cipher/llm/internal-llm-service.d.ts +4 -0
- package/dist/infra/cipher/llm/internal-llm-service.js +40 -12
- package/dist/infra/cipher/session/chat-session.d.ts +3 -0
- package/dist/infra/cipher/session/chat-session.js +7 -1
- 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.d.ts +1 -8
- package/dist/infra/cipher/tools/implementations/curate-tool.js +380 -24
- 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 +158 -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/connectors/mcp/index.d.ts +4 -0
- package/dist/infra/connectors/mcp/index.js +4 -0
- package/dist/infra/connectors/mcp/json-mcp-config-writer.d.ts +26 -0
- package/dist/infra/connectors/mcp/json-mcp-config-writer.js +71 -0
- package/dist/infra/connectors/mcp/mcp-connector-config.d.ts +229 -0
- package/dist/infra/connectors/mcp/mcp-connector-config.js +173 -0
- package/dist/infra/connectors/mcp/mcp-connector.d.ts +80 -0
- package/dist/infra/connectors/mcp/mcp-connector.js +324 -0
- package/dist/infra/connectors/mcp/toml-mcp-config-writer.d.ts +45 -0
- package/dist/infra/connectors/mcp/toml-mcp-config-writer.js +134 -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 +34 -0
- package/dist/infra/connectors/rules/rules-connector.js +139 -0
- package/dist/infra/connectors/shared/rule-file-manager.d.ts +72 -0
- package/dist/infra/connectors/shared/rule-file-manager.js +119 -0
- package/dist/infra/connectors/shared/template-service.d.ts +27 -0
- package/dist/infra/connectors/shared/template-service.js +125 -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/file/fs-file-service.d.ts +7 -0
- package/dist/infra/file/fs-file-service.js +15 -1
- package/dist/infra/mcp/index.d.ts +2 -0
- package/dist/infra/mcp/index.js +2 -0
- package/dist/infra/mcp/mcp-server.d.ts +58 -0
- package/dist/infra/mcp/mcp-server.js +178 -0
- package/dist/infra/mcp/tools/brv-curate-tool.d.ts +23 -0
- package/dist/infra/mcp/tools/brv-curate-tool.js +68 -0
- package/dist/infra/mcp/tools/brv-query-tool.d.ts +17 -0
- package/dist/infra/mcp/tools/brv-query-tool.js +68 -0
- package/dist/infra/mcp/tools/index.d.ts +3 -0
- package/dist/infra/mcp/tools/index.js +3 -0
- package/dist/infra/mcp/tools/task-result-waiter.d.ts +30 -0
- package/dist/infra/mcp/tools/task-result-waiter.js +56 -0
- package/dist/infra/process/agent-worker.d.ts +2 -2
- package/dist/infra/process/agent-worker.js +663 -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/curate-command.js +2 -2
- 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 +75 -0
- package/dist/infra/transport/socket-io-transport-client.js +308 -7
- package/dist/infra/transport/socket-io-transport-server.js +4 -0
- package/dist/infra/usecase/connectors-use-case.d.ts +63 -0
- package/dist/infra/usecase/connectors-use-case.js +222 -0
- package/dist/infra/usecase/init-use-case.d.ts +8 -43
- package/dist/infra/usecase/init-use-case.js +27 -252
- 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/infra/usecase/status-use-case.d.ts +10 -0
- package/dist/infra/usecase/status-use-case.js +53 -0
- package/dist/resources/prompts/curate.yml +114 -4
- 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/mcp-base.md +1 -0
- package/dist/templates/sections/brv-instructions.md +98 -0
- package/dist/templates/sections/mcp-workflow.md +13 -0
- package/dist/tui/app.js +4 -1
- package/dist/tui/components/command-details.js +1 -1
- package/dist/tui/components/execution/execution-changes.d.ts +2 -0
- package/dist/tui/components/execution/execution-changes.js +5 -1
- package/dist/tui/components/execution/execution-content.d.ts +2 -0
- package/dist/tui/components/execution/execution-content.js +8 -18
- package/dist/tui/components/execution/execution-input.d.ts +2 -0
- package/dist/tui/components/execution/execution-input.js +6 -4
- package/dist/tui/components/execution/execution-progress.d.ts +2 -0
- package/dist/tui/components/execution/execution-progress.js +6 -2
- package/dist/tui/components/execution/expanded-log-view.d.ts +20 -0
- package/dist/tui/components/execution/expanded-log-view.js +75 -0
- package/dist/tui/components/execution/expanded-message-view.d.ts +24 -0
- package/dist/tui/components/execution/expanded-message-view.js +68 -0
- package/dist/tui/components/execution/index.d.ts +2 -0
- package/dist/tui/components/execution/index.js +2 -0
- package/dist/tui/components/execution/log-item.d.ts +4 -0
- package/dist/tui/components/execution/log-item.js +2 -2
- package/dist/tui/components/footer.js +1 -1
- package/dist/tui/components/index.d.ts +2 -1
- package/dist/tui/components/index.js +2 -1
- package/dist/tui/components/init.js +2 -9
- package/dist/tui/components/logo.js +4 -3
- package/dist/tui/components/markdown.d.ts +13 -0
- package/dist/tui/components/markdown.js +88 -0
- package/dist/tui/components/message-item.js +1 -1
- package/dist/tui/components/onboarding/onboarding-flow.js +14 -11
- package/dist/tui/components/onboarding/welcome-box.js +1 -1
- package/dist/tui/components/suggestions.js +3 -3
- package/dist/tui/contexts/mode-context.js +6 -2
- package/dist/tui/contexts/onboarding-context.d.ts +4 -0
- package/dist/tui/contexts/onboarding-context.js +14 -2
- package/dist/tui/hooks/index.d.ts +1 -0
- package/dist/tui/hooks/index.js +1 -0
- package/dist/tui/hooks/use-is-latest-version.d.ts +6 -0
- package/dist/tui/hooks/use-is-latest-version.js +22 -0
- package/dist/tui/views/command-view.d.ts +1 -1
- package/dist/tui/views/command-view.js +87 -98
- package/dist/tui/views/logs-view.d.ts +8 -0
- package/dist/tui/views/logs-view.js +55 -27
- 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 +55 -4
- package/package.json +12 -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/rule/rule-template-service.d.ts +0 -18
- package/dist/infra/rule/rule-template-service.js +0 -88
- 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
|
@@ -9,8 +9,8 @@
|
|
|
9
9
|
* - NO Socket.IO server (Transport is the only server)
|
|
10
10
|
*
|
|
11
11
|
* IPC messages:
|
|
12
|
-
* - Receives: 'ping', 'shutdown'
|
|
13
|
-
* - Sends: 'ready', 'pong', 'stopped', 'error'
|
|
12
|
+
* - Receives: 'ping', 'shutdown', 'health-check'
|
|
13
|
+
* - Sends: 'ready', 'pong', 'stopped', 'error', 'health-check-result'
|
|
14
14
|
*
|
|
15
15
|
* Socket.IO events (as client):
|
|
16
16
|
* - Sends: 'agent:register' (identify as Agent)
|
|
@@ -20,7 +20,7 @@
|
|
|
20
20
|
import { randomUUID } from 'node:crypto';
|
|
21
21
|
import { getCurrentConfig } from '../../config/environment.js';
|
|
22
22
|
import { DEFAULT_LLM_MODEL, PROJECT } from '../../constants.js';
|
|
23
|
-
import { NotAuthenticatedError, ProcessorNotInitError, serializeTaskError } from '../../core/domain/errors/task-error.js';
|
|
23
|
+
import { AgentNotInitializedError, NotAuthenticatedError, ProcessorNotInitError, serializeTaskError, } from '../../core/domain/errors/task-error.js';
|
|
24
24
|
import { agentLog } from '../../utils/process-logger.js';
|
|
25
25
|
import { CipherAgent } from '../cipher/agent/index.js';
|
|
26
26
|
import { ProjectConfigStore } from '../config/file-config-store.js';
|
|
@@ -30,6 +30,7 @@ import { createTaskProcessor } from '../core/task-processor.js';
|
|
|
30
30
|
import { createTokenStore } from '../storage/token-store.js';
|
|
31
31
|
import { createTransportClient } from '../transport/transport-factory.js';
|
|
32
32
|
import { CURATE_MAX_CONCURRENT } from './constants.js';
|
|
33
|
+
import { createParentHeartbeat } from './parent-heartbeat.js';
|
|
33
34
|
import { TaskQueueManager } from './task-queue-manager.js';
|
|
34
35
|
// IPC types imported from ./ipc-types.ts
|
|
35
36
|
function sendToParent(message) {
|
|
@@ -63,12 +64,40 @@ let initializationError;
|
|
|
63
64
|
let isInitializing = false;
|
|
64
65
|
/** Guard: prevent double cleanup */
|
|
65
66
|
let isCleaningUp = false;
|
|
66
|
-
/** Parent
|
|
67
|
-
let
|
|
68
|
-
|
|
69
|
-
|
|
70
|
-
|
|
71
|
-
|
|
67
|
+
/** Parent heartbeat monitor - lazily initialized in runWorker() */
|
|
68
|
+
let parentHeartbeat;
|
|
69
|
+
// ============================================================================
|
|
70
|
+
// Credentials Polling (detects auth/config changes)
|
|
71
|
+
// ============================================================================
|
|
72
|
+
/**
|
|
73
|
+
* Credentials polling interval in milliseconds.
|
|
74
|
+
*
|
|
75
|
+
* 5 seconds balances:
|
|
76
|
+
* - Responsiveness: User expects agent to react within ~5s after login/logout/space switch
|
|
77
|
+
* - Efficiency: Polling every 5s has minimal CPU/IO overhead (reads 2 small files)
|
|
78
|
+
* - UX: Faster than explicit restart, acceptable latency for credential changes
|
|
79
|
+
*/
|
|
80
|
+
const CREDENTIALS_POLL_INTERVAL_MS = 5000;
|
|
81
|
+
/** Current cached credentials (set after successful init) */
|
|
82
|
+
let cachedCredentials;
|
|
83
|
+
/** Guard: prevent concurrent polling checks */
|
|
84
|
+
let isPolling = false;
|
|
85
|
+
/** Credentials polling running flag */
|
|
86
|
+
let credentialsPollingRunning = false;
|
|
87
|
+
/** Guard: prevent task enqueueing during reinit (fixes TOCTOU race condition) */
|
|
88
|
+
let isReinitializing = false;
|
|
89
|
+
/**
|
|
90
|
+
* Lazy-initialized stores for credentials polling.
|
|
91
|
+
* Avoids side effects at import time (file system access).
|
|
92
|
+
* Created once on first poll, then reused (avoid creating new instances every 5 seconds).
|
|
93
|
+
*/
|
|
94
|
+
let pollingTokenStore;
|
|
95
|
+
let pollingConfigStore;
|
|
96
|
+
function getPollingStores() {
|
|
97
|
+
pollingTokenStore ??= createTokenStore();
|
|
98
|
+
pollingConfigStore ??= new ProjectConfigStore();
|
|
99
|
+
return { pollingConfigStore, pollingTokenStore };
|
|
100
|
+
}
|
|
72
101
|
let eventForwarders = [];
|
|
73
102
|
// ============================================================================
|
|
74
103
|
// Task Queue Manager (replaces inline queue logic)
|
|
@@ -114,6 +143,7 @@ function cleanupAgentEventForwarding() {
|
|
|
114
143
|
const eventBus = cipherAgent?.agentEventBus;
|
|
115
144
|
if (eventBus) {
|
|
116
145
|
for (const { event, handler } of eventForwarders) {
|
|
146
|
+
// Uses fallback signature: off(eventName: string, listener: (data?: unknown) => void)
|
|
117
147
|
eventBus.off(event, handler);
|
|
118
148
|
}
|
|
119
149
|
}
|
|
@@ -121,6 +151,36 @@ function cleanupAgentEventForwarding() {
|
|
|
121
151
|
eventForwarders = [];
|
|
122
152
|
agentLog('Event forwarders cleaned up');
|
|
123
153
|
}
|
|
154
|
+
/**
|
|
155
|
+
* Check if there is pending work that would be disrupted by reinit.
|
|
156
|
+
* Returns true if tasks are active (running) OR queued (waiting).
|
|
157
|
+
*/
|
|
158
|
+
function hasPendingWork() {
|
|
159
|
+
return taskQueueManager.hasActiveTasks() || taskQueueManager.getQueuedCount() > 0;
|
|
160
|
+
}
|
|
161
|
+
/**
|
|
162
|
+
* Wait for active tasks to complete with a timeout.
|
|
163
|
+
* Used before reinit to allow in-flight tasks to finish gracefully.
|
|
164
|
+
*/
|
|
165
|
+
async function waitForActiveTasksToComplete(timeoutMs) {
|
|
166
|
+
const start = Date.now();
|
|
167
|
+
const checkInterval = 100;
|
|
168
|
+
// Poll until no active tasks or timeout
|
|
169
|
+
const pollUntilDone = async () => {
|
|
170
|
+
while (taskQueueManager.hasActiveTasks()) {
|
|
171
|
+
if (Date.now() - start >= timeoutMs) {
|
|
172
|
+
agentLog('Timeout waiting for active tasks - proceeding with reinit');
|
|
173
|
+
return;
|
|
174
|
+
}
|
|
175
|
+
// eslint-disable-next-line no-await-in-loop
|
|
176
|
+
await new Promise((resolve) => {
|
|
177
|
+
setTimeout(resolve, checkInterval);
|
|
178
|
+
});
|
|
179
|
+
}
|
|
180
|
+
agentLog('Task queue drained successfully');
|
|
181
|
+
};
|
|
182
|
+
await pollUntilDone();
|
|
183
|
+
}
|
|
124
184
|
/**
|
|
125
185
|
* Setup event forwarding from CipherAgent to Transport.
|
|
126
186
|
* agent-worker subscribes directly to agentEventBus (owns the agent).
|
|
@@ -137,10 +197,18 @@ function setupAgentEventForwarding(agent) {
|
|
|
137
197
|
agentLog('No agentEventBus available for event forwarding');
|
|
138
198
|
return;
|
|
139
199
|
}
|
|
140
|
-
// Helper to register and track event forwarder
|
|
200
|
+
// Helper to register and track event forwarder.
|
|
201
|
+
// Wraps typed handler to match event bus fallback signature.
|
|
141
202
|
const registerForwarder = (event, handler) => {
|
|
142
|
-
|
|
143
|
-
|
|
203
|
+
// Wrapper matches fallback: (data?: unknown) => void
|
|
204
|
+
// BOUNDARY CAST: Event bus delivers unknown data; handler expects T.
|
|
205
|
+
// Type guard not possible for generic T at runtime.
|
|
206
|
+
const wrappedHandler = (data) => {
|
|
207
|
+
handler(data);
|
|
208
|
+
};
|
|
209
|
+
// Uses fallback signature: on(eventName: string, listener: (data?: unknown) => void)
|
|
210
|
+
eventBus.on(event, wrappedHandler);
|
|
211
|
+
eventForwarders.push({ event, handler: wrappedHandler });
|
|
144
212
|
};
|
|
145
213
|
// Forward llmservice:thinking
|
|
146
214
|
registerForwarder('llmservice:thinking', (payload) => {
|
|
@@ -244,46 +312,159 @@ function setupAgentEventForwarding(agent) {
|
|
|
244
312
|
const TASK_EXECUTION_TIMEOUT_MS = 5 * 60 * 1000;
|
|
245
313
|
/**
|
|
246
314
|
* Setup the task executor for TaskQueueManager.
|
|
247
|
-
*
|
|
315
|
+
* Fix #2: Now called unconditionally at startup (before tryInitializeAgent).
|
|
316
|
+
* Lazy init inside executor enables processing tasks even if initial init failed.
|
|
248
317
|
*/
|
|
249
318
|
function setupTaskExecutor() {
|
|
250
319
|
taskQueueManager.setExecutor(async (task) => {
|
|
251
320
|
const { taskId, type } = task;
|
|
252
321
|
const stats = taskQueueManager.getStats(type);
|
|
253
322
|
agentLog(`Processing task ${taskId} (${type}), ${stats.queued} queued, ${stats.active} active`);
|
|
254
|
-
//
|
|
255
|
-
|
|
256
|
-
|
|
257
|
-
|
|
258
|
-
|
|
259
|
-
|
|
260
|
-
|
|
323
|
+
// Fix #2: Lazy initialization - if agent not ready, try to initialize now
|
|
324
|
+
// This enables processing tasks that arrived while init was failing
|
|
325
|
+
if (!isAgentInitialized) {
|
|
326
|
+
agentLog(`Task ${taskId} - agent not initialized, attempting lazy init...`);
|
|
327
|
+
const initialized = await tryInitializeAgent();
|
|
328
|
+
if (!initialized) {
|
|
329
|
+
agentLog(`Task ${taskId} rejected - lazy initialization failed`);
|
|
330
|
+
const error = serializeTaskError(initializationError ?? new AgentNotInitializedError('Agent initialization failed'));
|
|
331
|
+
transportClient?.request('task:error', { error, taskId }).catch(logTransportError);
|
|
332
|
+
return;
|
|
333
|
+
}
|
|
334
|
+
agentLog(`Task ${taskId} - lazy initialization successful, proceeding`);
|
|
335
|
+
}
|
|
336
|
+
// Pre-execution guard: Verify agent is still ready (catches race conditions)
|
|
337
|
+
// This catches the case where credentials polling stopped the agent
|
|
338
|
+
// between when lazy init succeeded and when execution starts.
|
|
339
|
+
if (!isAgentInitialized || !taskProcessor) {
|
|
340
|
+
agentLog(`Task ${taskId} rejected - agent stopped during queue wait`);
|
|
341
|
+
const error = serializeTaskError(new AgentNotInitializedError('Agent stopped during execution wait'));
|
|
342
|
+
transportClient?.request('task:error', { error, taskId }).catch(logTransportError);
|
|
343
|
+
return;
|
|
344
|
+
}
|
|
345
|
+
// Track timeout state for error handling
|
|
346
|
+
let timedOut = false;
|
|
347
|
+
const timeoutId = setTimeout(() => {
|
|
348
|
+
timedOut = true;
|
|
349
|
+
agentLog(`Task ${taskId} timed out after 5 minutes - cancelling via CipherAgent`);
|
|
350
|
+
// Cancel via CipherAgent's existing cancel() method
|
|
351
|
+
// This aborts activeStreamControllers and session, causing generate() to throw
|
|
352
|
+
if (cipherAgent) {
|
|
353
|
+
cipherAgent.cancel().catch((error) => {
|
|
354
|
+
agentLog(`Error cancelling CipherAgent on timeout: ${error}`);
|
|
355
|
+
});
|
|
356
|
+
}
|
|
357
|
+
}, TASK_EXECUTION_TIMEOUT_MS);
|
|
261
358
|
try {
|
|
262
|
-
//
|
|
263
|
-
await
|
|
359
|
+
// Execute task - if timeout fires, cipherAgent.cancel() will cause this to throw
|
|
360
|
+
await handleTaskExecute(task);
|
|
264
361
|
}
|
|
265
362
|
catch (error) {
|
|
266
|
-
// Handle timeout
|
|
267
|
-
if (
|
|
268
|
-
agentLog(`Task ${taskId}
|
|
363
|
+
// Handle timeout-triggered cancellation
|
|
364
|
+
if (timedOut) {
|
|
365
|
+
agentLog(`Task ${taskId} cancelled due to timeout`);
|
|
269
366
|
const errorData = serializeTaskError(new Error('Task exceeded 5 minute timeout'));
|
|
270
367
|
transportClient?.request('task:error', { error: errorData, taskId }).catch(logTransportError);
|
|
271
368
|
return;
|
|
272
369
|
}
|
|
273
|
-
// Handle other errors
|
|
370
|
+
// Handle other errors (not timeout)
|
|
274
371
|
agentLog(`Task execution failed: ${error}`);
|
|
275
372
|
const errorData = serializeTaskError(error);
|
|
276
373
|
transportClient?.request('task:error', { error: errorData, taskId }).catch(logTransportError);
|
|
277
374
|
}
|
|
278
375
|
finally {
|
|
279
376
|
// Always clear timeout to prevent memory leak
|
|
280
|
-
|
|
281
|
-
clearTimeout(timeoutId);
|
|
282
|
-
}
|
|
377
|
+
clearTimeout(timeoutId);
|
|
283
378
|
}
|
|
284
379
|
});
|
|
285
380
|
agentLog('Task executor setup complete');
|
|
286
381
|
}
|
|
382
|
+
/**
|
|
383
|
+
* Timeout for CipherAgent initialization operations.
|
|
384
|
+
* This prevents isInitializing flag from getting stuck if agent.start() or createSession() hangs.
|
|
385
|
+
* ProcessManager has 30s timeout for startup, but runtime reinit needs its own protection.
|
|
386
|
+
*/
|
|
387
|
+
const AGENT_INIT_TIMEOUT_MS = 30_000;
|
|
388
|
+
/**
|
|
389
|
+
* Helper to wrap a promise with a timeout.
|
|
390
|
+
* Throws an error if the operation doesn't complete within the timeout.
|
|
391
|
+
*/
|
|
392
|
+
async function withTimeout(promise, timeoutMs, operation) {
|
|
393
|
+
let timeoutId;
|
|
394
|
+
const timeoutPromise = new Promise((_, reject) => {
|
|
395
|
+
timeoutId = setTimeout(() => {
|
|
396
|
+
reject(new Error(`${operation} timed out after ${timeoutMs}ms`));
|
|
397
|
+
}, timeoutMs);
|
|
398
|
+
});
|
|
399
|
+
try {
|
|
400
|
+
return await Promise.race([promise, timeoutPromise]);
|
|
401
|
+
}
|
|
402
|
+
finally {
|
|
403
|
+
if (timeoutId) {
|
|
404
|
+
clearTimeout(timeoutId);
|
|
405
|
+
}
|
|
406
|
+
}
|
|
407
|
+
}
|
|
408
|
+
/**
|
|
409
|
+
* Validate auth token for initialization.
|
|
410
|
+
* Returns the token if valid, undefined if invalid (also sets initializationError).
|
|
411
|
+
*/
|
|
412
|
+
function validateAuthToken(authToken) {
|
|
413
|
+
if (!authToken) {
|
|
414
|
+
initializationError = new NotAuthenticatedError();
|
|
415
|
+
agentLog('Cannot initialize - no auth token');
|
|
416
|
+
return undefined;
|
|
417
|
+
}
|
|
418
|
+
if (authToken.isExpired()) {
|
|
419
|
+
initializationError = new NotAuthenticatedError();
|
|
420
|
+
agentLog('Cannot initialize - token expired (please run /login to re-authenticate)');
|
|
421
|
+
return undefined;
|
|
422
|
+
}
|
|
423
|
+
return authToken;
|
|
424
|
+
}
|
|
425
|
+
/**
|
|
426
|
+
* Check if cleanup started during init and abort if so.
|
|
427
|
+
* Returns true if should abort (cleanup started), false otherwise.
|
|
428
|
+
*/
|
|
429
|
+
async function shouldAbortInitForCleanup(agent, phase) {
|
|
430
|
+
if (isCleaningUp) {
|
|
431
|
+
agentLog(`Cleanup started during ${phase}, aborting`);
|
|
432
|
+
await agent.stop();
|
|
433
|
+
return true;
|
|
434
|
+
}
|
|
435
|
+
return false;
|
|
436
|
+
}
|
|
437
|
+
/**
|
|
438
|
+
* Stop a pending agent that was created but not yet assigned to cipherAgent.
|
|
439
|
+
* Used in catch block when timeout/error occurs during agent.start() or createSession().
|
|
440
|
+
*/
|
|
441
|
+
async function stopPendingAgent(agent) {
|
|
442
|
+
if (!agent)
|
|
443
|
+
return;
|
|
444
|
+
try {
|
|
445
|
+
await agent.stop();
|
|
446
|
+
}
|
|
447
|
+
catch (stopError) {
|
|
448
|
+
agentLog(`Error stopping pending agent: ${stopError}`);
|
|
449
|
+
}
|
|
450
|
+
}
|
|
451
|
+
/**
|
|
452
|
+
* Stop existing agent during force reinit.
|
|
453
|
+
*/
|
|
454
|
+
async function stopExistingAgentForReinit() {
|
|
455
|
+
if (!cipherAgent)
|
|
456
|
+
return;
|
|
457
|
+
agentLog('Reinitializing with new config...');
|
|
458
|
+
try {
|
|
459
|
+
await cipherAgent.stop();
|
|
460
|
+
}
|
|
461
|
+
catch (error) {
|
|
462
|
+
agentLog(`Error stopping previous agent: ${error}`);
|
|
463
|
+
}
|
|
464
|
+
cipherAgent = undefined;
|
|
465
|
+
taskProcessor = undefined;
|
|
466
|
+
isAgentInitialized = false;
|
|
467
|
+
}
|
|
287
468
|
/**
|
|
288
469
|
* Try to initialize/reinitialize the CipherAgent.
|
|
289
470
|
* Called on startup and lazily when tasks arrive but agent is not initialized.
|
|
@@ -292,9 +473,15 @@ function setupTaskExecutor() {
|
|
|
292
473
|
* @param forceReinit - Force reinitialization even if already initialized (for config reload)
|
|
293
474
|
*/
|
|
294
475
|
async function tryInitializeAgent(forceReinit = false) {
|
|
295
|
-
// Guard: prevent
|
|
296
|
-
if (isInitializing) {
|
|
297
|
-
agentLog('Initialization already in progress
|
|
476
|
+
// Guard: prevent initialization during cleanup or if already in progress
|
|
477
|
+
if (isCleaningUp || isInitializing) {
|
|
478
|
+
agentLog('Initialization blocked (cleanup or already in progress)');
|
|
479
|
+
// Clear isReinitializing if WE set it (forceReinit case)
|
|
480
|
+
// Without this, the flag would be stuck forever since we return before try block
|
|
481
|
+
// and thus finally block never runs. Next poll will re-detect and retry.
|
|
482
|
+
if (forceReinit) {
|
|
483
|
+
isReinitializing = false;
|
|
484
|
+
}
|
|
298
485
|
return false;
|
|
299
486
|
}
|
|
300
487
|
// Already initialized and not forcing reinit
|
|
@@ -302,34 +489,30 @@ async function tryInitializeAgent(forceReinit = false) {
|
|
|
302
489
|
return true;
|
|
303
490
|
}
|
|
304
491
|
isInitializing = true;
|
|
492
|
+
// Set isReinitializing flag for forceReinit to reject tasks during reinit
|
|
493
|
+
// Note: caller (pollCredentialsAndSync) may have already set this - that's OK, we'll clear in finally
|
|
494
|
+
if (forceReinit) {
|
|
495
|
+
isReinitializing = true;
|
|
496
|
+
}
|
|
497
|
+
// Declare outside try block so catch can cleanup on timeout/error
|
|
498
|
+
let pendingAgent;
|
|
305
499
|
try {
|
|
306
|
-
// If forcing reinit, stop existing agent first
|
|
307
|
-
if (forceReinit
|
|
308
|
-
|
|
309
|
-
|
|
310
|
-
|
|
311
|
-
|
|
312
|
-
|
|
313
|
-
|
|
314
|
-
}
|
|
315
|
-
cipherAgent = undefined;
|
|
316
|
-
taskProcessor = undefined;
|
|
317
|
-
isAgentInitialized = false;
|
|
500
|
+
// If forcing reinit, drain queue and stop existing agent first
|
|
501
|
+
if (forceReinit) {
|
|
502
|
+
// Drain task queue before reinit to prevent tasks executing with stale processor
|
|
503
|
+
agentLog('Draining task queue before reinit...');
|
|
504
|
+
taskQueueManager.clear(); // Clear queued (not yet started) tasks
|
|
505
|
+
// Wait for active tasks to complete (with timeout)
|
|
506
|
+
await waitForActiveTasksToComplete(10_000);
|
|
507
|
+
await stopExistingAgentForReinit();
|
|
318
508
|
}
|
|
319
509
|
const tokenStore = createTokenStore();
|
|
320
510
|
const configStore = new ProjectConfigStore();
|
|
321
|
-
const
|
|
511
|
+
const rawToken = await tokenStore.load();
|
|
322
512
|
const brvConfig = await configStore.read();
|
|
323
|
-
//
|
|
513
|
+
// Validate auth token (sets initializationError if invalid)
|
|
514
|
+
const authToken = validateAuthToken(rawToken);
|
|
324
515
|
if (!authToken) {
|
|
325
|
-
initializationError = new NotAuthenticatedError();
|
|
326
|
-
agentLog('Cannot initialize - no auth token');
|
|
327
|
-
return false;
|
|
328
|
-
}
|
|
329
|
-
// Check if token is expired - fail early with clear message instead of 401 later
|
|
330
|
-
if (authToken.isExpired()) {
|
|
331
|
-
initializationError = new NotAuthenticatedError();
|
|
332
|
-
agentLog('Cannot initialize - token expired (please run /login to re-authenticate)');
|
|
333
516
|
return false;
|
|
334
517
|
}
|
|
335
518
|
// Create Executors
|
|
@@ -353,44 +536,71 @@ async function tryInitializeAgent(forceReinit = false) {
|
|
|
353
536
|
projectId: PROJECT,
|
|
354
537
|
sessionKey: authToken.sessionKey,
|
|
355
538
|
};
|
|
356
|
-
|
|
357
|
-
|
|
539
|
+
pendingAgent = new CipherAgent(agentConfig, brvConfig ?? undefined);
|
|
540
|
+
// Wrap agent.start() with timeout to prevent isInitializing from getting stuck
|
|
541
|
+
await withTimeout(pendingAgent.start(), AGENT_INIT_TIMEOUT_MS, 'CipherAgent.start()');
|
|
358
542
|
agentLog('CipherAgent started');
|
|
359
|
-
//
|
|
543
|
+
// Check if cleanup started during agent.start() (fixes zombie agent race)
|
|
544
|
+
if (await shouldAbortInitForCleanup(pendingAgent, 'agent.start()')) {
|
|
545
|
+
return false;
|
|
546
|
+
}
|
|
547
|
+
// Create ChatSession (also with timeout to prevent hanging)
|
|
360
548
|
chatSessionId = `agent-session-${randomUUID()}`;
|
|
361
|
-
await
|
|
549
|
+
await withTimeout(pendingAgent.createSession(chatSessionId), AGENT_INIT_TIMEOUT_MS, 'CipherAgent.createSession()');
|
|
362
550
|
agentLog(`ChatSession created: ${chatSessionId}`);
|
|
551
|
+
// Check if cleanup started during createSession() (fixes zombie agent race)
|
|
552
|
+
if (await shouldAbortInitForCleanup(pendingAgent, 'createSession()')) {
|
|
553
|
+
return false;
|
|
554
|
+
}
|
|
363
555
|
// Setup event forwarding
|
|
364
|
-
setupAgentEventForwarding(
|
|
365
|
-
cipherAgent =
|
|
556
|
+
setupAgentEventForwarding(pendingAgent);
|
|
557
|
+
cipherAgent = pendingAgent;
|
|
558
|
+
pendingAgent = undefined; // Clear local ref - cipherAgent now owns it
|
|
366
559
|
// Create TaskProcessor
|
|
367
560
|
taskProcessor = createTaskProcessor({
|
|
368
561
|
curateExecutor,
|
|
369
562
|
queryExecutor,
|
|
370
563
|
});
|
|
371
564
|
taskProcessor.setAgent(cipherAgent);
|
|
372
|
-
//
|
|
373
|
-
|
|
565
|
+
// NOTE: setupTaskExecutor() is called once in startAgent() before tryInitializeAgent()
|
|
566
|
+
// No need to call again here - executor is already set and handles lazy init
|
|
374
567
|
// Mark as initialized
|
|
375
568
|
isAgentInitialized = true;
|
|
376
569
|
initializationError = undefined;
|
|
570
|
+
// Cache credentials for change detection polling
|
|
571
|
+
updateCachedCredentials(authToken.accessToken, authToken.sessionKey, brvConfig ? { spaceId: brvConfig.spaceId, teamId: brvConfig.teamId } : undefined);
|
|
377
572
|
if (brvConfig) {
|
|
378
573
|
agentLog(`Fully initialized with auth and config (team=${brvConfig.teamId}, space=${brvConfig.spaceId})`);
|
|
379
574
|
}
|
|
380
575
|
else {
|
|
381
576
|
agentLog('Initialized with auth only (no project config yet - will reinit when config available)');
|
|
382
577
|
}
|
|
578
|
+
// Broadcast status change to Transport (init success)
|
|
579
|
+
broadcastStatusChange();
|
|
383
580
|
return true;
|
|
384
581
|
}
|
|
385
582
|
catch (error) {
|
|
583
|
+
// Stop pendingAgent if it was created but not yet assigned to cipherAgent
|
|
584
|
+
// This handles timeout/error during agent.start() or createSession()
|
|
585
|
+
await stopPendingAgent(pendingAgent);
|
|
586
|
+
pendingAgent = undefined;
|
|
587
|
+
// Cleanup partial state before recording error
|
|
588
|
+
// This prevents stale refs from accumulating on repeated failures
|
|
589
|
+
await cleanupPartialInit();
|
|
386
590
|
// Catch errors and return false instead of throwing
|
|
387
591
|
// This allows lazy init to retry when tasks arrive
|
|
388
592
|
initializationError = error instanceof Error ? error : new Error(String(error));
|
|
389
593
|
agentLog(`Agent initialization failed: ${error}`);
|
|
594
|
+
// Broadcast status change to Transport (init failed)
|
|
595
|
+
broadcastStatusChange();
|
|
390
596
|
return false;
|
|
391
597
|
}
|
|
392
598
|
finally {
|
|
393
599
|
isInitializing = false;
|
|
600
|
+
// Clear isReinitializing flag (matches the set above for forceReinit)
|
|
601
|
+
if (forceReinit) {
|
|
602
|
+
isReinitializing = false;
|
|
603
|
+
}
|
|
394
604
|
}
|
|
395
605
|
}
|
|
396
606
|
/**
|
|
@@ -399,20 +609,8 @@ async function tryInitializeAgent(forceReinit = false) {
|
|
|
399
609
|
async function handleTaskExecute(data) {
|
|
400
610
|
const { clientCwd, content, files, taskId, type } = data;
|
|
401
611
|
agentLog(`Processing task: ${taskId} (type=${type})`);
|
|
402
|
-
//
|
|
403
|
-
|
|
404
|
-
agentLog('Not initialized, attempting lazy initialization...');
|
|
405
|
-
const initialized = await tryInitializeAgent();
|
|
406
|
-
if (!initialized) {
|
|
407
|
-
agentLog('Lazy initialization failed');
|
|
408
|
-
const error = serializeTaskError(initializationError ?? new ProcessorNotInitError());
|
|
409
|
-
transportClient?.request('task:error', { error, taskId }).catch(logTransportError);
|
|
410
|
-
return;
|
|
411
|
-
}
|
|
412
|
-
agentLog('Lazy initialization successful!');
|
|
413
|
-
}
|
|
414
|
-
// NOTE: Config change detection removed - use explicit agent:restart event instead
|
|
415
|
-
// (triggered by /init command via TransportHandlers)
|
|
612
|
+
// NOTE: Lazy initialization is handled in setupTaskExecutor() executor callback.
|
|
613
|
+
// By the time we reach here, agent is already initialized (executor does lazy init first).
|
|
416
614
|
if (!taskProcessor) {
|
|
417
615
|
agentLog('TaskProcessor not initialized');
|
|
418
616
|
const error = serializeTaskError(new ProcessorNotInitError());
|
|
@@ -483,13 +681,50 @@ async function startAgent() {
|
|
|
483
681
|
// Register as Agent
|
|
484
682
|
await transportClient.request('agent:register', {});
|
|
485
683
|
agentLog('Registered with Transport');
|
|
486
|
-
//
|
|
684
|
+
// Fix #1: Re-register on any reconnect (Socket.IO auto-reconnect OR force reconnect)
|
|
685
|
+
// When connection is restored, agent needs to re-register with Transport.
|
|
686
|
+
// Using onStateChange instead of 'connect' event to handle all reconnect types.
|
|
687
|
+
// Fix #4: Include status in register payload to prevent race condition window
|
|
688
|
+
let wasDisconnected = false;
|
|
689
|
+
transportClient.onStateChange(async (state) => {
|
|
690
|
+
if (state === 'disconnected' || state === 'reconnecting') {
|
|
691
|
+
wasDisconnected = true;
|
|
692
|
+
}
|
|
693
|
+
else if (state === 'connected' && wasDisconnected) {
|
|
694
|
+
agentLog('Transport reconnected - re-registering with Transport');
|
|
695
|
+
try {
|
|
696
|
+
// Include status in register payload (Transport caches it atomically)
|
|
697
|
+
await transportClient?.request('agent:register', { status: getAgentStatus() });
|
|
698
|
+
// Only clear flag after successful registration - if failed, next reconnect will retry
|
|
699
|
+
wasDisconnected = false;
|
|
700
|
+
agentLog('Re-registered with Transport after reconnect');
|
|
701
|
+
}
|
|
702
|
+
catch (error) {
|
|
703
|
+
// Keep wasDisconnected = true so next reconnect retries registration
|
|
704
|
+
agentLog(`Failed to re-register after reconnect: ${error}`);
|
|
705
|
+
}
|
|
706
|
+
}
|
|
707
|
+
});
|
|
708
|
+
// Fix #2: Setup task executor BEFORE init - enables lazy init when tasks arrive
|
|
709
|
+
// This ensures tasks don't get stuck in queue if initial init fails
|
|
710
|
+
setupTaskExecutor();
|
|
711
|
+
// Try to initialize agent (may fail if no auth yet - that's OK)
|
|
712
|
+
// tryInitializeAgent() broadcasts status on both success and failure,
|
|
713
|
+
// so Transport will have cached status before any task can arrive.
|
|
714
|
+
// If init fails, lazy init will retry when tasks arrive (handled by executor).
|
|
487
715
|
const initialized = await tryInitializeAgent();
|
|
488
716
|
if (!initialized) {
|
|
489
717
|
agentLog('Initial setup incomplete - will retry when tasks arrive (lazy init)');
|
|
490
718
|
}
|
|
491
719
|
// Setup event handlers - TaskQueueManager handles queueing and deduplication
|
|
492
720
|
transportClient.on('task:execute', (data) => {
|
|
721
|
+
// Reject tasks during reinit to prevent TOCTOU race condition
|
|
722
|
+
if (isReinitializing) {
|
|
723
|
+
agentLog(`Task ${data.taskId} rejected - agent reinitializing`);
|
|
724
|
+
const error = serializeTaskError(new AgentNotInitializedError('Agent is reinitializing'));
|
|
725
|
+
transportClient?.request('task:error', { error, taskId: data.taskId }).catch(logTransportError);
|
|
726
|
+
return;
|
|
727
|
+
}
|
|
493
728
|
const result = taskQueueManager.enqueue(data);
|
|
494
729
|
if (result.success) {
|
|
495
730
|
const stats = taskQueueManager.getStats(data.type);
|
|
@@ -506,15 +741,40 @@ async function startAgent() {
|
|
|
506
741
|
// Handle shutdown from Transport
|
|
507
742
|
transportClient.on('shutdown', () => {
|
|
508
743
|
agentLog('Received shutdown from Transport');
|
|
509
|
-
stopAgent()
|
|
744
|
+
stopAgent()
|
|
745
|
+
.then(() => {
|
|
510
746
|
sendToParent({ type: 'stopped' });
|
|
511
747
|
// eslint-disable-next-line n/no-process-exit, unicorn/no-process-exit
|
|
512
748
|
process.exit(0);
|
|
749
|
+
})
|
|
750
|
+
.catch((error) => {
|
|
751
|
+
agentLog(`Error during shutdown: ${error}`);
|
|
752
|
+
sendToParent({ type: 'stopped' });
|
|
753
|
+
// eslint-disable-next-line n/no-process-exit, unicorn/no-process-exit
|
|
754
|
+
process.exit(1);
|
|
513
755
|
});
|
|
514
756
|
});
|
|
515
757
|
// Handle agent:restart from Transport (triggered by client, e.g., after /init)
|
|
516
758
|
transportClient.on('agent:restart', async (data) => {
|
|
517
759
|
agentLog(`Agent restart requested: ${data.reason ?? 'no reason'}`);
|
|
760
|
+
// Guard: reject if initialization already in progress (prevents concurrent reinit race condition)
|
|
761
|
+
if (isInitializing || isReinitializing) {
|
|
762
|
+
agentLog('Agent restart rejected - initialization already in progress');
|
|
763
|
+
await transportClient?.request('agent:restarted', {
|
|
764
|
+
error: 'Initialization already in progress',
|
|
765
|
+
success: false,
|
|
766
|
+
});
|
|
767
|
+
return;
|
|
768
|
+
}
|
|
769
|
+
// Reject restart if tasks are in progress or queued (prevents killing active tasks)
|
|
770
|
+
if (hasPendingWork()) {
|
|
771
|
+
agentLog('Agent restart rejected - tasks in progress or queued');
|
|
772
|
+
await transportClient?.request('agent:restarted', {
|
|
773
|
+
error: 'Tasks in progress. Please wait for tasks to complete.',
|
|
774
|
+
success: false,
|
|
775
|
+
});
|
|
776
|
+
return;
|
|
777
|
+
}
|
|
518
778
|
try {
|
|
519
779
|
// Reinitialize agent with fresh config
|
|
520
780
|
const success = await tryInitializeAgent(true); // forceReinit = true
|
|
@@ -523,10 +783,21 @@ async function startAgent() {
|
|
|
523
783
|
// Notify Transport that restart completed
|
|
524
784
|
await transportClient?.request('agent:restarted', { success: true });
|
|
525
785
|
}
|
|
786
|
+
else if (isCleaningUp) {
|
|
787
|
+
// Cleanup in progress - can't restart during shutdown
|
|
788
|
+
agentLog('Agent reinitialization rejected - cleanup in progress');
|
|
789
|
+
await transportClient?.request('agent:restarted', {
|
|
790
|
+
error: 'Agent is shutting down',
|
|
791
|
+
success: false,
|
|
792
|
+
});
|
|
793
|
+
}
|
|
526
794
|
else {
|
|
795
|
+
// Actual failure - missing auth or config
|
|
796
|
+
// Note: isInitializing is guaranteed to be false here because tryInitializeAgent()
|
|
797
|
+
// always clears it in its finally block before returning
|
|
527
798
|
agentLog('Agent reinitialization failed - config incomplete');
|
|
528
799
|
await transportClient?.request('agent:restarted', {
|
|
529
|
-
error: 'Config incomplete (no auth token or config)',
|
|
800
|
+
error: initializationError?.message ?? 'Config incomplete (no auth token or config)',
|
|
530
801
|
success: false,
|
|
531
802
|
});
|
|
532
803
|
}
|
|
@@ -537,72 +808,281 @@ async function startAgent() {
|
|
|
537
808
|
await transportClient?.request('agent:restarted', { error: message, success: false });
|
|
538
809
|
}
|
|
539
810
|
});
|
|
811
|
+
// Handle agent:newSession from Transport (triggered by /new command)
|
|
812
|
+
transportClient.on('agent:newSession', async (data) => {
|
|
813
|
+
agentLog(`New session requested: ${data.reason ?? 'no reason'}`);
|
|
814
|
+
try {
|
|
815
|
+
if (!cipherAgent) {
|
|
816
|
+
agentLog('Cannot create new session - agent not initialized');
|
|
817
|
+
await transportClient?.request('agent:newSessionCreated', {
|
|
818
|
+
error: 'Agent not initialized',
|
|
819
|
+
success: false,
|
|
820
|
+
});
|
|
821
|
+
return;
|
|
822
|
+
}
|
|
823
|
+
// Generate new session ID
|
|
824
|
+
const newSessionId = `agent-session-${randomUUID()}`;
|
|
825
|
+
// Create new session
|
|
826
|
+
await cipherAgent.createSession(newSessionId);
|
|
827
|
+
// Switch the agent's default session to the new one
|
|
828
|
+
// This ensures execute()/generate()/stream() use the new session
|
|
829
|
+
cipherAgent.switchDefaultSession(newSessionId);
|
|
830
|
+
// Update the local session ID reference
|
|
831
|
+
chatSessionId = newSessionId;
|
|
832
|
+
agentLog(`New session created: ${newSessionId}`);
|
|
833
|
+
// Notify Transport that new session was created
|
|
834
|
+
await transportClient?.request('agent:newSessionCreated', {
|
|
835
|
+
sessionId: newSessionId,
|
|
836
|
+
success: true,
|
|
837
|
+
});
|
|
838
|
+
}
|
|
839
|
+
catch (error) {
|
|
840
|
+
const message = error instanceof Error ? error.message : String(error);
|
|
841
|
+
agentLog(`New session creation error: ${message}`);
|
|
842
|
+
await transportClient?.request('agent:newSessionCreated', {
|
|
843
|
+
error: message,
|
|
844
|
+
success: false,
|
|
845
|
+
});
|
|
846
|
+
}
|
|
847
|
+
});
|
|
540
848
|
agentLog('Ready to process tasks');
|
|
541
849
|
}
|
|
542
850
|
// ============================================================================
|
|
543
|
-
//
|
|
851
|
+
// Credentials Polling Functions
|
|
544
852
|
// ============================================================================
|
|
545
853
|
/**
|
|
546
|
-
*
|
|
547
|
-
*
|
|
548
|
-
|
|
549
|
-
|
|
550
|
-
|
|
551
|
-
|
|
552
|
-
|
|
854
|
+
* Stop CipherAgent only (does NOT exit process or disconnect transport).
|
|
855
|
+
* Used when credentials are missing/invalid but we want to keep polling.
|
|
856
|
+
*/
|
|
857
|
+
async function stopCipherAgent() {
|
|
858
|
+
// Cleanup event forwarders
|
|
859
|
+
cleanupAgentEventForwarding();
|
|
860
|
+
// Clear task queue (can't process without agent)
|
|
861
|
+
taskQueueManager.clear();
|
|
862
|
+
// Stop CipherAgent
|
|
863
|
+
if (cipherAgent) {
|
|
864
|
+
try {
|
|
865
|
+
await cipherAgent.stop();
|
|
866
|
+
}
|
|
867
|
+
catch (error) {
|
|
868
|
+
agentLog(`Error stopping CipherAgent: ${error}`);
|
|
869
|
+
}
|
|
870
|
+
cipherAgent = undefined;
|
|
871
|
+
}
|
|
872
|
+
taskProcessor = undefined;
|
|
873
|
+
chatSessionId = undefined;
|
|
874
|
+
isAgentInitialized = false;
|
|
875
|
+
cachedCredentials = undefined;
|
|
876
|
+
agentLog('CipherAgent stopped (credentials missing or invalid)');
|
|
877
|
+
// Broadcast status change to Transport (cipher stopped)
|
|
878
|
+
broadcastStatusChange();
|
|
879
|
+
}
|
|
880
|
+
/**
|
|
881
|
+
* Cleanup partial state after failed initialization.
|
|
882
|
+
* Called when tryInitializeAgent() fails partway through.
|
|
883
|
+
* Does NOT broadcast status (caller handles that).
|
|
884
|
+
*/
|
|
885
|
+
async function cleanupPartialInit() {
|
|
886
|
+
agentLog('Cleaning up partial initialization state...');
|
|
887
|
+
// Cleanup event forwarders (may have been partially set up)
|
|
888
|
+
cleanupAgentEventForwarding();
|
|
889
|
+
// Stop CipherAgent if it was partially started
|
|
890
|
+
if (cipherAgent) {
|
|
891
|
+
try {
|
|
892
|
+
await cipherAgent.stop();
|
|
893
|
+
}
|
|
894
|
+
catch (error) {
|
|
895
|
+
agentLog(`Error stopping partial CipherAgent: ${error}`);
|
|
896
|
+
}
|
|
897
|
+
cipherAgent = undefined;
|
|
898
|
+
}
|
|
899
|
+
// Clear partial state
|
|
900
|
+
taskProcessor = undefined;
|
|
901
|
+
chatSessionId = undefined;
|
|
902
|
+
// Note: DON'T clear cachedCredentials here - keep old credentials for comparison
|
|
903
|
+
// Note: DON'T set isAgentInitialized - it should already be false
|
|
904
|
+
agentLog('Partial initialization state cleaned up');
|
|
905
|
+
}
|
|
906
|
+
/**
|
|
907
|
+
* Update cached credentials after successful initialization.
|
|
908
|
+
*/
|
|
909
|
+
function updateCachedCredentials(accessToken, sessionKey, config) {
|
|
910
|
+
cachedCredentials = {
|
|
911
|
+
accessToken,
|
|
912
|
+
sessionKey,
|
|
913
|
+
spaceId: config?.spaceId,
|
|
914
|
+
teamId: config?.teamId,
|
|
915
|
+
};
|
|
916
|
+
}
|
|
917
|
+
/**
|
|
918
|
+
* Check if credentials have changed compared to cache.
|
|
919
|
+
*/
|
|
920
|
+
function credentialsChanged(currentToken, currentConfig) {
|
|
921
|
+
// No cached credentials = first run or was stopped
|
|
922
|
+
if (!cachedCredentials) {
|
|
923
|
+
return currentToken ? 'changed' : 'missing';
|
|
924
|
+
}
|
|
925
|
+
// Token missing = credentials gone
|
|
926
|
+
if (!currentToken) {
|
|
927
|
+
return 'missing';
|
|
928
|
+
}
|
|
929
|
+
// Compare token
|
|
930
|
+
if (currentToken.accessToken !== cachedCredentials.accessToken ||
|
|
931
|
+
currentToken.sessionKey !== cachedCredentials.sessionKey) {
|
|
932
|
+
return 'changed';
|
|
933
|
+
}
|
|
934
|
+
// Compare config (spaceId/teamId)
|
|
935
|
+
const currentSpaceId = currentConfig?.spaceId;
|
|
936
|
+
const currentTeamId = currentConfig?.teamId;
|
|
937
|
+
if (currentSpaceId !== cachedCredentials.spaceId || currentTeamId !== cachedCredentials.teamId) {
|
|
938
|
+
return 'changed';
|
|
939
|
+
}
|
|
940
|
+
return 'unchanged';
|
|
941
|
+
}
|
|
942
|
+
/**
|
|
943
|
+
* Poll credentials and sync CipherAgent state.
|
|
553
944
|
*
|
|
554
|
-
*
|
|
555
|
-
* If
|
|
945
|
+
* Called periodically to detect auth/config changes:
|
|
946
|
+
* - If credentials MISSING → stop CipherAgent
|
|
947
|
+
* - If credentials CHANGED → reinit CipherAgent
|
|
948
|
+
* - If UNCHANGED → do nothing
|
|
556
949
|
*/
|
|
557
|
-
function
|
|
558
|
-
//
|
|
559
|
-
if (
|
|
950
|
+
async function pollCredentialsAndSync() {
|
|
951
|
+
// Guard: prevent concurrent polling
|
|
952
|
+
if (isPolling) {
|
|
560
953
|
return;
|
|
561
|
-
|
|
562
|
-
|
|
563
|
-
|
|
564
|
-
|
|
565
|
-
|
|
566
|
-
|
|
567
|
-
|
|
568
|
-
|
|
569
|
-
|
|
570
|
-
|
|
571
|
-
|
|
572
|
-
|
|
573
|
-
|
|
574
|
-
|
|
575
|
-
|
|
576
|
-
|
|
577
|
-
|
|
578
|
-
|
|
579
|
-
|
|
580
|
-
|
|
581
|
-
|
|
582
|
-
|
|
583
|
-
|
|
584
|
-
|
|
585
|
-
|
|
586
|
-
//
|
|
587
|
-
|
|
588
|
-
|
|
589
|
-
|
|
954
|
+
}
|
|
955
|
+
// Guard: don't poll during cleanup or initialization
|
|
956
|
+
if (isCleaningUp || isInitializing) {
|
|
957
|
+
return;
|
|
958
|
+
}
|
|
959
|
+
isPolling = true;
|
|
960
|
+
try {
|
|
961
|
+
// Use lazy-initialized cached stores (avoid creating new instances every poll)
|
|
962
|
+
const stores = getPollingStores();
|
|
963
|
+
const authToken = await stores.pollingTokenStore.load();
|
|
964
|
+
const brvConfig = await stores.pollingConfigStore.read();
|
|
965
|
+
// Detect change
|
|
966
|
+
const tokenInfo = authToken ? { accessToken: authToken.accessToken, sessionKey: authToken.sessionKey } : undefined;
|
|
967
|
+
const configInfo = brvConfig ? { spaceId: brvConfig.spaceId, teamId: brvConfig.teamId } : undefined;
|
|
968
|
+
const changeStatus = credentialsChanged(tokenInfo, configInfo);
|
|
969
|
+
switch (changeStatus) {
|
|
970
|
+
case 'changed': {
|
|
971
|
+
// Check RIGHT BEFORE reinit - after all awaits, to catch tasks added during awaits
|
|
972
|
+
// Must check BOTH active (running) AND queued tasks to prevent race condition
|
|
973
|
+
// where task is enqueued after this check but before stopCipherAgent() clears taskProcessor
|
|
974
|
+
if (hasPendingWork() || isReinitializing) {
|
|
975
|
+
agentLog('Credentials changed but tasks in progress, queued, or reinit in progress - deferring');
|
|
976
|
+
return;
|
|
977
|
+
}
|
|
978
|
+
// Set flag IMMEDIATELY after check to close TOCTOU window
|
|
979
|
+
// tryInitializeAgent will manage the flag from here (set on entry, clear in finally)
|
|
980
|
+
isReinitializing = true;
|
|
981
|
+
// Credentials changed - reinit CipherAgent
|
|
982
|
+
agentLog('Credentials changed - reinitializing CipherAgent');
|
|
983
|
+
const success = await tryInitializeAgent(true); // forceReinit
|
|
984
|
+
if (success) {
|
|
985
|
+
agentLog('CipherAgent reinitialized with new credentials');
|
|
986
|
+
}
|
|
987
|
+
else {
|
|
988
|
+
agentLog('CipherAgent reinitialization failed');
|
|
989
|
+
}
|
|
990
|
+
break;
|
|
991
|
+
}
|
|
992
|
+
case 'missing': {
|
|
993
|
+
// Credentials gone - stop CipherAgent if running
|
|
994
|
+
if (isAgentInitialized) {
|
|
995
|
+
if (hasPendingWork()) {
|
|
996
|
+
agentLog('Credentials missing but tasks in progress - deferring stop');
|
|
997
|
+
return;
|
|
998
|
+
}
|
|
999
|
+
agentLog('Credentials missing - stopping CipherAgent');
|
|
1000
|
+
await stopCipherAgent();
|
|
1001
|
+
}
|
|
1002
|
+
break;
|
|
1003
|
+
}
|
|
1004
|
+
case 'unchanged': {
|
|
1005
|
+
// No change - check if token expired (edge case)
|
|
1006
|
+
if (authToken?.isExpired() && isAgentInitialized) {
|
|
1007
|
+
if (hasPendingWork()) {
|
|
1008
|
+
agentLog('Token expired but tasks in progress - deferring stop');
|
|
1009
|
+
return;
|
|
1010
|
+
}
|
|
1011
|
+
agentLog('Token expired - stopping CipherAgent');
|
|
1012
|
+
await stopCipherAgent();
|
|
1013
|
+
}
|
|
1014
|
+
break;
|
|
1015
|
+
}
|
|
590
1016
|
}
|
|
591
|
-
|
|
592
|
-
|
|
593
|
-
|
|
1017
|
+
}
|
|
1018
|
+
catch (error) {
|
|
1019
|
+
// Don't crash on poll errors - just log and continue
|
|
1020
|
+
agentLog(`Credentials poll error: ${error}`);
|
|
1021
|
+
}
|
|
1022
|
+
finally {
|
|
1023
|
+
isPolling = false;
|
|
1024
|
+
}
|
|
1025
|
+
}
|
|
1026
|
+
/**
|
|
1027
|
+
* Start credentials polling.
|
|
1028
|
+
* Uses recursive setTimeout pattern (same as parent heartbeat).
|
|
1029
|
+
*/
|
|
1030
|
+
function startCredentialsPolling() {
|
|
1031
|
+
if (credentialsPollingRunning) {
|
|
1032
|
+
return;
|
|
1033
|
+
}
|
|
1034
|
+
credentialsPollingRunning = true;
|
|
1035
|
+
const poll = () => {
|
|
1036
|
+
if (!credentialsPollingRunning) {
|
|
1037
|
+
return;
|
|
594
1038
|
}
|
|
1039
|
+
pollCredentialsAndSync()
|
|
1040
|
+
.catch((error) => {
|
|
1041
|
+
agentLog(`Credentials poll failed: ${error}`);
|
|
1042
|
+
})
|
|
1043
|
+
.finally(() => {
|
|
1044
|
+
// Schedule next poll (only if still running)
|
|
1045
|
+
if (credentialsPollingRunning) {
|
|
1046
|
+
setTimeout(poll, CREDENTIALS_POLL_INTERVAL_MS);
|
|
1047
|
+
}
|
|
1048
|
+
});
|
|
1049
|
+
};
|
|
1050
|
+
// Start first poll after delay
|
|
1051
|
+
setTimeout(poll, CREDENTIALS_POLL_INTERVAL_MS);
|
|
1052
|
+
agentLog('Credentials polling started');
|
|
1053
|
+
}
|
|
1054
|
+
/**
|
|
1055
|
+
* Stop credentials polling.
|
|
1056
|
+
*/
|
|
1057
|
+
function stopCredentialsPolling() {
|
|
1058
|
+
credentialsPollingRunning = false;
|
|
1059
|
+
}
|
|
1060
|
+
// ============================================================================
|
|
1061
|
+
// Agent Status Reporting
|
|
1062
|
+
// ============================================================================
|
|
1063
|
+
/**
|
|
1064
|
+
* Get current agent status for health check.
|
|
1065
|
+
* Used by Transport to check if agent is ready before forwarding tasks.
|
|
1066
|
+
*/
|
|
1067
|
+
function getAgentStatus() {
|
|
1068
|
+
return {
|
|
1069
|
+
activeTasks: taskQueueManager.getActiveCount(),
|
|
1070
|
+
hasAuth: cachedCredentials !== undefined,
|
|
1071
|
+
// Check both spaceId and teamId for safety (both must be set after /init)
|
|
1072
|
+
hasConfig: cachedCredentials?.spaceId !== undefined && cachedCredentials?.teamId !== undefined,
|
|
1073
|
+
isInitialized: isAgentInitialized,
|
|
1074
|
+
lastError: initializationError?.message,
|
|
1075
|
+
queuedTasks: taskQueueManager.getQueuedCount(),
|
|
595
1076
|
};
|
|
596
|
-
// Start first check after delay
|
|
597
|
-
setTimeout(checkParent, PARENT_HEARTBEAT_INTERVAL_MS);
|
|
598
|
-
agentLog(`Parent heartbeat monitoring started (PPID: ${parentPid})`);
|
|
599
1077
|
}
|
|
600
1078
|
/**
|
|
601
|
-
*
|
|
602
|
-
*
|
|
1079
|
+
* Broadcast status change to Transport.
|
|
1080
|
+
* Called when cipher state changes (init success/fail, stop, credentials change).
|
|
1081
|
+
* Transport will forward to all connected clients.
|
|
603
1082
|
*/
|
|
604
|
-
function
|
|
605
|
-
|
|
1083
|
+
function broadcastStatusChange() {
|
|
1084
|
+
const status = getAgentStatus();
|
|
1085
|
+
transportClient?.request('agent:status:changed', status).catch(logTransportError);
|
|
606
1086
|
}
|
|
607
1087
|
/**
|
|
608
1088
|
* Stop Agent Process.
|
|
@@ -615,8 +1095,9 @@ async function stopAgent() {
|
|
|
615
1095
|
}
|
|
616
1096
|
isCleaningUp = true;
|
|
617
1097
|
try {
|
|
618
|
-
// Stop
|
|
619
|
-
|
|
1098
|
+
// Stop polling and heartbeat first
|
|
1099
|
+
stopCredentialsPolling();
|
|
1100
|
+
parentHeartbeat?.stop();
|
|
620
1101
|
// Clear task queue
|
|
621
1102
|
taskQueueManager.clear();
|
|
622
1103
|
// Cleanup event forwarders before stopping agent
|
|
@@ -648,7 +1129,14 @@ async function runWorker() {
|
|
|
648
1129
|
sendToParent({ type: 'ready' });
|
|
649
1130
|
// Start parent heartbeat monitoring after ready
|
|
650
1131
|
// This ensures we self-terminate if parent dies (SIGKILL scenario)
|
|
651
|
-
|
|
1132
|
+
parentHeartbeat = createParentHeartbeat({
|
|
1133
|
+
cleanup: stopAgent,
|
|
1134
|
+
log: agentLog,
|
|
1135
|
+
});
|
|
1136
|
+
parentHeartbeat.start();
|
|
1137
|
+
// Start credentials polling to detect auth/config changes
|
|
1138
|
+
// This ensures CipherAgent stays in sync with user's login state
|
|
1139
|
+
startCredentialsPolling();
|
|
652
1140
|
}
|
|
653
1141
|
catch (error) {
|
|
654
1142
|
const message = error instanceof Error ? error.message : String(error);
|
|
@@ -661,14 +1149,47 @@ async function runWorker() {
|
|
|
661
1149
|
}
|
|
662
1150
|
// IPC message handler
|
|
663
1151
|
process.on('message', async (msg) => {
|
|
664
|
-
|
|
665
|
-
|
|
666
|
-
|
|
667
|
-
|
|
668
|
-
|
|
669
|
-
|
|
670
|
-
|
|
671
|
-
|
|
1152
|
+
switch (msg.type) {
|
|
1153
|
+
case 'health-check': {
|
|
1154
|
+
// Fix #3: Health-check after sleep/wake - verify and repair connection
|
|
1155
|
+
agentLog('Received health-check from parent - verifying connection');
|
|
1156
|
+
// Guard: skip health check during initialization (would interfere with startup sequence)
|
|
1157
|
+
if (isInitializing) {
|
|
1158
|
+
agentLog('Health-check skipped - initialization in progress');
|
|
1159
|
+
sendToParent({ success: true, type: 'health-check-result' });
|
|
1160
|
+
break;
|
|
1161
|
+
}
|
|
1162
|
+
// Guard: transportClient must exist for health check
|
|
1163
|
+
if (!transportClient) {
|
|
1164
|
+
agentLog('Health-check failed - transportClient not initialized');
|
|
1165
|
+
sendToParent({ success: false, type: 'health-check-result' });
|
|
1166
|
+
break;
|
|
1167
|
+
}
|
|
1168
|
+
try {
|
|
1169
|
+
// Re-register with Transport to ensure connection is alive
|
|
1170
|
+
// Include status in register payload (Transport caches it atomically)
|
|
1171
|
+
await transportClient.request('agent:register', { status: getAgentStatus() });
|
|
1172
|
+
agentLog('Health-check passed - connection verified');
|
|
1173
|
+
sendToParent({ success: true, type: 'health-check-result' });
|
|
1174
|
+
}
|
|
1175
|
+
catch (error) {
|
|
1176
|
+
agentLog(`Health-check failed - connection may be stale: ${error}`);
|
|
1177
|
+
sendToParent({ success: false, type: 'health-check-result' });
|
|
1178
|
+
// Socket.IO will attempt reconnection automatically
|
|
1179
|
+
}
|
|
1180
|
+
break;
|
|
1181
|
+
}
|
|
1182
|
+
case 'ping': {
|
|
1183
|
+
sendToParent({ type: 'pong' });
|
|
1184
|
+
break;
|
|
1185
|
+
}
|
|
1186
|
+
case 'shutdown': {
|
|
1187
|
+
await stopAgent();
|
|
1188
|
+
sendToParent({ type: 'stopped' });
|
|
1189
|
+
// eslint-disable-next-line n/no-process-exit
|
|
1190
|
+
process.exit(0);
|
|
1191
|
+
// Note: break unreachable due to process.exit() above
|
|
1192
|
+
}
|
|
672
1193
|
}
|
|
673
1194
|
});
|
|
674
1195
|
// Signal handlers
|