arcane-os 0.93.0 → 0.95.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.
Files changed (42) hide show
  1. package/CHANGELOG.md +33 -0
  2. package/README.md +1 -1
  3. package/browser-runtime/ai/twin-cloud.mjs +109 -46
  4. package/browser-runtime/core/client.mjs +197 -0
  5. package/docs/reference/ai/twin-cloud.md +35 -14
  6. package/docs/reference/availability-and-normalization.md +1 -1
  7. package/docs/reference/inventory/package-api.json +96 -12
  8. package/docs/reference/local-ai.md +81 -5
  9. package/docs/reference/native-magpie.md +98 -0
  10. package/docs/reference/native-nemotron.md +204 -0
  11. package/docs/reference/native-speech.md +364 -11
  12. package/docs/reference/runtime-modules.md +21 -18
  13. package/docs/reference/sdk-api.md +152 -11
  14. package/package.json +6 -1
  15. package/runtime/arcane/modules/AI.js +114 -14
  16. package/runtime/arcane/modules/AIProviderRuntime.js +32 -15
  17. package/runtime/arcane/modules/ContinuousVoiceCapture.js +118 -21
  18. package/runtime/arcane/modules/NativeSpeechCapture.js +198 -0
  19. package/runtime/arcane/modules/NativeSpeechPlayback.js +304 -0
  20. package/runtime/arcane/modules/VoiceCaptureWorklet.js +62 -2
  21. package/schemas/arcane-app.schema.json +2 -2
  22. package/schemas/arcane-package.schema.json +2 -2
  23. package/src/core/services/speech.mjs +320 -25
  24. package/src/local-ai/config.mjs +12 -1
  25. package/src/local-ai/dev.mjs +71 -4
  26. package/src/local-ai/install.mjs +113 -5
  27. package/src/local-ai/kokoro/frontend.mjs +219 -0
  28. package/src/local-ai/kokoro/helper.mjs +168 -0
  29. package/src/local-ai/kokoro/index.mjs +645 -0
  30. package/src/local-ai/kokoro/native/CMakeLists.txt +45 -0
  31. package/src/local-ai/kokoro/native/main.cpp +249 -0
  32. package/src/local-ai/magpie/build.mjs +36 -0
  33. package/src/local-ai/magpie/helper.mjs +254 -0
  34. package/src/local-ai/magpie/index.mjs +488 -0
  35. package/src/local-ai/magpie/native/CMakeLists.txt +10 -0
  36. package/src/local-ai/magpie/native/main.cpp +270 -0
  37. package/src/local-ai/nemotron/build.mjs +53 -0
  38. package/src/local-ai/nemotron/helper.mjs +308 -0
  39. package/src/local-ai/nemotron/index.mjs +637 -0
  40. package/src/local-ai/nemotron/native/CMakeLists.txt +17 -0
  41. package/src/local-ai/nemotron/native/main.cpp +282 -0
  42. package/src/native/portable-layout.mjs +67 -9
