@cereworker/cli 26.329.11 → 26.329.13

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.
@@ -0,0 +1,181 @@
1
+ import { mkdtempSync, readFileSync, rmSync } from 'node:fs';
2
+ import { tmpdir } from 'node:os';
3
+ import { join } from 'node:path';
4
+ import { afterEach, beforeEach, describe, expect, it, vi } from 'vitest';
5
+ import { configSchema } from '@cereworker/config';
6
+ import { createService } from './service.js';
7
+ import { buildChannelConversationKey } from './channel-conversations.js';
8
+ function makeConfig(overrides = {}) {
9
+ return configSchema.parse({
10
+ cerebrum: {
11
+ defaultProvider: 'local',
12
+ defaultModel: 'llama3.3',
13
+ providers: {
14
+ local: {
15
+ baseUrl: 'http://127.0.0.1:11434',
16
+ model: 'llama3.3',
17
+ },
18
+ },
19
+ streamStallThreshold: 1,
20
+ maxNudgeRetries: 1,
21
+ },
22
+ cerebellum: {
23
+ enabled: false,
24
+ verification: { enabled: false },
25
+ finetune: { enabled: false },
26
+ },
27
+ hippocampus: { enabled: false },
28
+ proactive: { enabled: false },
29
+ subAgents: { enabled: false },
30
+ tools: {
31
+ shell: { enabled: false },
32
+ fileOps: { enabled: false },
33
+ http: { enabled: false },
34
+ web: { enabled: false },
35
+ browser: { enabled: false },
36
+ },
37
+ channels: { dmPolicy: 'open' },
38
+ ...overrides,
39
+ });
40
+ }
41
+ function createWatchdogCerebellum() {
42
+ return {
43
+ isConnected: vi.fn(() => true),
44
+ verifyToolResult: vi.fn(async () => ({ passed: false, checks: [], modelVerdict: false })),
45
+ ingestTrainingData: vi.fn(async () => 0),
46
+ startFineTune: vi.fn(async () => ({ jobId: '', started: false, error: '' })),
47
+ getFineTuneStatus: vi.fn(async () => null),
48
+ };
49
+ }
50
+ describe('createService integration', () => {
51
+ let homeDir;
52
+ beforeEach(() => {
53
+ homeDir = mkdtempSync(join(tmpdir(), 'cereworker-service-'));
54
+ vi.stubEnv('HOME', homeDir);
55
+ });
56
+ afterEach(async () => {
57
+ vi.useRealTimers();
58
+ vi.unstubAllEnvs();
59
+ rmSync(homeDir, { recursive: true, force: true });
60
+ });
61
+ it('retries a stalled stream through the real service bridge without surfacing a false error', async () => {
62
+ vi.useFakeTimers();
63
+ const service = createService(makeConfig());
64
+ const cerebellum = createWatchdogCerebellum();
65
+ service.orchestrator.setCerebellum(cerebellum, { enabled: false });
66
+ let attempts = 0;
67
+ service.cerebrum.stream = vi.fn(async (_messages, _tools, callbacks, options) => {
68
+ attempts++;
69
+ if (attempts === 1) {
70
+ await new Promise((_resolve, reject) => {
71
+ const signal = options?.abortSignal;
72
+ if (!signal) {
73
+ reject(new Error('missing abort signal'));
74
+ return;
75
+ }
76
+ const onAbort = () => {
77
+ callbacks.onError(new Error('intentional nudge abort'));
78
+ reject(new Error('intentional nudge abort'));
79
+ };
80
+ if (signal.aborted) {
81
+ onAbort();
82
+ return;
83
+ }
84
+ signal.addEventListener('abort', onAbort, { once: true });
85
+ });
86
+ return;
87
+ }
88
+ callbacks.onChunk('Recovered reply');
89
+ callbacks.onFinish('Recovered reply');
90
+ });
91
+ const errors = [];
92
+ const nudges = [];
93
+ service.orchestrator.on('error', ({ error }) => errors.push(error));
94
+ service.orchestrator.on('cerebrum:stall:nudge', ({ attempt }) => nudges.push(attempt));
95
+ const sendPromise = service.orchestrator.sendMessage('hello from watchdog');
96
+ await vi.advanceTimersByTimeAsync(15_000);
97
+ await sendPromise;
98
+ const messages = service.orchestrator.getMessages();
99
+ expect(attempts).toBe(2);
100
+ expect(errors).toEqual([]);
101
+ expect(nudges).toEqual([1]);
102
+ expect(messages.slice(-3).map((message) => [message.role, message.content])).toEqual([
103
+ ['user', 'hello from watchdog'],
104
+ ['system', '[Cerebellum] You stopped mid-response. Continue from where you left off.'],
105
+ ['cerebrum', 'Recovered reply'],
106
+ ]);
107
+ await service.shutdown();
108
+ });
109
+ it('keeps short-term channel conversations separate while persisting the session map', async () => {
110
+ const service = createService(makeConfig());
111
+ service.cerebrum.stream = vi.fn(async (messages, _tools, callbacks) => {
112
+ const lastUser = [...messages].reverse().find((message) => message.role === 'user');
113
+ callbacks.onFinish(`reply:${lastUser?.content ?? ''}`);
114
+ });
115
+ let inboundHandler = null;
116
+ let connected = false;
117
+ const fakeChannel = {
118
+ id: 'discord',
119
+ meta: { name: 'Discord', emoji: '💬' },
120
+ start: vi.fn(async (handler) => {
121
+ inboundHandler = handler;
122
+ connected = true;
123
+ }),
124
+ stop: vi.fn(async () => {
125
+ connected = false;
126
+ }),
127
+ send: vi.fn(async () => { }),
128
+ isAllowed: vi.fn(() => true),
129
+ isConnected: vi.fn(() => connected),
130
+ };
131
+ service.channelManager.register(fakeChannel);
132
+ await service.startChannels();
133
+ expect(inboundHandler).not.toBeNull();
134
+ const messageA1 = {
135
+ channelId: 'discord',
136
+ senderId: 'user-a',
137
+ senderName: 'Alice',
138
+ sessionId: 'dm:user-a',
139
+ text: 'hello from a',
140
+ timestamp: Date.now(),
141
+ };
142
+ const messageB1 = {
143
+ channelId: 'discord',
144
+ senderId: 'user-b',
145
+ senderName: 'Bob',
146
+ sessionId: 'dm:user-b',
147
+ text: 'hello from b',
148
+ timestamp: Date.now() + 1,
149
+ };
150
+ const messageA2 = {
151
+ ...messageA1,
152
+ text: 'follow up from a',
153
+ timestamp: Date.now() + 2,
154
+ };
155
+ await expect(inboundHandler(messageA1)).resolves.toBe('reply:hello from a');
156
+ await expect(inboundHandler(messageB1)).resolves.toBe('reply:hello from b');
157
+ await expect(inboundHandler(messageA2)).resolves.toBe('reply:follow up from a');
158
+ const mapPath = join(homeDir, '.cereworker', 'channel-conversations.json');
159
+ const savedMap = JSON.parse(readFileSync(mapPath, 'utf-8'));
160
+ const keyA = buildChannelConversationKey(messageA1);
161
+ const keyB = buildChannelConversationKey(messageB1);
162
+ expect(savedMap[keyA]).toBeTruthy();
163
+ expect(savedMap[keyB]).toBeTruthy();
164
+ expect(savedMap[keyA]).not.toBe(savedMap[keyB]);
165
+ const conversationStore = service.orchestrator.getConversationStore();
166
+ const conversationA = conversationStore.get(savedMap[keyA]);
167
+ const conversationB = conversationStore.get(savedMap[keyB]);
168
+ expect(conversationA?.messages.map((message) => message.content)).toEqual([
169
+ 'hello from a',
170
+ 'reply:hello from a',
171
+ 'follow up from a',
172
+ 'reply:follow up from a',
173
+ ]);
174
+ expect(conversationB?.messages.map((message) => message.content)).toEqual([
175
+ 'hello from b',
176
+ 'reply:hello from b',
177
+ ]);
178
+ await service.shutdown();
179
+ });
180
+ });
181
+ //# sourceMappingURL=service.integration.test.js.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"service.integration.test.js","sourceRoot":"","sources":["../src/service.integration.test.ts"],"names":[],"mappings":"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"}
package/dist/service.js CHANGED
@@ -9,13 +9,22 @@ import { CerebrumProvider, createBuiltinTools } from '@cereworker/cerebrum';
9
9
  import { createChannelManager } from '@cereworker/channels';
