@cereworker/cli 26.329.12 → 26.329.14
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/dist/cli.smoke.test.d.ts +2 -0
- package/dist/cli.smoke.test.d.ts.map +1 -0
- package/dist/cli.smoke.test.js +343 -0
- package/dist/cli.smoke.test.js.map +1 -0
- package/dist/commands.d.ts +1 -0
- package/dist/commands.d.ts.map +1 -1
- package/dist/commands.js +5 -1
- package/dist/commands.js.map +1 -1
- package/dist/service.d.ts +13 -2
- package/dist/service.d.ts.map +1 -1
- package/dist/service.integration.test.d.ts +2 -0
- package/dist/service.integration.test.d.ts.map +1 -0
- package/dist/service.integration.test.js +181 -0
- package/dist/service.integration.test.js.map +1 -0
- package/dist/service.js +40 -31
- package/dist/service.js.map +1 -1
- package/dist/update-check.d.ts +8 -1
- package/dist/update-check.d.ts.map +1 -1
- package/dist/update-check.js +16 -12
- package/dist/update-check.js.map +1 -1
- package/package.json +10 -10
|
@@ -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
|
@@ -15,7 +15,16 @@ import { GatewayServer, GatewayNodeClient, createProxyTools } from '@cereworker/
|
|
|
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 =
|
|
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(
|
|
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(
|
|
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(
|
|
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 =
|
|
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(
|
|
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 =
|
|
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
|
-
|
|
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
|
-
|
|
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
|
-
|
|
498
|
+
execSyncImpl('sudo -n systemctl start docker', { stdio: 'pipe', timeout: 15_000 });
|
|
490
499
|
log.info('Started Docker service');
|
|
491
|
-
|
|
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
|
-
|
|
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
|
-
|
|
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 =
|
|
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 =
|
|
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
|
-
|
|
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
|
-
|
|
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 =
|
|
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 =
|
|
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 =
|
|
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
|
-
|
|
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
|
-
|
|
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
|
-
|
|
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(/^~/,
|
|
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
|
-
|
|
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
|
-
|
|
703
|
+
execSyncImpl('which docker', { stdio: 'pipe' });
|
|
695
704
|
// Docker binary found but daemon not running or permission denied
|
|
696
705
|
try {
|
|
697
|
-
|
|
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 =
|
|
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 =
|
|
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 =
|
|
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
|
}
|