@@ -0,0 +1,282 @@
1
+ #include <nemo_speech/asr.h>
2
+
3
+ #include <cstdint>
4
+ #include <cstring>
5
+ #include <iomanip>
6
+ #include <iostream>
7
+ #include <limits>
8
+ #include <locale>
9
+ #include <memory>
10
+ #include <sstream>
11
+ #include <stdexcept>
12
+ #include <string>
13
+ #include <vector>
14
+
15
+ #ifdef _WIN32
16
+ #include <fcntl.h>
17
+ #include <io.h>
18
+ #include <windows.h>
19
+ #endif
20
+
21
+ namespace {
22
+
23
+ using Recognizer = std::unique_ptr<nemo_speech_asr_recognizer, decltype(&nemo_speech_asr_destroy)>;
24
+ using Stream = std::unique_ptr<nemo_speech_asr_stream, decltype(&nemo_speech_asr_stream_close)>;
25
+ using Result = std::unique_ptr<nemo_speech_asr_result, decltype(&nemo_speech_asr_result_destroy)>;
26
+
27
+ void check(nemo_speech_asr_status status) {
28
+ if (status != NEMO_SPEECH_ASR_OK) {
29
+ const char* message = nemo_speech_asr_last_error();
30
+ throw std::runtime_error(message ? message : "The native ASR operation failed.");
31
+ }
32
+ }
33
+
34
+ std::string json_string(const char* value) {
35
+ std::ostringstream output;
36
+ output << '"';
37
+ if (value) {
38
+ for (const unsigned char character : std::string(value)) {
39
+ switch (character) {
40
+ case '"': output << "\\\""; break;
41
+ case '\\': output << "\\\\"; break;
42
+ case '\n': output << "\\n"; break;
43
+ case '\r': output << "\\r"; break;
44
+ case '\t': output << "\\t"; break;
45
+ default:
46
+ if (character < 0x20) {
47
+ output << "\\u" << std::hex << std::setw(4) << std::setfill('0')
48
+ << static_cast<unsigned int>(character) << std::dec;
49
+ } else {
50
+ output << character;
51
+ }
52
+ }
53
+ }
54
+ }
55
+ output << '"';
56
+ return output.str();
57
+ }
58
+
59
+ void emit_error(uint64_t request, const std::string& message) {
60
+ std::cout << "{\"request\":" << request << ",\"error\":" << json_string(message.c_str())
61
+ << "}\n" << std::flush;
62
+ }
63
+
64
+ void emit_ack(uint64_t request) {
65
+ std::cout << "{\"request\":" << request << ",\"ok\":true}\n" << std::flush;
66
+ }
67
+
68
+ void emit_running(uint64_t request) {
69
+ std::cout << "{\"request\":" << request << ",\"running\":true}\n" << std::flush;
70
+ }
71
+
72
+ std::string read_text(size_t count) {
73
+ std::string text(count, '\0');
74
+ if (count && !std::cin.read(text.data(), count)) {
75
+ throw std::runtime_error("Nemotron input ended during a language field.");
76
+ }
77
+ return text;
78
+ }
79
+
80
+ std::vector<float> read_audio(size_t samples) {
81
+ std::vector<float> audio(samples);
82
+ std::vector<unsigned char> encoded(samples * sizeof(float));
83
+ if (!encoded.empty() && !std::cin.read(reinterpret_cast<char*>(encoded.data()), encoded.size())) {
84
+ throw std::runtime_error("Nemotron input ended during a float32 audio frame.");
85
+ }
86
+ for (size_t index = 0; index < samples; ++index) {
87
+ const auto* value = encoded.data() + index * sizeof(float);
88
+ const uint32_t bits = static_cast<uint32_t>(value[0])
89
+ | (static_cast<uint32_t>(value[1]) << 8)
90
+ | (static_cast<uint32_t>(value[2]) << 16)
91
+ | (static_cast<uint32_t>(value[3]) << 24);
92
+ std::memcpy(&audio[index], &bits, sizeof(float));
93
+ }
94
+ return audio;
95
+ }
96
+
97
+ void emit_result(uint64_t request, uint64_t sequence, const nemo_speech_asr_result* result) {
98
+ std::ostringstream output;
99
+ output.imbue(std::locale::classic());
100
+ output << std::setprecision(std::numeric_limits<float>::max_digits10);
101
+ output << "{\"request\":" << request << ",\"update\":{\"sequence\":" << sequence
102
+ << ",\"final\":" << (nemo_speech_asr_result_is_final(result) ? "true" : "false")
103
+ << ",\"audioProcessedSeconds\":" << nemo_speech_asr_result_audio_processed(result)
104
+ << ",\"channelTag\":" << nemo_speech_asr_result_channel_tag(result)
105
+ << ",\"alternatives\":[";
106
+ const size_t alternatives = nemo_speech_asr_result_alternative_count(result);
107
+ for (size_t alternative = 0; alternative < alternatives; ++alternative) {
108
+ if (alternative) output << ',';
109
+ output << "{\"text\":" << json_string(nemo_speech_asr_result_transcript(result, alternative))
110
+ << ",\"confidence\":" << nemo_speech_asr_result_confidence(result, alternative)
111
+ << ",\"words\":[";
112
+ const size_t words = nemo_speech_asr_result_word_count(result, alternative);
113
+ for (size_t index = 0; index < words; ++index) {
114
+ if (index) output << ',';
115
+ output << "{\"word\":" << json_string(nemo_speech_asr_result_word_text(result, alternative, index))
116
+ << ",\"startTimeMs\":" << nemo_speech_asr_result_word_start_time(result, alternative, index)
117
+ << ",\"endTimeMs\":" << nemo_speech_asr_result_word_end_time(result, alternative, index)
118
+ << ",\"confidence\":" << nemo_speech_asr_result_word_confidence(result, alternative, index)
119
+ << ",\"speaker\":" << nemo_speech_asr_result_word_speaker_tag(result, alternative, index) << '}';
120
+ }
121
+ output << "],\"languageCodes\":[";
122
+ const size_t languages = nemo_speech_asr_result_language_count(result, alternative);
123
+ for (size_t index = 0; index < languages; ++index) {
124
+ if (index) output << ',';
125
+ output << json_string(nemo_speech_asr_result_language_code(result, alternative, index));
126
+ }
127
+ output << "]}";
128
+ }
129
+ output << "]}}\n";
130
+ // A complete result is emitted before asking for the next one. An error
131
+ // later in the drain cannot erase earlier native results.
132
+ std::cout << output.str() << std::flush;
133
+ }
134
+
135
+ void drain(uint64_t request, nemo_speech_asr_stream* stream, uint64_t& sequence) {
136
+ while (true) {
137
+ nemo_speech_asr_result* raw = nullptr;
138
+ const auto status = nemo_speech_asr_stream_next(stream, &raw);
139
+ const Result result(raw, nemo_speech_asr_result_destroy);
140
+ check(status);
141
+ if (!result) return;
142
+ emit_result(request, ++sequence, result.get());
143
+ }
144
+ }
145
+
146
+ int serve(const std::string& model_path, const std::string& diarization_path, int gpu) {
147
+ #ifdef _WIN32
148
+ _setmode(_fileno(stdin), _O_BINARY);
149
+ _setmode(_fileno(stdout), _O_BINARY);
150
+ #endif
151
+ nemo_speech_asr_backend_config backend{};
152
+ backend.size = sizeof(backend);
153
+ backend.gpu = gpu;
154
+ nemo_speech_asr_model_config model{};
155
+ model.size = sizeof(model);
156
+ model.path = model_path.c_str();
157
+ nemo_speech_asr_diar_config diarization{};
158
+ diarization.size = sizeof(diarization);
159
+ diarization.model_path = diarization_path.empty() ? nullptr : diarization_path.c_str();
160
+ diarization.left_context_frames = -1;
161
+ nemo_speech_asr_endpointing_config endpointing{};
162
+ endpointing.size = sizeof(endpointing);
163
+ endpointing.enable = false;
164
+ nemo_speech_asr_recognizer_config configuration{};
165
+ configuration.size = sizeof(configuration);
166
+ configuration.backend = &backend;
167
+ configuration.model = &model;
168
+ configuration.diar = &diarization;
169
+ configuration.endpointing = &endpointing;
170
+ nemo_speech_asr_recognizer* raw_recognizer = nullptr;
171
+ const auto status = nemo_speech_asr_create(&configuration, &raw_recognizer);
172
+ const Recognizer recognizer(raw_recognizer, nemo_speech_asr_destroy);
173
+ check(status);
174
+ // The stream is destroyed before its recognizer. All stream calls stay on
175
+ // this thread; the host owns capture, timing and conversational decisions.
176
+ Stream stream(nullptr, nemo_speech_asr_stream_close);
177
+ int sample_rate = 16000;
178
+ uint64_t sequence = 0;
179
+ std::string language;
180
+ std::cout << "{\"ready\":true,\"version\":" << json_string(nemo_speech_asr_version())
181
+ << ",\"requestedGpu\":" << gpu << "}\n" << std::flush;
182
+ std::string line;
183
+ while (std::getline(std::cin, line)) {
184
+ uint64_t request = 0;
185
+ std::string operation;
186
+ std::istringstream command(line);
187
+ if (!(command >> operation >> request)) {
188
+ emit_error(0, "Unreadable Nemotron command.");
189
+ return 1;
190
+ }
191
+ try {
192
+ if (operation == "open") {
193
+ size_t language_length = 0;
194
+ int requested_rate = 0;
195
+ if (!(command >> requested_rate >> language_length)) {
196
+ throw std::runtime_error("Unreadable Nemotron stream configuration.");
197
+ }
198
+ const std::string requested_language = read_text(language_length);
199
+ if (stream) throw std::runtime_error("A Nemotron stream is already open.");
200
+ language = requested_language;
201
+ sample_rate = requested_rate;
202
+ auto options = nemo_speech_asr_recognition_options_default();
203
+ options.language_code = language.empty() ? nullptr : language.c_str();
204
+ options.interim_results = true;
205
+ options.enable_word_time_offsets = true;
206
+ options.enable_speaker_diarization = !diarization_path.empty();
207
+ options.enable_automatic_punctuation = false;
208
+ options.profanity_filter = false;
209
+ options.verbatim_transcripts = true;
210
+ nemo_speech_asr_stream* raw_stream = nullptr;
211
+ emit_running(request);
212
+ const auto opened = nemo_speech_asr_streaming_recognize(recognizer.get(), &options, &raw_stream);
213
+ stream.reset(raw_stream);
214
+ check(opened);
215
+ sequence = 0;
216
+ } else if (operation == "close") {
217
+ stream.reset();
218
+ } else {
219
+ std::vector<float> audio;
220
+ if (operation == "push") {
221
+ size_t samples = 0;
222
+ if (!(command >> samples)) throw std::runtime_error("Unreadable Nemotron audio frame.");
223
+ audio = read_audio(samples);
224
+ }
225
+ if (!stream) throw std::runtime_error("No Nemotron stream is open.");
226
+ emit_running(request);
227
+ if (operation == "push") {
228
+ check(nemo_speech_asr_stream_push_f32(stream.get(), audio.data(), audio.size(), sample_rate));
229
+ } else if (operation == "endpoint") {
230
+ check(nemo_speech_asr_stream_force_endpoint(stream.get()));
231
+ } else if (operation == "finish") {
232
+ check(nemo_speech_asr_stream_finish(stream.get()));
233
+ } else {
234
+ throw std::runtime_error("Unknown Nemotron operation.");
235
+ }
236
+ drain(request, stream.get(), sequence);
237
+ if (operation == "finish") stream.reset();
238
+ }
239
+ emit_ack(request);
240
+ } catch (const std::exception& error) {
241
+ emit_error(request, error.what());
242
+ if (!std::cin) return 1;
243
+ }
244
+ }
245
+ return 0;
246
+ }
247
+
248
+ int run(const std::vector<std::string>& arguments) {
249
+ try {
250
+ if (arguments.size() != 4) {
251
+ throw std::runtime_error("Usage: arcane-nemotron ASR.gguf DIARIZATION.gguf GPU_ORDINAL (-1 selects CPU).");
252
+ }
253
+ return serve(arguments[1], arguments[2], std::stoi(arguments[3]));
254
+ } catch (const std::exception& error) {
255
+ emit_error(0, error.what());
256
+ return 1;
257
+ }
258
+ }
259
+
260
+ } // namespace
261
+
262
+ #ifdef _WIN32
263
+ int wmain(int argc, wchar_t** argv) {
264
+ std::vector<std::string> arguments;
265
+ for (int index = 0; index < argc; ++index) {
266
+ const int length = WideCharToMultiByte(CP_UTF8, 0, argv[index], -1, nullptr, 0, nullptr, nullptr);
267
+ if (length == 0) {
268
+ emit_error(0, "Windows could not encode a Nemotron argument as UTF-8.");
269
+ return 1;
270
+ }
271
+ std::string argument(length, '\0');
272
+ WideCharToMultiByte(CP_UTF8, 0, argv[index], -1, argument.data(), length, nullptr, nullptr);
273
+ argument.pop_back();
274
+ arguments.push_back(argument);
275
+ }
276
+ return run(arguments);
277
+ }
278
+ #else
279
+ int main(int argc, char** argv) {
280
+ return run(std::vector<std::string>(argv, argv + argc));
281
+ }
282
+ #endif
@@ -156,11 +156,18 @@ export function coreEntrySource(application, version, services, {localAI, runtim
156
156
  return (typeof requirement === 'string' ? requirement : requirement.id) === 'stable-diffusion.cpp';
157
157
  }) ?? false;