10
10
  import { createBrowserTools, PuppeteerBackend, CdpBackend, BrowserRelay, ExtensionBackend } from '@cereworker/browser';
11
11
  import { loadSkills, filterEligibleSkills, SkillRegistry } from '@cereworker/skills';
12
- import { parseCommand, handleSlashCommand } from './commands.js';
12
+ import { parseCommand, handleSlashCommand, CHANNEL_COMMANDS } from './commands.js';
13
13
  import { HippocampusStore, HippocampusCurator, ConversationExtractor, createMemoryTools, memoryReadParameters, memoryWriteParameters, memoryLogParameters, memorySearchParameters, } from '@cereworker/hippocampus';
14
14
  import { GatewayServer, GatewayNodeClient, createProxyTools } from '@cereworker/gateway';
15
15
  import { buildCerebellumComposeCommand, buildCerebellumComposeEnv, resolveCerebellumDockerModel, } from './cerebellum-docker.js';
16
16
  import { buildChannelConversationKey, loadChannelConversationState, saveChannelConversationState, } from './channel-conversations.js';
17
17
  const log = createLogger('service');
18
- export function createService(config) {
18
+ export function createService(config, deps = {}) {
19
+ const execSyncImpl = deps.execSync ?? execSync;
20
+ const execFileSyncImpl = deps.execFileSync ?? execFileSync;
21
+ const spawnImpl = deps.spawn ?? spawn;
22
+ const homeDir = deps.homeDir ?? homedir;
23
+ const createCerebrumImpl = deps.createCerebrum
24
+ ?? ((providerConfig, options) => new CerebrumProvider(providerConfig, options));
25
+ const createChannelManagerImpl = deps.createChannelManager ?? createChannelManager;
26
+ const createCerebellumClientImpl = deps.createCerebellumClient
27
+ ?? ((address) => new CerebellumClient(address));
19
28
  // Create persistent conversation store
20
29
  const conversationStore = new ConversationStore();
21
30
  // Instance identity
@@ -62,7 +71,7 @@ export function createService(config) {
62
71
  maxSteps: config.cerebrum.maxSteps,
63
72
  temperature: config.cerebrum.temperature,
64
73
  };
65
- const cerebrum = new CerebrumProvider(cerebrumConfig, {
74
+ const cerebrum = createCerebrumImpl(cerebrumConfig, {
66
75
  denyList: config.tools.shell.denyList,
67
76
  timeout: config.tools.shell.timeout,
68
77
  maxOutputSize: config.tools.shell.maxOutputSize,
@@ -86,7 +95,7 @@ export function createService(config) {
86
95
  const skillRegistry = new SkillRegistry();
87
96
  const skillDirs = [
88
97
  ...config.skills.directories,
89
- join(homedir(), '.cereworker', 'skills'),
98
+ join(homeDir(), '.cereworker', 'skills'),
90
99
  join(process.cwd(), 'skills'),
91
100
  ];
92
101
  const allSkills = loadSkills(skillDirs);
@@ -104,7 +113,7 @@ export function createService(config) {
104
113
  log.info('Recurring tasks configured', { count: enabledTasks.length });
105
114
  }
106
115
  // Load persisted task state (conversationId mappings)
107
- const taskStateFile = join(homedir(), '.cereworker', 'task-state.json');
116
+ const taskStateFile = join(homeDir(), '.cereworker', 'task-state.json');
108
117
  let taskState = {};
109
118
  try {
110
119
  if (existsSync(taskStateFile)) {
@@ -129,7 +138,7 @@ export function createService(config) {
129
138
  log.warn('Failed to save task state', { error: err.message });
130
139
  }
131
140
  }
132
- const channelConversationStateFile = join(homedir(), '.cereworker', 'channel-conversations.json');
141
+ const channelConversationStateFile = join(homeDir(), '.cereworker', 'channel-conversations.json');
133
142
  let channelConversationState = loadChannelConversationState(channelConversationStateFile);
134
143
  for (const [sessionKey, conversationId] of Object.entries(channelConversationState)) {
135
144
  if (!conversationStore.get(conversationId)) {
@@ -296,7 +305,7 @@ export function createService(config) {
296
305
  }
297
306
  }
298
307
  // Create channel manager
299
- const channelManager = createChannelManager(config);
308
+ const channelManager = createChannelManagerImpl(config, CHANNEL_COMMANDS);
300
309
  // Create pairing store and wire to channel manager
301
310
  const pairingStore = new PairingStore();
302
311
  channelManager.setDmPolicy(config.channels.dmPolicy);
@@ -373,7 +382,7 @@ export function createService(config) {
373
382
  log.info('Discovery completed, instance profile updated', { name: result.name });
374
383
  // Write training pairs from the discovery conversation to pending.jsonl
375
384
  try {
376
- const pendingPath = join(homedir(), '.cereworker', 'finetune', 'pending.jsonl');
385
+ const pendingPath = join(homeDir(), '.cereworker', 'finetune', 'pending.jsonl');
377
386
  mkdirSync(dirname(pendingPath), { recursive: true });
378
387
  const messages = orchestrator.getMessages();
379
388
  for (let i = 0; i < messages.length - 1; i++) {
@@ -457,14 +466,14 @@ export function createService(config) {
457
466
  // Check if Docker is installed (check PATH, then common locations)
458
467
  let dockerBin = '';
459
468
  try {
460
- dockerBin = execSync('which docker', { stdio: 'pipe' }).toString().trim();
469
+ dockerBin = execSyncImpl('which docker', { stdio: 'pipe' }).toString().trim();
461
470
  }
462
471
  catch {
463
472
  // Not in PATH — check common install locations
464
473
  const candidates = ['/usr/bin/docker', '/usr/local/bin/docker', '/snap/bin/docker'];
465
474
  for (const c of candidates) {
466
475
  try {
467
- execSync(`test -x ${c}`, { stdio: 'pipe' });
476
+ execSyncImpl(`test -x ${c}`, { stdio: 'pipe' });
468
477
  dockerBin = c;
469
478
  break;
470
479
  }
@@ -477,7 +486,7 @@ export function createService(config) {
477
486
  }
478
487
  // Try without sudo first
479
488
  try {
480
- execSync(`${dockerBin} info`, { stdio: 'pipe', timeout: 10_000 });
489
+ execSyncImpl(`${dockerBin} info`, { stdio: 'pipe', timeout: 10_000 });
481
490
  return true;
482
491
  }
483
492
  catch (err) {
@@ -486,9 +495,9 @@ export function createService(config) {
486
495
  if (msg.includes('Is the docker daemon running') || msg.includes('Cannot connect to the Docker daemon')) {
487
496
  // Try to start the service
488
497
  try {
489
- execSync('sudo -n systemctl start docker', { stdio: 'pipe', timeout: 15_000 });
498
+ execSyncImpl('sudo -n systemctl start docker', { stdio: 'pipe', timeout: 15_000 });
490
499
  log.info('Started Docker service');
491
- execSync(`${dockerBin} info`, { stdio: 'pipe', timeout: 10_000 });
500
+ execSyncImpl(`${dockerBin} info`, { stdio: 'pipe', timeout: 10_000 });
492
501
  return true;
493
502
  }
494
503
  catch {
@@ -498,7 +507,7 @@ export function createService(config) {
498
507
  }
499
508
  // Permission denied — try with sudo
500
509
  try {
501
- execSync(`sudo -n ${dockerBin} info`, { stdio: 'pipe', timeout: 10_000 });
510
+ execSyncImpl(`sudo -n ${dockerBin} info`, { stdio: 'pipe', timeout: 10_000 });
502
511
  dockerPrefix = 'sudo ';
503
512
  log.info('Using sudo for Docker commands (add user to docker group to avoid this)');
504
513
  return true;
@@ -508,7 +517,7 @@ export function createService(config) {
508
517
  const user = process.env.USER || process.env.LOGNAME;
509
518
  if (user) {
510
519
  try {
511
- execSync(`sudo -n usermod -aG docker ${user}`, { stdio: 'pipe' });
520
+ execSyncImpl(`sudo -n usermod -aG docker ${user}`, { stdio: 'pipe' });
512
521
  dockerPrefix = 'sudo ';
513
522
  log.info('Added user to docker group. Using sudo for this session — re-login to use Docker without sudo.');
514
523
  return true;
@@ -525,13 +534,13 @@ export function createService(config) {
525
534
  function ensureImageExists() {
526
535
  const image = config.cerebellum.docker.image;
527
536
  try {
528
- const exists = execSync(`${dockerPrefix}docker images -q ${image}`, { stdio: 'pipe' }).toString().trim();
537
+ const exists = execSyncImpl(`${dockerPrefix}docker images -q ${image}`, { stdio: 'pipe' }).toString().trim();
529
538
  if (exists) {
530
539
  // Image exists — try background pull for updates (non-blocking)
531
540
  try {
532
541
  const pullCmd = dockerPrefix ? 'sudo' : 'docker';
533
542
  const pullArgs = dockerPrefix ? ['docker', 'pull', image] : ['pull', image];
534
- const child = spawn(pullCmd, pullArgs, { stdio: ['ignore', 'pipe', 'ignore'], detached: true });
543
+ const child = spawnImpl(pullCmd, pullArgs, { stdio: ['ignore', 'pipe', 'ignore'], detached: true });
535
544
  child.unref();
536
545
  let pullOutput = '';
537
546
  child.stdout?.on('data', (data) => { pullOutput += data.toString(); });
@@ -553,7 +562,7 @@ export function createService(config) {
553
562
  // Image missing — pull synchronously
554
563
  try {
555
564
  log.info(`Pulling Cerebellum image ${image} from Docker Hub...`);
556
- execSync(`${dockerPrefix}docker pull ${image}`, { stdio: 'pipe', timeout: 3_600_000 });
565
+ execSyncImpl(`${dockerPrefix}docker pull ${image}`, { stdio: 'pipe', timeout: 3_600_000 });
557
566
  log.info('Cerebellum image pulled from Docker Hub');
558
567
  return true;
559
568
  }
@@ -565,7 +574,7 @@ export function createService(config) {
565
574
  if (composeFile) {
566
575
  log.info('Building Cerebellum Docker image from source...');
567
576
  try {
568
- execSync(`${dockerPrefix}docker compose -f "${composeFile}" build cerebellum`, {
577
+ execSyncImpl(`${dockerPrefix}docker compose -f "${composeFile}" build cerebellum`, {
569
578
  cwd: dirname(composeFile),
570
579
  stdio: 'pipe',
571
580
  timeout: 600_000,
@@ -586,7 +595,7 @@ export function createService(config) {
586
595
  const resolvedModel = resolveCerebellumDockerModel(config);
587
596
  const getConfiguredModelPath = () => {
588
597
  try {
589
- const envLines = execSync(`${dockerPrefix}docker inspect -f "{{range .Config.Env}}{{println .}}{{end}}" cereworker-cerebellum`, { stdio: 'pipe' }).toString().trim().split('\n');
598
+ const envLines = execSyncImpl(`${dockerPrefix}docker inspect -f "{{range .Config.Env}}{{println .}}{{end}}" cereworker-cerebellum`, { stdio: 'pipe' }).toString().trim().split('\n');
590
599
  const modelLine = envLines.find((line) => line.startsWith('MODEL_PATH='));
591
600
  return modelLine ? modelLine.slice('MODEL_PATH='.length) : null;
592
601
  }
@@ -596,7 +605,7 @@ export function createService(config) {
596
605
  };
597
606
  // Check if already running
598
607
  try {
599
- const out = execSync(`${dockerPrefix}docker ps -q -f name=cereworker-cerebellum`, {
608
+ const out = execSyncImpl(`${dockerPrefix}docker ps -q -f name=cereworker-cerebellum`, {
600
609
  stdio: 'pipe',
601
610
  }).toString().trim();
602
611
  if (out) {
@@ -609,20 +618,20 @@ export function createService(config) {
609
618
  }
610
619
  // Check if container exists but stopped
611
620
  try {
612
- const stopped = execSync(`${dockerPrefix}docker ps -aq -f name=cereworker-cerebellum`, {
621
+ const stopped = execSyncImpl(`${dockerPrefix}docker ps -aq -f name=cereworker-cerebellum`, {
613
622
  stdio: 'pipe',
614
623
  }).toString().trim();
615
624
  if (stopped) {
616
625
  const configuredModelPath = getConfiguredModelPath();
617
626
  if (configuredModelPath && configuredModelPath !== resolvedModel.modelPath) {
618
- execSync(`${dockerPrefix}docker rm -f cereworker-cerebellum`, { stdio: 'pipe' });
627
+ execSyncImpl(`${dockerPrefix}docker rm -f cereworker-cerebellum`, { stdio: 'pipe' });
619
628
  log.info('Removed stale Cerebellum container to refresh model path', {
620
629
  previousModelPath: configuredModelPath,
621
630
  nextModelPath: resolvedModel.modelPath,
622
631
  });
623
632
  }
624
633
  else {
625
- execSync(`${dockerPrefix}docker start cereworker-cerebellum`, { stdio: 'pipe' });
634
+ execSyncImpl(`${dockerPrefix}docker start cereworker-cerebellum`, { stdio: 'pipe' });
626
635
  log.info('Restarted stopped Cerebellum container');
627
636
  return true;
628
637
  }
@@ -640,7 +649,7 @@ export function createService(config) {
640
649
  try {
641
650
  const composeEnv = buildCerebellumComposeEnv(config);
642
651
  const command = buildCerebellumComposeCommand(composeFile, composeEnv, Boolean(dockerPrefix));
643
- execFileSync(command.command, command.args, {
652
+ execFileSyncImpl(command.command, command.args, {
644
653
  ...(command.env ? { env: command.env } : {}),
645
654
  stdio: 'pipe',
646
655
  cwd: dirname(composeFile),
@@ -660,11 +669,11 @@ export function createService(config) {
660
669
  const port = config.cerebellum.address.split(':')[1] ?? '50051';
661
670
  // Use host models directory if it exists (pre-downloaded during onboarding),
662
671
  // otherwise fall back to Docker named volume
663
- const modelsPath = config.cerebellum.docker.modelsPath.replace(/^~/, homedir());
672
+ const modelsPath = config.cerebellum.docker.modelsPath.replace(/^~/, homeDir());
664
673
  const modelsVolume = existsSync(modelsPath)
665
674
  ? `"${modelsPath}":/root/.cache/huggingface`
666
675
  : 'cerebellum-models:/root/.cache/huggingface';
667
- execSync(`${dockerPrefix}docker run -d --name cereworker-cerebellum` +
676
+ execSyncImpl(`${dockerPrefix}docker run -d --name cereworker-cerebellum` +
668
677
  ` -p ${port}:50051` +
669
678
  ` -e MODEL_PATH=${modelId}` +
670
679
  ` -e HEARTBEAT_INTERVAL=${interval}` +
@@ -691,10 +700,10 @@ export function createService(config) {
691
700
  if (!isDockerAvailable()) {
692
701
  // Determine specific reason
693
702
  try {
694
- execSync('which docker', { stdio: 'pipe' });
703
+ execSyncImpl('which docker', { stdio: 'pipe' });
695
704
  // Docker binary found but daemon not running or permission denied
696
705
  try {
697
- execSync('docker info', { stdio: 'pipe', timeout: 5000 });
706
+ execSyncImpl('docker info', { stdio: 'pipe', timeout: 5000 });
698
707
  }
699
708
  catch (e) {
700
709
  const msg = e.message;
@@ -715,7 +724,7 @@ export function createService(config) {
715
724
  else if (!ensureDockerRunning()) {
716
725
  // Gather container logs for diagnostics
717
726
  try {
718
- const logs = execSync(`${dockerPrefix}docker logs --tail 20 cereworker-cerebellum 2>&1`, { stdio: 'pipe', timeout: 5000 }).toString().trim();
727
+ const logs = execSyncImpl(`${dockerPrefix}docker logs --tail 20 cereworker-cerebellum 2>&1`, { stdio: 'pipe', timeout: 5000 }).toString().trim();
719
728
  dockerReason = `Container failed to start.\nLast logs:\n${logs}`;
720
729
  }
721
730
  catch {
@@ -726,7 +735,7 @@ export function createService(config) {
726
735
  // Connect gRPC client with retries
727
736
  // First run downloads model weights (~1.2 GB for Qwen3 0.6B) and loads them,
728
737
  // which can take 2-5 minutes depending on network and hardware.
729
- const client = new CerebellumClient(config.cerebellum.address);
738
+ const client = createCerebellumClientImpl(config.cerebellum.address);
730
739
  const maxRetries = 60;
731
740
  const retryDelay = 5000;
732
741
  let lastError = '';
@@ -769,7 +778,7 @@ export function createService(config) {
769
778
  else {
770
779
  // Docker started fine but gRPC failed — grab container logs for clues
771
780
  try {
772
- const logs = execSync(`${dockerPrefix}docker logs --tail 20 cereworker-cerebellum 2>&1`, { stdio: 'pipe', timeout: 5000 }).toString().trim();
781
+ const logs = execSyncImpl(`${dockerPrefix}docker logs --tail 20 cereworker-cerebellum 2>&1`, { stdio: 'pipe', timeout: 5000 }).toString().trim();
773
782
  if (logs)
774
783
  parts.push(`Container logs:\n${logs}`);
775
784
  }