byterover-cli 1.4.0 → 1.5.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 +61 -1
- package/dist/core/domain/cipher/process/types.d.ts +1 -1
- package/dist/core/domain/entities/provider-config.d.ts +92 -0
- package/dist/core/domain/entities/provider-config.js +181 -0
- package/dist/core/domain/entities/provider-registry.d.ts +55 -0
- package/dist/core/domain/entities/provider-registry.js +74 -0
- package/dist/core/interfaces/cipher/i-content-generator.d.ts +30 -0
- package/dist/core/interfaces/cipher/i-content-generator.js +12 -1
- package/dist/core/interfaces/cipher/message-factory.d.ts +4 -1
- package/dist/core/interfaces/cipher/message-factory.js +5 -0
- package/dist/core/interfaces/cipher/message-types.d.ts +19 -1
- package/dist/core/interfaces/i-provider-config-store.d.ts +88 -0
- package/dist/core/interfaces/i-provider-config-store.js +1 -0
- package/dist/core/interfaces/i-provider-keychain-store.d.ts +33 -0
- package/dist/core/interfaces/i-provider-keychain-store.js +1 -0
- package/dist/infra/cipher/file-system/file-system-service.js +5 -5
- package/dist/infra/cipher/http/internal-llm-http-service.d.ts +40 -0
- package/dist/infra/cipher/http/internal-llm-http-service.js +152 -2
- package/dist/infra/cipher/llm/formatters/gemini-formatter.js +8 -1
- package/dist/infra/cipher/llm/generators/byterover-content-generator.d.ts +2 -3
- package/dist/infra/cipher/llm/generators/byterover-content-generator.js +20 -11
- package/dist/infra/cipher/llm/generators/openrouter-content-generator.d.ts +1 -0
- package/dist/infra/cipher/llm/generators/openrouter-content-generator.js +26 -0
- package/dist/infra/cipher/llm/internal-llm-service.d.ts +13 -0
- package/dist/infra/cipher/llm/internal-llm-service.js +75 -4
- package/dist/infra/cipher/llm/model-capabilities.d.ts +74 -0
- package/dist/infra/cipher/llm/model-capabilities.js +157 -0
- package/dist/infra/cipher/llm/openrouter-llm-service.d.ts +35 -1
- package/dist/infra/cipher/llm/openrouter-llm-service.js +216 -28
- package/dist/infra/cipher/llm/stream-processor.d.ts +22 -2
- package/dist/infra/cipher/llm/stream-processor.js +78 -4
- package/dist/infra/cipher/llm/thought-parser.d.ts +1 -1
- package/dist/infra/cipher/llm/thought-parser.js +5 -5
- package/dist/infra/cipher/llm/transformers/openrouter-stream-transformer.d.ts +49 -0
- package/dist/infra/cipher/llm/transformers/openrouter-stream-transformer.js +272 -0
- package/dist/infra/cipher/llm/transformers/reasoning-extractor.d.ts +71 -0
- package/dist/infra/cipher/llm/transformers/reasoning-extractor.js +253 -0
- package/dist/infra/cipher/process/process-service.js +1 -1
- package/dist/infra/cipher/session/chat-session.d.ts +2 -0
- package/dist/infra/cipher/session/chat-session.js +13 -2
- package/dist/infra/cipher/storage/message-storage-service.js +4 -0
- package/dist/infra/cipher/tools/implementations/bash-exec-tool.js +3 -3
- package/dist/infra/cipher/tools/implementations/task-tool.js +1 -1
- package/dist/infra/http/openrouter-api-client.d.ts +148 -0
- package/dist/infra/http/openrouter-api-client.js +161 -0
- package/dist/infra/mcp/tools/task-result-waiter.js +9 -1
- package/dist/infra/process/agent-worker.js +178 -70
- package/dist/infra/process/transport-handlers.d.ts +25 -4
- package/dist/infra/process/transport-handlers.js +57 -10
- package/dist/infra/repl/commands/connectors-command.js +2 -2
- package/dist/infra/repl/commands/index.js +5 -0
- package/dist/infra/repl/commands/model-command.d.ts +13 -0
- package/dist/infra/repl/commands/model-command.js +212 -0
- package/dist/infra/repl/commands/provider-command.d.ts +13 -0
- package/dist/infra/repl/commands/provider-command.js +181 -0
- package/dist/infra/repl/transport-client-helper.js +6 -2
- package/dist/infra/storage/file-provider-config-store.d.ts +83 -0
- package/dist/infra/storage/file-provider-config-store.js +157 -0
- package/dist/infra/storage/provider-keychain-store.d.ts +37 -0
- package/dist/infra/storage/provider-keychain-store.js +75 -0
- package/dist/infra/transport/socket-io-transport-client.d.ts +20 -0
- package/dist/infra/transport/socket-io-transport-client.js +88 -1
- package/dist/resources/tools/bash_exec.txt +1 -1
- package/dist/tui/components/api-key-dialog.d.ts +39 -0
- package/dist/tui/components/api-key-dialog.js +94 -0
- package/dist/tui/components/execution/execution-changes.d.ts +3 -1
- package/dist/tui/components/execution/execution-changes.js +4 -4
- package/dist/tui/components/execution/execution-content.d.ts +1 -1
- package/dist/tui/components/execution/execution-content.js +4 -12
- package/dist/tui/components/execution/execution-input.js +1 -1
- package/dist/tui/components/execution/execution-progress.d.ts +10 -13
- package/dist/tui/components/execution/execution-progress.js +70 -17
- package/dist/tui/components/execution/execution-reasoning.d.ts +16 -0
- package/dist/tui/components/execution/execution-reasoning.js +34 -0
- package/dist/tui/components/execution/execution-tool.d.ts +23 -0
- package/dist/tui/components/execution/execution-tool.js +125 -0
- package/dist/tui/components/execution/expanded-log-view.js +3 -3
- package/dist/tui/components/execution/log-item.d.ts +2 -0
- package/dist/tui/components/execution/log-item.js +6 -4
- package/dist/tui/components/index.d.ts +2 -0
- package/dist/tui/components/index.js +2 -0
- package/dist/tui/components/inline-prompts/inline-select.js +3 -2
- package/dist/tui/components/model-dialog.d.ts +63 -0
- package/dist/tui/components/model-dialog.js +89 -0
- package/dist/tui/components/onboarding/onboarding-flow.js +8 -2
- package/dist/tui/components/provider-dialog.d.ts +27 -0
- package/dist/tui/components/provider-dialog.js +31 -0
- package/dist/tui/components/reasoning-text.d.ts +26 -0
- package/dist/tui/components/reasoning-text.js +49 -0
- package/dist/tui/components/selectable-list.d.ts +54 -0
- package/dist/tui/components/selectable-list.js +180 -0
- package/dist/tui/components/streaming-text.d.ts +30 -0
- package/dist/tui/components/streaming-text.js +52 -0
- package/dist/tui/contexts/tasks-context.d.ts +15 -0
- package/dist/tui/contexts/tasks-context.js +224 -40
- package/dist/tui/contexts/theme-context.d.ts +1 -0
- package/dist/tui/contexts/theme-context.js +3 -2
- package/dist/tui/hooks/use-activity-logs.js +7 -1
- package/dist/tui/types/messages.d.ts +32 -5
- package/dist/tui/utils/index.d.ts +1 -1
- package/dist/tui/utils/index.js +1 -1
- package/dist/tui/utils/log.d.ts +0 -9
- package/dist/tui/utils/log.js +2 -53
- package/dist/tui/views/command-view.js +4 -1
- package/oclif.manifest.json +1 -1
- package/package.json +1 -1
|
@@ -27,6 +27,8 @@ import { ProjectConfigStore } from '../config/file-config-store.js';
|
|
|
27
27
|
import { CurateExecutor } from '../core/executors/curate-executor.js';
|
|
28
28
|
import { QueryExecutor } from '../core/executors/query-executor.js';
|
|
29
29
|
import { createTaskProcessor } from '../core/task-processor.js';
|
|
30
|
+
import { FileProviderConfigStore } from '../storage/file-provider-config-store.js';
|
|
31
|
+
import { ProviderKeychainStore } from '../storage/provider-keychain-store.js';
|
|
30
32
|
import { createTokenStore } from '../storage/token-store.js';
|
|
31
33
|
import { createTransportClient } from '../transport/transport-factory.js';
|
|
32
34
|
import { createParentHeartbeat } from './parent-heartbeat.js';
|
|
@@ -44,6 +46,41 @@ function logTransportError(error) {
|
|
|
44
46
|
const message = error instanceof Error ? error.message : String(error);
|
|
45
47
|
agentLog(`Transport error (non-fatal): ${message}`);
|
|
46
48
|
}
|
|
49
|
+
/**
|
|
50
|
+
* Send a critical transport event with retry logic.
|
|
51
|
+
* Uses exponential backoff for retries to handle transient network issues.
|
|
52
|
+
* Inspired by opencode's robust event forwarding pattern.
|
|
53
|
+
*
|
|
54
|
+
* Critical events include: task:completed, task:error, task:cancelled
|
|
55
|
+
* These should be retried because if they fail, TUI won't know the task ended.
|
|
56
|
+
*
|
|
57
|
+
* @param eventName - The event name to send
|
|
58
|
+
* @param data - The event payload
|
|
59
|
+
* @param maxRetries - Maximum number of retry attempts (default: 3)
|
|
60
|
+
*/
|
|
61
|
+
function sendCriticalEvent(eventName, data, maxRetries = 3) {
|
|
62
|
+
const BASE_DELAY_MS = 100;
|
|
63
|
+
const attemptSend = (attempt) => {
|
|
64
|
+
transportClient
|
|
65
|
+
?.request(eventName, data)
|
|
66
|
+
.then(() => {
|
|
67
|
+
// Success - nothing more to do
|
|
68
|
+
})
|
|
69
|
+
.catch((error) => {
|
|
70
|
+
const isLastAttempt = attempt >= maxRetries;
|
|
71
|
+
const message = error instanceof Error ? error.message : String(error);
|
|
72
|
+
if (isLastAttempt) {
|
|
73
|
+
agentLog(`Critical event ${eventName} failed after ${maxRetries + 1} attempts: ${message}`);
|
|
74
|
+
return;
|
|
75
|
+
}
|
|
76
|
+
// Retry with exponential backoff
|
|
77
|
+
const delay = BASE_DELAY_MS * 2 ** attempt;
|
|
78
|
+
agentLog(`Critical event ${eventName} failed (attempt ${attempt + 1}/${maxRetries + 1}), retrying in ${delay}ms: ${message}`);
|
|
79
|
+
setTimeout(() => attemptSend(attempt + 1), delay);
|
|
80
|
+
});
|
|
81
|
+
};
|
|
82
|
+
attemptSend(0);
|
|
83
|
+
}
|
|
47
84
|
// Task types imported from core/domain/transport/schemas.ts:
|
|
48
85
|
// - TaskExecute: Transport → Agent (task:execute event)
|
|
49
86
|
// - TaskCancel: Transport → Agent (task:cancel event)
|
|
@@ -345,7 +382,7 @@ function setupTaskExecutor() {
|
|
|
345
382
|
if (!initialized) {
|
|
346
383
|
agentLog(`Task ${taskId} rejected - lazy initialization failed`);
|
|
347
384
|
const error = serializeTaskError(initializationError ?? new AgentNotInitializedError('Agent initialization failed'));
|
|
348
|
-
|
|
385
|
+
sendCriticalEvent('task:error', { error, taskId });
|
|
349
386
|
return;
|
|
350
387
|
}
|
|
351
388
|
agentLog(`Task ${taskId} - lazy initialization successful, proceeding`);
|
|
@@ -356,7 +393,7 @@ function setupTaskExecutor() {
|
|
|
356
393
|
if (!isAgentInitialized || !taskProcessor) {
|
|
357
394
|
agentLog(`Task ${taskId} rejected - agent stopped during queue wait`);
|
|
358
395
|
const error = serializeTaskError(new AgentNotInitializedError('Agent stopped during execution wait'));
|
|
359
|
-
|
|
396
|
+
sendCriticalEvent('task:error', { error, taskId });
|
|
360
397
|
return;
|
|
361
398
|
}
|
|
362
399
|
// Track timeout state for error handling
|
|
@@ -381,13 +418,13 @@ function setupTaskExecutor() {
|
|
|
381
418
|
if (timedOut) {
|
|
382
419
|
agentLog(`Task ${taskId} cancelled due to timeout`);
|
|
383
420
|
const errorData = serializeTaskError(new Error('Task exceeded 5 minute timeout'));
|
|
384
|
-
|
|
421
|
+
sendCriticalEvent('task:error', { error: errorData, taskId });
|
|
385
422
|
return;
|
|
386
423
|
}
|
|
387
424
|
// Handle other errors (not timeout)
|
|
388
425
|
agentLog(`Task execution failed: ${error}`);
|
|
389
426
|
const errorData = serializeTaskError(error);
|
|
390
|
-
|
|
427
|
+
sendCriticalEvent('task:error', { error: errorData, taskId });
|
|
391
428
|
}
|
|
392
429
|
finally {
|
|
393
430
|
// Always clear timeout to prevent memory leak
|
|
@@ -482,6 +519,118 @@ async function stopExistingAgentForReinit() {
|
|
|
482
519
|
taskProcessor = undefined;
|
|
483
520
|
isAgentInitialized = false;
|
|
484
521
|
}
|
|
522
|
+
/**
|
|
523
|
+
* Prepare for reinitialization by draining queue and stopping existing agent.
|
|
524
|
+
*
|
|
525
|
+
* NOTE: We do NOT cleanup transport event handlers here because they are meant
|
|
526
|
+
* to be long-lived for the entire worker process lifecycle. Only agent event
|
|
527
|
+
* forwarders (which are tied to the CipherAgent instance) are cleaned up via
|
|
528
|
+
* cleanupAgentEventForwarding() in stopExistingAgentForReinit().
|
|
529
|
+
*/
|
|
530
|
+
async function prepareForReinit() {
|
|
531
|
+
// Drain task queue before reinit to prevent tasks executing with stale processor
|
|
532
|
+
agentLog('Draining task queue before reinit...');
|
|
533
|
+
notifyQueuedTasksAboutDropAndClear('credential/config change');
|
|
534
|
+
// Wait for active tasks to complete (with timeout)
|
|
535
|
+
await waitForActiveTasksToComplete(10_000);
|
|
536
|
+
await stopExistingAgentForReinit();
|
|
537
|
+
}
|
|
538
|
+
/**
|
|
539
|
+
* Load provider configuration and return API key and model if using external provider.
|
|
540
|
+
*/
|
|
541
|
+
async function loadProviderConfiguration() {
|
|
542
|
+
const providerConfigStore = new FileProviderConfigStore();
|
|
543
|
+
const providerKeychainStore = new ProviderKeychainStore();
|
|
544
|
+
const providerConfig = await providerConfigStore.read();
|
|
545
|
+
const activeProviderId = providerConfig.activeProvider;
|
|
546
|
+
// Get OpenRouter API key if using external provider
|
|
547
|
+
let openRouterApiKey;
|
|
548
|
+
let modelFromProvider;
|
|
549
|
+
if (activeProviderId !== 'byterover') {
|
|
550
|
+
openRouterApiKey = await providerKeychainStore.getApiKey(activeProviderId);
|
|
551
|
+
modelFromProvider = await providerConfigStore.getActiveModel(activeProviderId);
|
|
552
|
+
if (openRouterApiKey) {
|
|
553
|
+
agentLog(`Using external provider: ${activeProviderId}${modelFromProvider ? ` with model: ${modelFromProvider}` : ''}`);
|
|
554
|
+
}
|
|
555
|
+
}
|
|
556
|
+
return { modelFromProvider, openRouterApiKey };
|
|
557
|
+
}
|
|
558
|
+
/**
|
|
559
|
+
* Build agent configuration object from auth token and provider settings.
|
|
560
|
+
*/
|
|
561
|
+
function buildAgentConfig(authToken, modelFromProvider, openRouterApiKey) {
|
|
562
|
+
const envConfig = getCurrentConfig();
|
|
563
|
+
return {
|
|
564
|
+
accessToken: authToken.accessToken,
|
|
565
|
+
apiBaseUrl: envConfig.llmApiBaseUrl,
|
|
566
|
+
fileSystem: { workingDirectory: process.cwd() },
|
|
567
|
+
llm: {
|
|
568
|
+
maxIterations: 10,
|
|
569
|
+
maxTokens: 4096,
|
|
570
|
+
temperature: 0.7,
|
|
571
|
+
topK: 10,
|
|
572
|
+
topP: 0.95,
|
|
573
|
+
verbose: false,
|
|
574
|
+
},
|
|
575
|
+
model: modelFromProvider ?? DEFAULT_LLM_MODEL,
|
|
576
|
+
openRouterApiKey,
|
|
577
|
+
projectId: PROJECT,
|
|
578
|
+
sessionKey: authToken.sessionKey,
|
|
579
|
+
};
|
|
580
|
+
}
|
|
581
|
+
/**
|
|
582
|
+
* Initialize agent instance by starting it and creating a session.
|
|
583
|
+
* Returns the initialized agent or undefined if cleanup was triggered.
|
|
584
|
+
*/
|
|
585
|
+
async function initializeAgentInstance(agent) {
|
|
586
|
+
// Wrap agent.start() with timeout to prevent isInitializing from getting stuck
|
|
587
|
+
await withTimeout(agent.start(), AGENT_INIT_TIMEOUT_MS, 'CipherAgent.start()');
|
|
588
|
+
agentLog('CipherAgent started');
|
|
589
|
+
// Check if cleanup started during agent.start() (fixes zombie agent race)
|
|
590
|
+
if (await shouldAbortInitForCleanup(agent, 'agent.start()')) {
|
|
591
|
+
return undefined;
|
|
592
|
+
}
|
|
593
|
+
// Create ChatSession (also with timeout to prevent hanging)
|
|
594
|
+
chatSessionId = `agent-session-${randomUUID()}`;
|
|
595
|
+
await withTimeout(agent.createSession(chatSessionId), AGENT_INIT_TIMEOUT_MS, 'CipherAgent.createSession()');
|
|
596
|
+
agentLog(`ChatSession created: ${chatSessionId}`);
|
|
597
|
+
// Check if cleanup started during createSession() (fixes zombie agent race)
|
|
598
|
+
if (await shouldAbortInitForCleanup(agent, 'createSession()')) {
|
|
599
|
+
return undefined;
|
|
600
|
+
}
|
|
601
|
+
return agent;
|
|
602
|
+
}
|
|
603
|
+
/**
|
|
604
|
+
* Finalize agent initialization by setting up event forwarding, creating task processor,
|
|
605
|
+
* and updating cached credentials.
|
|
606
|
+
*/
|
|
607
|
+
function finalizeAgentInitialization(params) {
|
|
608
|
+
const { agent, authToken, brvConfig, curateExecutor, queryExecutor } = params;
|
|
609
|
+
// Setup event forwarding
|
|
610
|
+
setupAgentEventForwarding(agent);
|
|
611
|
+
cipherAgent = agent;
|
|
612
|
+
// Create TaskProcessor
|
|
613
|
+
taskProcessor = createTaskProcessor({
|
|
614
|
+
curateExecutor,
|
|
615
|
+
queryExecutor,
|
|
616
|
+
});
|
|
617
|
+
taskProcessor.setAgent(cipherAgent);
|
|
618
|
+
// NOTE: setupTaskExecutor() is called once in startAgent() before tryInitializeAgent()
|
|
619
|
+
// No need to call again here - executor is already set and handles lazy init
|
|
620
|
+
// Mark as initialized
|
|
621
|
+
isAgentInitialized = true;
|
|
622
|
+
initializationError = undefined;
|
|
623
|
+
// Cache credentials for change detection polling
|
|
624
|
+
updateCachedCredentials(authToken.accessToken, authToken.sessionKey, brvConfig ? { spaceId: brvConfig.spaceId, teamId: brvConfig.teamId } : undefined);
|
|
625
|
+
if (brvConfig) {
|
|
626
|
+
agentLog(`Fully initialized with auth and config (team=${brvConfig.teamId}, space=${brvConfig.spaceId})`);
|
|
627
|
+
}
|
|
628
|
+
else {
|
|
629
|
+
agentLog('Initialized with auth only (no project config yet - will reinit when config available)');
|
|
630
|
+
}
|
|
631
|
+
// Broadcast status change to Transport (init success)
|
|
632
|
+
broadcastStatusChange();
|
|
633
|
+
}
|
|
485
634
|
/**
|
|
486
635
|
* Try to initialize/reinitialize the CipherAgent.
|
|
487
636
|
* Called on startup and lazily when tasks arrive but agent is not initialized.
|
|
@@ -516,11 +665,7 @@ async function tryInitializeAgent(forceReinit = false) {
|
|
|
516
665
|
try {
|
|
517
666
|
// If forcing reinit, drain queue and stop existing agent first
|
|
518
667
|
if (forceReinit) {
|
|
519
|
-
|
|
520
|
-
notifyQueuedTasksAboutDropAndClear('credential/config change');
|
|
521
|
-
// Wait for active tasks to complete (with timeout)
|
|
522
|
-
await waitForActiveTasksToComplete(10_000);
|
|
523
|
-
await stopExistingAgentForReinit();
|
|
668
|
+
await prepareForReinit();
|
|
524
669
|
}
|
|
525
670
|
const tokenStore = createTokenStore();
|
|
526
671
|
const configStore = new ProjectConfigStore();
|
|
@@ -534,65 +679,25 @@ async function tryInitializeAgent(forceReinit = false) {
|
|
|
534
679
|
// Create Executors
|
|
535
680
|
const curateExecutor = new CurateExecutor();
|
|
536
681
|
const queryExecutor = new QueryExecutor();
|
|
537
|
-
//
|
|
538
|
-
const
|
|
539
|
-
|
|
540
|
-
|
|
541
|
-
|
|
542
|
-
fileSystem: { workingDirectory: process.cwd() },
|
|
543
|
-
llm: {
|
|
544
|
-
maxIterations: 10,
|
|
545
|
-
maxTokens: 4096,
|
|
546
|
-
temperature: 0.7,
|
|
547
|
-
topK: 10,
|
|
548
|
-
topP: 0.95,
|
|
549
|
-
verbose: false,
|
|
550
|
-
},
|
|
551
|
-
model: DEFAULT_LLM_MODEL,
|
|
552
|
-
projectId: PROJECT,
|
|
553
|
-
sessionKey: authToken.sessionKey,
|
|
554
|
-
};
|
|
682
|
+
// Read provider configuration
|
|
683
|
+
const { modelFromProvider, openRouterApiKey } = await loadProviderConfiguration();
|
|
684
|
+
// Build agent configuration
|
|
685
|
+
const agentConfig = buildAgentConfig(authToken, modelFromProvider, openRouterApiKey);
|
|
686
|
+
// Initialize agent instance
|
|
555
687
|
pendingAgent = new CipherAgent(agentConfig, brvConfig ?? undefined);
|
|
556
|
-
|
|
557
|
-
|
|
558
|
-
agentLog('CipherAgent started');
|
|
559
|
-
// Check if cleanup started during agent.start() (fixes zombie agent race)
|
|
560
|
-
if (await shouldAbortInitForCleanup(pendingAgent, 'agent.start()')) {
|
|
561
|
-
return false;
|
|
562
|
-
}
|
|
563
|
-
// Create ChatSession (also with timeout to prevent hanging)
|
|
564
|
-
chatSessionId = `agent-session-${randomUUID()}`;
|
|
565
|
-
await withTimeout(pendingAgent.createSession(chatSessionId), AGENT_INIT_TIMEOUT_MS, 'CipherAgent.createSession()');
|
|
566
|
-
agentLog(`ChatSession created: ${chatSessionId}`);
|
|
567
|
-
// Check if cleanup started during createSession() (fixes zombie agent race)
|
|
568
|
-
if (await shouldAbortInitForCleanup(pendingAgent, 'createSession()')) {
|
|
688
|
+
const initializedAgent = await initializeAgentInstance(pendingAgent);
|
|
689
|
+
if (!initializedAgent) {
|
|
569
690
|
return false;
|
|
570
691
|
}
|
|
571
|
-
//
|
|
572
|
-
|
|
573
|
-
|
|
574
|
-
|
|
575
|
-
|
|
576
|
-
taskProcessor = createTaskProcessor({
|
|
692
|
+
// Finalize initialization
|
|
693
|
+
finalizeAgentInitialization({
|
|
694
|
+
agent: initializedAgent,
|
|
695
|
+
authToken,
|
|
696
|
+
brvConfig,
|
|
577
697
|
curateExecutor,
|
|
578
698
|
queryExecutor,
|
|
579
699
|
});
|
|
580
|
-
|
|
581
|
-
// NOTE: setupTaskExecutor() is called once in startAgent() before tryInitializeAgent()
|
|
582
|
-
// No need to call again here - executor is already set and handles lazy init
|
|
583
|
-
// Mark as initialized
|
|
584
|
-
isAgentInitialized = true;
|
|
585
|
-
initializationError = undefined;
|
|
586
|
-
// Cache credentials for change detection polling
|
|
587
|
-
updateCachedCredentials(authToken.accessToken, authToken.sessionKey, brvConfig ? { spaceId: brvConfig.spaceId, teamId: brvConfig.teamId } : undefined);
|
|
588
|
-
if (brvConfig) {
|
|
589
|
-
agentLog(`Fully initialized with auth and config (team=${brvConfig.teamId}, space=${brvConfig.spaceId})`);
|
|
590
|
-
}
|
|
591
|
-
else {
|
|
592
|
-
agentLog('Initialized with auth only (no project config yet - will reinit when config available)');
|
|
593
|
-
}
|
|
594
|
-
// Broadcast status change to Transport (init success)
|
|
595
|
-
broadcastStatusChange();
|
|
700
|
+
pendingAgent = undefined; // Clear local ref - cipherAgent now owns it
|
|
596
701
|
return true;
|
|
597
702
|
}
|
|
598
703
|
catch (error) {
|
|
@@ -630,11 +735,11 @@ async function handleTaskExecute(data) {
|
|
|
630
735
|
if (!taskProcessor) {
|
|
631
736
|
agentLog('TaskProcessor not initialized');
|
|
632
737
|
const error = serializeTaskError(new ProcessorNotInitError());
|
|
633
|
-
|
|
738
|
+
sendCriticalEvent('task:error', { error, taskId });
|
|
634
739
|
return;
|
|
635
740
|
}
|
|
636
|
-
// Notify task started
|
|
637
|
-
|
|
741
|
+
// Notify task started - use sendCriticalEvent for reliability
|
|
742
|
+
sendCriticalEvent('task:started', { taskId });
|
|
638
743
|
try {
|
|
639
744
|
// Process task - events stream via agentEventBus subscription
|
|
640
745
|
// Response is forwarded via llmservice:response event (no manual send needed)
|
|
@@ -649,13 +754,15 @@ async function handleTaskExecute(data) {
|
|
|
649
754
|
type,
|
|
650
755
|
});
|
|
651
756
|
// Notify completion with result (required by TaskCompletedEventSchema)
|
|
757
|
+
// Use sendCriticalEvent for retry logic - TUI must know task ended
|
|
652
758
|
agentLog(`Task completed: ${taskId}`);
|
|
653
|
-
|
|
759
|
+
sendCriticalEvent('task:completed', { result, taskId });
|
|
654
760
|
}
|
|
655
761
|
catch (error) {
|
|
656
762
|
const errorData = serializeTaskError(error);
|
|
657
763
|
agentLog(`Task error: ${taskId} - [${errorData.name}] ${errorData.message}`);
|
|
658
|
-
|
|
764
|
+
// Use sendCriticalEvent for retry logic - TUI must know task ended
|
|
765
|
+
sendCriticalEvent('task:error', { error: errorData, taskId });
|
|
659
766
|
}
|
|
660
767
|
}
|
|
661
768
|
/**
|
|
@@ -670,8 +777,8 @@ function handleTaskCancel(data) {
|
|
|
670
777
|
if (result.wasQueued) {
|
|
671
778
|
// Task was in queue, not yet processing - removed by queue manager
|
|
672
779
|
agentLog(`Task ${taskId} removed from ${result.taskType} queue (was waiting)`);
|
|
673
|
-
// Notify transport that task was cancelled
|
|
674
|
-
|
|
780
|
+
// Notify transport that task was cancelled - use retry for reliability
|
|
781
|
+
sendCriticalEvent('task:cancelled', { taskId });
|
|
675
782
|
}
|
|
676
783
|
else {
|
|
677
784
|
// Task is currently processing - cancel via taskProcessor
|
|
@@ -733,12 +840,13 @@ async function startAgent() {
|
|
|
733
840
|
agentLog('Initial setup incomplete - will retry when tasks arrive (lazy init)');
|
|
734
841
|
}
|
|
735
842
|
// Setup event handlers - TaskQueueManager handles queueing and deduplication
|
|
843
|
+
// These handlers are registered once and persist for the worker's lifetime
|
|
736
844
|
transportClient.on('task:execute', (data) => {
|
|
737
845
|
// Reject tasks during reinit to prevent TOCTOU race condition
|
|
738
846
|
if (isReinitializing) {
|
|
739
847
|
agentLog(`Task ${data.taskId} rejected - agent reinitializing`);
|
|
740
848
|
const error = serializeTaskError(new AgentNotInitializedError('Agent is reinitializing'));
|
|
741
|
-
|
|
849
|
+
sendCriticalEvent('task:error', { error, taskId: data.taskId });
|
|
742
850
|
return;
|
|
743
851
|
}
|
|
744
852
|
const result = taskQueueManager.enqueue(data);
|
|
@@ -42,6 +42,12 @@ export declare class TransportHandlers {
|
|
|
42
42
|
private agentClientId;
|
|
43
43
|
/** Cached agent status from last status:changed broadcast */
|
|
44
44
|
private cachedAgentStatus;
|
|
45
|
+
/**
|
|
46
|
+
* Track recently completed tasks for grace period.
|
|
47
|
+
* Allows late-arriving llmservice:* events to be routed even after task:completed.
|
|
48
|
+
* Key: taskId, Value: {task: TaskInfo, completedAt: timestamp}
|
|
49
|
+
*/
|
|
50
|
+
private completedTasks;
|
|
45
51
|
/** Track active tasks */
|
|
46
52
|
private tasks;
|
|
47
53
|
/** Transport server reference */
|
|
@@ -55,6 +61,11 @@ export declare class TransportHandlers {
|
|
|
55
61
|
* Setup all message handlers.
|
|
56
62
|
*/
|
|
57
63
|
setup(): void;
|
|
64
|
+
/**
|
|
65
|
+
* Get task info from either active or recently completed tasks.
|
|
66
|
+
* Returns undefined if task is not found in either map.
|
|
67
|
+
*/
|
|
68
|
+
private getTaskInfo;
|
|
58
69
|
/**
|
|
59
70
|
* Handle Agent registration.
|
|
60
71
|
* Agent connects as Socket.IO client and sends 'agent:register'.
|
|
@@ -70,12 +81,13 @@ export declare class TransportHandlers {
|
|
|
70
81
|
/**
|
|
71
82
|
* Handle task:cancelled from Agent.
|
|
72
83
|
* Terminal event: task was cancelled before completion.
|
|
73
|
-
* Route to task owner + broadcast-room, then cleanup.
|
|
84
|
+
* Route to task owner + broadcast-room, then cleanup with grace period.
|
|
74
85
|
*/
|
|
75
86
|
private handleTaskCancelled;
|
|
76
87
|
/**
|
|
77
88
|
* Handle task:completed from Agent.
|
|
78
89
|
* Route directly to task owner + broadcast-room for monitoring.
|
|
90
|
+
* Uses grace period cleanup to allow late-arriving llmservice:* events.
|
|
79
91
|
*/
|
|
80
92
|
private handleTaskCompleted;
|
|
81
93
|
/**
|
|
@@ -86,6 +98,7 @@ export declare class TransportHandlers {
|
|
|
86
98
|
/**
|
|
87
99
|
* Handle task:error from Agent.
|
|
88
100
|
* Route directly to task owner + broadcast-room for monitoring.
|
|
101
|
+
* Uses grace period cleanup to allow late-arriving llmservice:* events.
|
|
89
102
|
*/
|
|
90
103
|
private handleTaskError;
|
|
91
104
|
/**
|
|
@@ -93,6 +106,11 @@ export declare class TransportHandlers {
|
|
|
93
106
|
* Route directly to task owner + broadcast-room for monitoring.
|
|
94
107
|
*/
|
|
95
108
|
private handleTaskStarted;
|
|
109
|
+
/**
|
|
110
|
+
* Move a task to the completed tasks map with grace period cleanup.
|
|
111
|
+
* This allows late-arriving llmservice:* events to still be routed.
|
|
112
|
+
*/
|
|
113
|
+
private moveToCompleted;
|
|
96
114
|
private registerLlmEvent;
|
|
97
115
|
/**
|
|
98
116
|
* Generic handler for routing LLM events from Agent to clients.
|
|
@@ -100,9 +118,12 @@ export declare class TransportHandlers {
|
|
|
100
118
|
*
|
|
101
119
|
* All llmservice:* events follow the same routing pattern:
|
|
102
120
|
* 1. Extract taskId from payload
|
|
103
|
-
* 2. Check if task is
|
|
104
|
-
* 3. Send to task owner + broadcast-room if
|
|
105
|
-
* 4. Drop silently if task
|
|
121
|
+
* 2. Check if task is active OR recently completed (within grace period)
|
|
122
|
+
* 3. Send to task owner + broadcast-room if found
|
|
123
|
+
* 4. Drop silently if task not found (truly ended beyond grace period)
|
|
124
|
+
*
|
|
125
|
+
* The grace period allows late-arriving events (due to network delays or
|
|
126
|
+
* out-of-order delivery) to still be routed to clients.
|
|
106
127
|
*/
|
|
107
128
|
private routeLlmEvent;
|
|
108
129
|
/**
|
|
@@ -39,6 +39,15 @@ import { isValidTaskType } from '../../utils/type-guards.js';
|
|
|
39
39
|
// - TaskStartedEvent, TaskCompletedEvent, TaskErrorEvent: Agent → Transport (task lifecycle events)
|
|
40
40
|
// - LlmThinkingEvent, LlmChunkEvent, LlmResponseEvent, etc: Agent → Transport (LLM events)
|
|
41
41
|
// ============================================================================
|
|
42
|
+
// Constants
|
|
43
|
+
// ============================================================================
|
|
44
|
+
/**
|
|
45
|
+
* Grace period (in ms) to keep completed tasks in memory for late-arriving events.
|
|
46
|
+
* This prevents silent event drops when llmservice:* events arrive after task:completed.
|
|
47
|
+
* Inspired by opencode's session callback queuing pattern.
|
|
48
|
+
*/
|
|
49
|
+
const TASK_CLEANUP_GRACE_PERIOD_MS = 5000;
|
|
50
|
+
// ============================================================================
|
|
42
51
|
// Transport Handlers
|
|
43
52
|
// ============================================================================
|
|
44
53
|
/**
|
|
@@ -52,6 +61,12 @@ export class TransportHandlers {
|
|
|
52
61
|
agentClientId;
|
|
53
62
|
/** Cached agent status from last status:changed broadcast */
|
|
54
63
|
cachedAgentStatus;
|
|
64
|
+
/**
|
|
65
|
+
* Track recently completed tasks for grace period.
|
|
66
|
+
* Allows late-arriving llmservice:* events to be routed even after task:completed.
|
|
67
|
+
* Key: taskId, Value: {task: TaskInfo, completedAt: timestamp}
|
|
68
|
+
*/
|
|
69
|
+
completedTasks = new Map();
|
|
55
70
|
/** Track active tasks */
|
|
56
71
|
tasks = new Map();
|
|
57
72
|
/** Transport server reference */
|
|
@@ -64,6 +79,7 @@ export class TransportHandlers {
|
|
|
64
79
|
*/
|
|
65
80
|
cleanup() {
|
|
66
81
|
this.tasks.clear();
|
|
82
|
+
this.completedTasks.clear();
|
|
67
83
|
this.agentClientId = undefined;
|
|
68
84
|
this.cachedAgentStatus = undefined;
|
|
69
85
|
}
|
|
@@ -76,6 +92,13 @@ export class TransportHandlers {
|
|
|
76
92
|
this.setupClientHandlers();
|
|
77
93
|
this.setupAgentControlHandlers();
|
|
78
94
|
}
|
|
95
|
+
/**
|
|
96
|
+
* Get task info from either active or recently completed tasks.
|
|
97
|
+
* Returns undefined if task is not found in either map.
|
|
98
|
+
*/
|
|
99
|
+
getTaskInfo(taskId) {
|
|
100
|
+
return this.tasks.get(taskId) ?? this.completedTasks.get(taskId)?.task;
|
|
101
|
+
}
|
|
79
102
|
/**
|
|
80
103
|
* Handle Agent registration.
|
|
81
104
|
* Agent connects as Socket.IO client and sends 'agent:register'.
|
|
@@ -119,7 +142,7 @@ export class TransportHandlers {
|
|
|
119
142
|
/**
|
|
120
143
|
* Handle task:cancelled from Agent.
|
|
121
144
|
* Terminal event: task was cancelled before completion.
|
|
122
|
-
* Route to task owner + broadcast-room, then cleanup.
|
|
145
|
+
* Route to task owner + broadcast-room, then cleanup with grace period.
|
|
123
146
|
*/
|
|
124
147
|
handleTaskCancelled(data) {
|
|
125
148
|
const { taskId } = data;
|
|
@@ -129,11 +152,13 @@ export class TransportHandlers {
|
|
|
129
152
|
this.transport.sendTo(task.clientId, TransportTaskEventNames.CANCELLED, { taskId });
|
|
130
153
|
}
|
|
131
154
|
this.transport.broadcastTo('broadcast-room', TransportTaskEventNames.CANCELLED, { taskId });
|
|
132
|
-
|
|
155
|
+
// Move to completed tasks with grace period instead of immediate deletion
|
|
156
|
+
this.moveToCompleted(taskId);
|
|
133
157
|
}
|
|
134
158
|
/**
|
|
135
159
|
* Handle task:completed from Agent.
|
|
136
160
|
* Route directly to task owner + broadcast-room for monitoring.
|
|
161
|
+
* Uses grace period cleanup to allow late-arriving llmservice:* events.
|
|
137
162
|
*/
|
|
138
163
|
handleTaskCompleted(data) {
|
|
139
164
|
const { result, taskId } = data;
|
|
@@ -143,7 +168,9 @@ export class TransportHandlers {
|
|
|
143
168
|
this.transport.sendTo(task.clientId, TransportTaskEventNames.COMPLETED, { result, taskId });
|
|
144
169
|
}
|
|
145
170
|
this.transport.broadcastTo('broadcast-room', TransportTaskEventNames.COMPLETED, { result, taskId });
|
|
146
|
-
|
|
171
|
+
// Move to completed tasks with grace period instead of immediate deletion
|
|
172
|
+
// This allows late-arriving llmservice:* events to still be routed
|
|
173
|
+
this.moveToCompleted(taskId);
|
|
147
174
|
}
|
|
148
175
|
/**
|
|
149
176
|
* Handle task:create request from client.
|
|
@@ -221,6 +248,7 @@ export class TransportHandlers {
|
|
|
221
248
|
/**
|
|
222
249
|
* Handle task:error from Agent.
|
|
223
250
|
* Route directly to task owner + broadcast-room for monitoring.
|
|
251
|
+
* Uses grace period cleanup to allow late-arriving llmservice:* events.
|
|
224
252
|
*/
|
|
225
253
|
handleTaskError(data) {
|
|
226
254
|
const { error, taskId } = data;
|
|
@@ -230,7 +258,8 @@ export class TransportHandlers {
|
|
|
230
258
|
this.transport.sendTo(task.clientId, TransportTaskEventNames.ERROR, { error, taskId });
|
|
231
259
|
}
|
|
232
260
|
this.transport.broadcastTo('broadcast-room', TransportTaskEventNames.ERROR, { error, taskId });
|
|
233
|
-
|
|
261
|
+
// Move to completed tasks with grace period instead of immediate deletion
|
|
262
|
+
this.moveToCompleted(taskId);
|
|
234
263
|
}
|
|
235
264
|
/**
|
|
236
265
|
* Handle task:started from Agent.
|
|
@@ -257,6 +286,21 @@ export class TransportHandlers {
|
|
|
257
286
|
this.transport.broadcastTo('broadcast-room', TransportTaskEventNames.STARTED, { taskId });
|
|
258
287
|
}
|
|
259
288
|
}
|
|
289
|
+
/**
|
|
290
|
+
* Move a task to the completed tasks map with grace period cleanup.
|
|
291
|
+
* This allows late-arriving llmservice:* events to still be routed.
|
|
292
|
+
*/
|
|
293
|
+
moveToCompleted(taskId) {
|
|
294
|
+
const task = this.tasks.get(taskId);
|
|
295
|
+
if (task) {
|
|
296
|
+
this.completedTasks.set(taskId, { completedAt: Date.now(), task });
|
|
297
|
+
this.tasks.delete(taskId);
|
|
298
|
+
// Schedule cleanup after grace period
|
|
299
|
+
setTimeout(() => {
|
|
300
|
+
this.completedTasks.delete(taskId);
|
|
301
|
+
}, TASK_CLEANUP_GRACE_PERIOD_MS);
|
|
302
|
+
}
|
|
303
|
+
}
|
|
260
304
|
registerLlmEvent(eventName) {
|
|
261
305
|
this.transport.onRequest(eventName, (data) => {
|
|
262
306
|
this.routeLlmEvent(eventName, data);
|
|
@@ -268,15 +312,18 @@ export class TransportHandlers {
|
|
|
268
312
|
*
|
|
269
313
|
* All llmservice:* events follow the same routing pattern:
|
|
270
314
|
* 1. Extract taskId from payload
|
|
271
|
-
* 2. Check if task is
|
|
272
|
-
* 3. Send to task owner + broadcast-room if
|
|
273
|
-
* 4. Drop silently if task
|
|
315
|
+
* 2. Check if task is active OR recently completed (within grace period)
|
|
316
|
+
* 3. Send to task owner + broadcast-room if found
|
|
317
|
+
* 4. Drop silently if task not found (truly ended beyond grace period)
|
|
318
|
+
*
|
|
319
|
+
* The grace period allows late-arriving events (due to network delays or
|
|
320
|
+
* out-of-order delivery) to still be routed to clients.
|
|
274
321
|
*/
|
|
275
322
|
routeLlmEvent(eventName, data) {
|
|
276
323
|
const { taskId, ...rest } = data;
|
|
277
|
-
|
|
278
|
-
|
|
279
|
-
//
|
|
324
|
+
// Use getTaskInfo to check both active and recently completed tasks
|
|
325
|
+
const task = this.getTaskInfo(taskId);
|
|
326
|
+
// Guard: Drop events for tasks not found in either active or completed maps
|
|
280
327
|
if (!task) {
|
|
281
328
|
return;
|
|
282
329
|
}
|
|
@@ -41,9 +41,9 @@ export const connectorsCommand = {
|
|
|
41
41
|
},
|
|
42
42
|
type: 'streaming',
|
|
43
43
|
}),
|
|
44
|
-
aliases: [],
|
|
44
|
+
aliases: ['connectors'],
|
|
45
45
|
autoExecute: true,
|
|
46
46
|
description: 'Manage agent connectors (rules, hook, mcp, or skill)',
|
|
47
47
|
kind: CommandKind.BUILT_IN,
|
|
48
|
-
name: '
|
|
48
|
+
name: 'connector',
|
|
49
49
|
};
|
|
@@ -3,7 +3,9 @@ import { curateCommand } from './curate-command.js';
|
|
|
3
3
|
import { initCommand } from './init-command.js';
|
|
4
4
|
import { loginCommand } from './login-command.js';
|
|
5
5
|
import { logoutCommand } from './logout-command.js';
|
|
6
|
+
import { modelCommand } from './model-command.js';
|
|
6
7
|
import { newCommand } from './new-command.js';
|
|
8
|
+
import { providerCommand } from './provider-command.js';
|
|
7
9
|
import { pullCommand } from './pull-command.js';
|
|
8
10
|
import { pushCommand } from './push-command.js';
|
|
9
11
|
import { queryCommand } from './query-command.js';
|
|
@@ -23,6 +25,9 @@ export const load = () => [
|
|
|
23
25
|
queryCommand, // Query context tree
|
|
24
26
|
// Connectors management
|
|
25
27
|
connectorsCommand, // Manage agent connectors (rules/hook)
|
|
28
|
+
// Provider management
|
|
29
|
+
providerCommand, // Connect to LLM providers
|
|
30
|
+
modelCommand, // Select model from provider
|
|
26
31
|
// Sync operations
|
|
27
32
|
pushCommand, // Push to cloud
|
|
28
33
|
pullCommand, // Pull from cloud
|
|
@@ -0,0 +1,13 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Model Command
|
|
3
|
+
*
|
|
4
|
+
* Interactive command for selecting LLM models from the active provider.
|
|
5
|
+
* Uses the streaming command pattern with inline prompts.
|
|
6
|
+
*
|
|
7
|
+
* Usage: /model
|
|
8
|
+
*/
|
|
9
|
+
import type { SlashCommand } from '../../../tui/types.js';
|
|
10
|
+
/**
|
|
11
|
+
* Model command definition.
|
|
12
|
+
*/
|
|
13
|
+
export declare const modelCommand: SlashCommand;
|