158
158
  const modelAssetsSelected = imageSelected || (localAI?.runtimes?.some(function selectedProjectedRuntime(requirement) {
159
- return ['onnx', 'llama.cpp'].includes(typeof requirement === 'string' ? requirement : requirement.id);
159
+ return ['onnx', 'llama.cpp', 'kokoro-native'].includes(typeof requirement === 'string' ? requirement : requirement.id);
160
160
  }) ?? false);
161
161
  const whisperSelected = localAI?.runtimes?.some(function selectedWhisper(requirement) {
162
162
  return (typeof requirement === 'string' ? requirement : requirement.id) === 'whisper.cpp';
163
163
  }) ?? false;
164
+ const kokoroRequirement = localAI?.runtimes?.find(function selectedKokoro(requirement) {
165
+ return (typeof requirement === 'string' ? requirement : requirement.id) === 'kokoro-native';
166
+ });
167
+ const kokoroSelected = kokoroRequirement !== undefined;
168
+ const onnxSelected = localAI?.runtimes?.some(function selectedONNX(requirement) {
169
+ return (typeof requirement === 'string' ? requirement : requirement.id) === 'onnx';
170
+ }) ?? false;
164
171
  const imports = services.map(function serviceImport(service, index) {
165
172
  const specifier = '../app/' + service.module.split('/').map(encodeURIComponent).join('/');
166
173
  return {binding: `{default: createService${index}}`, specifier};
@@ -174,7 +181,7 @@ export function coreEntrySource(application, version, services, {localAI, runtim
174
181
  ...localAI,
175
182
  runtimes: localAI.runtimes.filter(function selectedChat(requirement) {
176
183
  const id = typeof requirement === 'string' ? requirement : requirement.id;
177
- return id !== 'stable-diffusion.cpp' && id !== 'whisper.cpp';
184
+ return id !== 'stable-diffusion.cpp' && id !== 'whisper.cpp' && id !== 'kokoro-native';
178
185
  })
179
186
  };
180
187
  imports.push({binding: '{createLocalAIService}', specifier: 'arcane-os/core/local-ai'});
@@ -189,10 +196,15 @@ export function coreEntrySource(application, version, services, {localAI, runtim
189
196
  imports.push({binding: '{createLocalImageService}', specifier: 'arcane-os/core/image'});
190
197
  definitions.push(` register(createLocalImageService(JSON.parse(${JSON.stringify(JSON.stringify(localAI))}), {...context, modelAssets}));`);
191
198
  }
192
- if (whisperSelected) {
199
+ if (whisperSelected || kokoroSelected) {
193
200
  imports.push({binding: '{createSpeechService}', specifier: 'arcane-os/core/speech'});
201
+ }
202
+ if (whisperSelected) {
194
203
  imports.push({binding: '{createWhisperRuntime}', specifier: 'arcane-os/local-ai/whisper'});
195
204
  }
205
+ if (kokoroSelected) {
206
+ imports.push({binding: '{createNativeKokoroRuntime}', specifier: 'arcane-os/local-ai/kokoro'});
207
+ }
196
208
  if (packagedWeb) {
197
209
  imports.push({binding: '{createPackagedWebService}', specifier: 'arcane-os/core/packaged-web'});
198
210
  definitions.push(" packagedWeb = createPackagedWebService({artifactRoot: fileURLToPath(new URL('../', import.meta.url))}, context);",
@@ -228,7 +240,7 @@ export function coreEntrySource(application, version, services, {localAI, runtim
228
240
  ...(localAI === undefined ? [] : [
229
241
  ` runtimes: JSON.parse(${JSON.stringify(JSON.stringify(runtimes))}).map(function runtimeLocation(runtime) {`,
230
242
  ' const resolved = {...runtime};',
231
- " for (const field of ['root', 'executable', 'modulePath', 'includeDirectory', 'libraryDirectory', 'binaryDirectory', 'cmakeDirectory', 'libraryPath', 'bindingModulePath', 'helperExecutable', 'helperRoot', 'decoderExecutable', 'decoderRoot']) {",
243
+ " for (const field of ['root', 'executable', 'modulePath', 'includeDirectory', 'libraryDirectory', 'binaryDirectory', 'cmakeDirectory', 'libraryPath', 'bindingModulePath', 'helperExecutable', 'helperRoot', 'espeakDataDirectory', 'decoderExecutable', 'decoderRoot']) {",
232
244
  ' if (runtime[field] !== undefined) resolved[field] = runtimePath(runtime[field]);',
233
245
  ' }',
234
246
  ' if (runtime.variants) resolved.variants = runtime.variants.map(function nativeVariant(variant) {',
@@ -268,7 +280,7 @@ export function coreEntrySource(application, version, services, {localAI, runtim
268
280
  ''
269
281
  ]),
270
282
  ...(packagedWeb ? ['let packagedWeb;', ''] : []),
271
- 'async function createServices(register) {',
283
+ kokoroSelected ? 'async function createServices(register, getService) {' : 'async function createServices(register) {',
272
284
  ` const [${imports.map(function binding(selection) { return selection.binding; }).join(', ')}] = await Promise.all([`,
273
285
  ...imports.map(function importService(selection) { return ` import(${JSON.stringify(selection.specifier)})`; }).map(
274
286
  function separateImport(line, index) { return line + (index < imports.length - 1 ? ',' : ''); }
@@ -289,8 +301,50 @@ export function coreEntrySource(application, version, services, {localAI, runtim
289
301
  " temporaryDirectory: path.join(context.stateRoot ?? path.join(context.appRoot, '.arcane'), 'speech', 'whisper'),",
290
302
  ' onEvent: context.onEvent',
291
303
  ' }',
292
- ' );',
293
- ' register(createSpeechService({stt, signal: context.signal}));'
304
+ ' );'
305
+ ] : []),
306
+ ...(kokoroSelected ? [
307
+ ` const kokoroRequirement = JSON.parse(${JSON.stringify(JSON.stringify(typeof kokoroRequirement === 'string' ? {} : kokoroRequirement))});`,
308
+ ' const tts = createNativeKokoroRuntime({',
309
+ ' modelId: kokoroRequirement.modelId,',
310
+ ' model: kokoroRequirement.model,',
311
+ ' revision: kokoroRequirement.revision,',
312
+ ' dtype: kokoroRequirement.dtype,',
313
+ ' paths: kokoroRequirement.paths,',
314
+ ' assetProjectionId: kokoroRequirement.assetProjectionId,',
315
+ ' resourcePaths: kokoroRequirement.resourcePaths,',
316
+ ' sessionOptions: kokoroRequirement.sessionOptions,',
317
+ ' executionPreference: kokoroRequirement.executionPreference,',
318
+ ' executionTarget: kokoroRequirement.executionTarget,',
319
+ ' modelAssets, signal: context.signal, onEvent: context.onEvent,',
320
+ ' async prepare({signal: activationSignal}) {',
321
+ ' activationSignal.throwIfAborted();',
322
+ ...(onnxSelected ? [] : [
323
+ " throw Object.assign(new Error('Native Kokoro requires an explicitly selected onnx runtime.'), {code: 'ARCANE_TARGET_UNAVAILABLE'});"
324
+ ]),
325
+ ' let cancel;',
326
+ ' const cancelled = new Promise(function observePreparationCancellation(_resolve, reject) {',
327
+ ' cancel = function stopWaitingForONNX() { reject(activationSignal.reason); };',
328
+ " activationSignal.addEventListener('abort', cancel, {once: true});",
329
+ ' });',
330
+ ' try {',
331
+ ' // This activation waits on the existing owner without cancelling its startup.',
332
+ " const owner = await Promise.race([getService('local-ai'), cancelled]);",
333
+ ' activationSignal.throwIfAborted();',
334
+ ' return {',
335
+ ' onnx: owner.getONNXRuntime(),',
336
+ ' runtime: context.runtimes.find(function selectedKokoroRuntime(runtime) {',
337
+ " return runtime.id === 'kokoro-native';",
338
+ ' })',
339
+ ' };',
340
+ ' } finally {',
341
+ " activationSignal.removeEventListener('abort', cancel);",
342
+ ' }',
343
+ ' }',
344
+ ' });'
345
+ ] : []),
346
+ ...(whisperSelected || kokoroSelected ? [
347
+ ` register(createSpeechService({${whisperSelected ? 'stt, ' : ''}${kokoroSelected ? 'tts, ' : ''}signal: context.signal}));`
294
348
  ] : []),
295
349
  ...definitions,
296
350
  '}',
@@ -300,7 +354,9 @@ export function coreEntrySource(application, version, services, {localAI, runtim
300
354
  'let listener;',
301
355
  'if (context.sharedHost === undefined) {',
302
356
  ' const services = [];',
303
- ' await createServices(function register(service) { services.push(service); });',
357
+ kokoroSelected
358
+ ? ' await createServices(function register(service) { services.push(service); }, function getService(name) { return host.runtime.getService(name); });'
359
+ : ' await createServices(function register(service) { services.push(service); });',
304
360
  ' host = startCoreHost({application, version, services});',
305
361
  ' if (context.coreListener !== undefined) {',
306
362
  " process.on('SIGINT', closeCore);",
@@ -338,7 +394,9 @@ export function coreEntrySource(application, version, services, {localAI, runtim
338
394
  "} else if (process.argv.includes('--arcane-core-headless')) {",
339
395
  ' host = await runSharedCoreHost({',
340
396
  ' endpoint: context.sharedHost.endpoint, application, version,',
341
- ' configure(runtime) { return createServices(function register(service) { runtime.registerService(service); }); },',
397
+ kokoroSelected
398
+ ? ' configure(runtime) { return createServices(function register(service) { runtime.registerService(service); }, function getService(name) { return runtime.getService(name); }); },'
399
+ : ' configure(runtime) { return createServices(function register(service) { runtime.registerService(service); }); },',
342
400
  ...(packagedWeb ? [
343
401
  ' getReplayEvents() {',
344
402
  ' const current = packagedWeb?.current();',