blun-king-cli 9.1.208 → 9.1.210

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.
@@ -33,12 +33,19 @@ const {
33
33
  RUNNING_UPDATE_HANDOFF_MESSAGE,
34
34
  RUNNING_UPDATE_MODE_MESSAGE,
35
35
  RUNNING_UPDATE_PREPARED_MESSAGE,
36
+ RUNTIME_EXIT_INTENT_MESSAGE,
36
37
  activateRuntime,
38
+ clearActiveRuntime,
37
39
  handoffArgsForMode,
38
40
  handoffRuntime,
39
41
  prepareRunningUpdate,
40
42
  pruneRunningUpdateReleases,
43
+ recordRunningUpdateEvent,
41
44
  readActiveRuntime,
45
+ readPendingRuntime,
46
+ restoreActiveRuntime,
47
+ clearPendingRuntime,
48
+ stageRuntime,
42
49
  } = require('./running-update.cjs');
43
50
  const {
44
51
  RUNNING_UPDATE_MODES,
@@ -62,9 +69,9 @@ const PROFILE = parseProfileLaunchArgs(RAW_ARGS, launcherModeFromArgv(process.ar
62
69
  const ARGS = PROFILE.args;
63
70
  const CORE_LOAD_TIMEOUT_MS = 30_000;
64
71
  const RUNTIME_READY_TIMEOUT_MS = 60_000;
65
- const RUNNING_UPDATE_RECHECK_MS = 60_000;
66
- const RUNNING_UPDATE_RECHECK_JITTER_MS = 30_000;
67
- const RUNNING_UPDATE_HANDOFF_NOTICE = 'Ein Update wird geladen. Die TUI wird anschließend neu gestartet.';
72
+ const RUNNING_UPDATE_RECHECK_MS = 5 * 60_000;
73
+ const RUNNING_UPDATE_RECHECK_JITTER_MS = 2 * 60_000;
74
+ const RUNNING_UPDATE_STABILIZATION_MS = 2 * 60 * 1000;
68
75
 
69
76
  function runningUpdateRecheckDelay(randomValue = Math.random()) {
70
77
  const normalized = Number.isFinite(randomValue)
@@ -83,10 +90,6 @@ function scheduleRunningUpdateRecheck(callback, options = {}) {
83
90
  return timer;
84
91
  }
85
92
 
86
- function writeRunningUpdateHandoffNotice(output = process.stdout) {
87
- output.write(`\n${RUNNING_UPDATE_HANDOFF_NOTICE}\n`);
88
- }
89
-
90
93
  function normalizeWindowsPath(value) {
91
94
  return path.win32.normalize(value).replace(/\\+$/u, '').toLowerCase();
92
95
  }
@@ -268,6 +271,14 @@ function resolveHandoffCwd(candidate, fallback) {
268
271
  }
269
272
  }
270
273
 
274
+ function shouldRecoverActivatedRuntime(result, recovery, options = {}) {
275
+ if (recovery === undefined || options.runtimeExitIntent === true) return false;
276
+ const signal = result?.signal;
277
+ if (signal === 'SIGINT' || signal === 'SIGTERM' || signal === 'SIGHUP') return false;
278
+ const now = options.now ?? Date.now();
279
+ return now - recovery.activatedAt <= RUNNING_UPDATE_STABILIZATION_MS;
280
+ }
281
+
271
282
  async function superviseProtectedCore(args, env, cwd, releaseNotice, options = {}) {
272
283
  const packageRoot = path.resolve(options.packageRoot || PKG);
273
284
  let preparedTarget;
@@ -276,6 +287,7 @@ async function superviseProtectedCore(args, env, cwd, releaseNotice, options = {
276
287
  let handoffCwd = cwd;
277
288
  let updateStarted = false;
278
289
  let runtimeReady = false;
290
+ let runtimeExitIntent = false;
279
291
  let runningUpdatePollTimer;
280
292
  let runningUpdateMode = readRunningUpdateMode(env.BLUN_HOME);
281
293
  const automaticMode = () => runningUpdateMode === RUNNING_UPDATE_MODES.RESUME
@@ -306,11 +318,17 @@ async function superviseProtectedCore(args, env, cwd, releaseNotice, options = {
306
318
  };
307
319
  const announcePrepared = (child) => {
308
320
  if (preparedTarget === undefined || !automaticMode() || child.connected !== true) return;
309
- child.send({
310
- type: RUNNING_UPDATE_PREPARED_MESSAGE,
311
- version: preparedTarget.version,
312
- mode: runningUpdateMode,
313
- });
321
+ try {
322
+ child.send({
323
+ type: RUNNING_UPDATE_PREPARED_MESSAGE,
324
+ version: preparedTarget.version,
325
+ mode: runningUpdateMode,
326
+ }, (error) => {
327
+ if (error) options.onRunningUpdateError?.(error);
328
+ });
329
+ } catch (error) {
330
+ options.onRunningUpdateError?.(error);
331
+ }
314
332
  };
315
333
  const startPreparation = (child) => {
316
334
  if (updateStarted || options.runningUpdate === false || !automaticMode()) return;
@@ -332,6 +350,15 @@ async function superviseProtectedCore(args, env, cwd, releaseNotice, options = {
332
350
  return;
333
351
  }
334
352
  preparedTarget = target;
353
+ (options.stageRuntime || stageRuntime)(sharedHome, target, { mode: runningUpdateMode });
354
+ (options.recordRunningUpdateEvent || recordRunningUpdateEvent)(sharedHome, {
355
+ event: 'prepared-for-next-start',
356
+ fromVersion: readPackageVersionAt(packageRoot),
357
+ toVersion: target.version,
358
+ }, { now: options.nowImpl });
359
+ Promise.resolve((options.pruneRunningUpdateReleases || pruneRunningUpdateReleases)(sharedHome, {
360
+ activePackageRoot: packageRoot,
361
+ })).catch((error) => options.onRunningUpdateError?.(error));
335
362
  clearRunningUpdatePoll();
336
363
  announcePrepared(child);
337
364
  }).catch((error) => {
@@ -341,6 +368,7 @@ async function superviseProtectedCore(args, env, cwd, releaseNotice, options = {
341
368
  });
342
369
  };
343
370
  const handleCoreMessage = (message, child) => {
371
+ if (message?.type === RUNTIME_EXIT_INTENT_MESSAGE) runtimeExitIntent = true;
344
372
  if (message?.type === RUNTIME_READY_MESSAGE) {
345
373
  runtimeReady = true;
346
374
  refreshRunningUpdateMode();
@@ -369,6 +397,13 @@ async function superviseProtectedCore(args, env, cwd, releaseNotice, options = {
369
397
  handoffSessionId = message.sessionId;
370
398
  handoffMode = runningUpdateMode;
371
399
  handoffCwd = resolveHandoffCwd(message.cwd, cwd);
400
+ if (preparedTarget !== undefined) {
401
+ (options.stageRuntime || stageRuntime)(env.BLUN_SHARED_HOME, preparedTarget, {
402
+ mode: handoffMode,
403
+ sessionId: handoffSessionId,
404
+ cwd: handoffCwd,
405
+ });
406
+ }
372
407
  }
373
408
  };
374
409
  const spawnCore = options.spawnProtectedCore || spawnProtectedCore;
@@ -382,6 +417,21 @@ async function superviseProtectedCore(args, env, cwd, releaseNotice, options = {
382
417
  core.child.on('message', existingMessageHandler);
383
418
  }
384
419
  const loaded = await core.loaded;
420
+ if (loaded && options.startupPendingRuntime !== undefined) {
421
+ await (options.activateRuntime || activateRuntime)(env.BLUN_SHARED_HOME, options.startupPendingRuntime);
422
+ (options.clearPendingRuntime || clearPendingRuntime)(env.BLUN_SHARED_HOME, options.startupPendingRuntime);
423
+ (options.recordRunningUpdateEvent || recordRunningUpdateEvent)(env.BLUN_SHARED_HOME, {
424
+ event: 'activated-on-start',
425
+ version: options.startupPendingRuntime.version,
426
+ }, { now: options.nowImpl });
427
+ } else if (loaded && options.staleActiveRuntime !== undefined) {
428
+ (options.clearActiveRuntime || clearActiveRuntime)(env.BLUN_SHARED_HOME, options.staleActiveRuntime);
429
+ (options.recordRunningUpdateEvent || recordRunningUpdateEvent)(env.BLUN_SHARED_HOME, {
430
+ event: 'stale-active-cleared',
431
+ staleVersion: options.staleActiveRuntime.version,
432
+ runningVersion: readPackageVersionAt(packageRoot),
433
+ }, { now: options.nowImpl });
434
+ }
385
435
  if (loaded && !runtimeReady) {
386
436
  // Core load is an independent readiness signal. Start the download even
387
437
  // when the later TUI-ready IPC message is lost; handoff still waits for
@@ -395,12 +445,12 @@ async function superviseProtectedCore(args, env, cwd, releaseNotice, options = {
395
445
  const result = await core.completed;
396
446
  clearRunningUpdatePoll();
397
447
  if (existingMessageHandler) core.child.off('message', existingMessageHandler);
398
- if (result.error) throw result.error;
399
448
  if (result.code === RUNNING_UPDATE_HANDOFF_EXIT_CODE
400
449
  && preparedTarget !== undefined
401
450
  && handoffSessionId !== undefined
402
451
  && handoffMode !== undefined) {
403
- writeRunningUpdateHandoffNotice(options.runningUpdateNoticeOutput || process.stdout);
452
+ const previousActiveRuntime = (options.readActiveRuntime || readActiveRuntime)(env.BLUN_SHARED_HOME);
453
+ const activatedAt = (options.nowImpl || Date.now)();
404
454
  const handoff = await handoffRuntime({
405
455
  target: preparedTarget,
406
456
  previous: { packageRoot, version: readPackageVersionAt(packageRoot) },
@@ -421,6 +471,11 @@ async function superviseProtectedCore(args, env, cwd, releaseNotice, options = {
421
471
  },
422
472
  activateTarget: async (target) => {
423
473
  await (options.activateRuntime || activateRuntime)(env.BLUN_SHARED_HOME, target);
474
+ (options.recordRunningUpdateEvent || recordRunningUpdateEvent)(env.BLUN_SHARED_HOME, {
475
+ event: 'activated',
476
+ fromVersion: readPackageVersionAt(packageRoot),
477
+ toVersion: target.version,
478
+ }, { now: options.nowImpl });
424
479
  try {
425
480
  await (options.pruneRunningUpdateReleases || pruneRunningUpdateReleases)(env.BLUN_SHARED_HOME, {
426
481
  activePackageRoot: target.packageRoot,
@@ -437,8 +492,49 @@ async function superviseProtectedCore(args, env, cwd, releaseNotice, options = {
437
492
  ...options,
438
493
  existingCore: resumed,
439
494
  packageRoot: handoff.runtime.packageRoot,
495
+ startupPendingRuntime: undefined,
496
+ staleActiveRuntime: undefined,
497
+ recoveryRuntime: handoff.kind === 'activated' ? {
498
+ activatedAt,
499
+ activeRuntime: previousActiveRuntime,
500
+ packageRoot,
501
+ version: readPackageVersionAt(packageRoot),
502
+ } : undefined,
440
503
  });
441
504
  }
505
+ const recovery = options.recoveryRuntime;
506
+ if (shouldRecoverActivatedRuntime(result, recovery, {
507
+ now: (options.nowImpl || Date.now)(),
508
+ runtimeExitIntent,
509
+ })) {
510
+ (options.recordRunningUpdateEvent || recordRunningUpdateEvent)(env.BLUN_SHARED_HOME, {
511
+ event: 'post-activation-exit',
512
+ activeVersion: readPackageVersionAt(packageRoot),
513
+ code: Number.isInteger(result.code) ? result.code : null,
514
+ error: result.error?.message || null,
515
+ signal: result.signal || null,
516
+ recoveryVersion: recovery.version,
517
+ }, { now: options.nowImpl });
518
+ await (options.restoreActiveRuntime || restoreActiveRuntime)(env.BLUN_SHARED_HOME, recovery.activeRuntime);
519
+ const fallback = spawnCore(args, env, cwd, { packageRoot: recovery.packageRoot });
520
+ const fallbackLoaded = await fallback.loaded;
521
+ const fallbackReady = fallbackLoaded && await waitForRuntimeReady(fallback, options.readyTimeoutMs);
522
+ (options.recordRunningUpdateEvent || recordRunningUpdateEvent)(env.BLUN_SHARED_HOME, {
523
+ event: fallbackReady ? 'recovery-ready' : 'recovery-failed',
524
+ version: recovery.version,
525
+ }, { now: options.nowImpl });
526
+ if (fallbackReady) {
527
+ return superviseProtectedCore(args, env, cwd, async () => {}, {
528
+ ...options,
529
+ existingCore: fallback,
530
+ packageRoot: recovery.packageRoot,
531
+ startupPendingRuntime: undefined,
532
+ staleActiveRuntime: undefined,
533
+ recoveryRuntime: undefined,
534
+ });
535
+ }
536
+ }
537
+ if (result.error) throw result.error;
442
538
  return exitCodeForChild(result, loaded);
443
539
  }
444
540
 
@@ -512,12 +608,12 @@ function spawnManagedLauncher(binary, cwd) {
512
608
  });
513
609
  }
514
610
 
515
- function spawnActiveLauncher(packageRoot, cwd) {
611
+ function spawnActiveLauncher(packageRoot, cwd, args = RAW_ARGS) {
516
612
  const entryName = path.basename(process.argv[1] || 'king.js').toLowerCase() === 'blun.js'
517
613
  ? 'blun.js'
518
614
  : 'king.js';
519
615
  return new Promise((resolve, reject) => {
520
- const child = spawn(process.execPath, [path.join(packageRoot, 'bin', entryName), ...RAW_ARGS], {
616
+ const child = spawn(process.execPath, [path.join(packageRoot, 'bin', entryName), ...args], {
521
617
  cwd,
522
618
  env: { ...process.env, BLUN_ACTIVE_RUNTIME_ROOT: packageRoot },
523
619
  stdio: 'inherit',
@@ -535,16 +631,40 @@ function spawnActiveLauncher(packageRoot, cwd) {
535
631
 
536
632
  async function runLauncher(options = {}) {
537
633
  const callerCwd = process.cwd();
634
+ let startupPendingRuntime;
635
+ let staleActiveRuntime;
538
636
  if (options.skipActiveRuntime !== true) {
539
637
  const activeHome = resolveLauncherPrivatePaths().sharedHome;
638
+ const pendingRuntime = readPendingRuntime(activeHome);
540
639
  const activeRuntime = readActiveRuntime(activeHome);
541
640
  const currentVersion = readPackageVersion();
542
- if (activeRuntime !== null
641
+ if (pendingRuntime !== null
642
+ && path.resolve(pendingRuntime.packageRoot) !== PKG
643
+ && compareSemver(currentVersion, pendingRuntime.version) === -1) {
644
+ const pendingArgs = pendingRuntime.mode !== undefined && pendingRuntime.sessionId !== undefined
645
+ ? handoffArgsForMode(RAW_ARGS, pendingRuntime.mode, pendingRuntime.sessionId)
646
+ : RAW_ARGS;
647
+ process.exitCode = await spawnActiveLauncher(
648
+ pendingRuntime.packageRoot,
649
+ pendingRuntime.cwd || callerCwd,
650
+ pendingArgs,
651
+ );
652
+ return;
653
+ }
654
+ if (pendingRuntime !== null && path.resolve(pendingRuntime.packageRoot) === PKG) {
655
+ startupPendingRuntime = pendingRuntime;
656
+ }
657
+ if (startupPendingRuntime === undefined && activeRuntime !== null
543
658
  && path.resolve(activeRuntime.packageRoot) !== PKG
544
659
  && compareSemver(currentVersion, activeRuntime.version) === -1) {
545
660
  process.exitCode = await spawnActiveLauncher(activeRuntime.packageRoot, callerCwd);
546
661
  return;
547
662
  }
663
+ if (activeRuntime !== null
664
+ && path.resolve(activeRuntime.packageRoot) !== PKG
665
+ && compareSemver(currentVersion, activeRuntime.version) >= 0) {
666
+ staleActiveRuntime = activeRuntime;
667
+ }
548
668
  }
549
669
  const mode = options.mode || launcherModeFromArgv(process.argv);
550
670
  const explicitUpdateRequest = isNpmPackageUpdateRequest(ARGS);
@@ -759,6 +879,7 @@ async function runLauncher(options = {}) {
759
879
  env,
760
880
  callerCwd,
761
881
  releaseNotice,
882
+ { startupPendingRuntime, staleActiveRuntime },
762
883
  );
763
884
  } finally {
764
885
  await releaseNotice();
@@ -768,14 +889,15 @@ async function runLauncher(options = {}) {
768
889
  module.exports = {
769
890
  RUNNING_UPDATE_RECHECK_JITTER_MS,
770
891
  RUNNING_UPDATE_RECHECK_MS,
892
+ RUNNING_UPDATE_STABILIZATION_MS,
771
893
  createManagedNodeEnvironment,
772
894
  resolveLauncherPrivatePaths,
773
895
  resolveHandoffCwd,
774
896
  runningUpdateRecheckDelay,
775
897
  runLauncher,
776
898
  scheduleRunningUpdateRecheck,
899
+ shouldRecoverActivatedRuntime,
777
900
  shouldDetachProtectedCore,
778
901
  spawnManagedLauncher,
779
902
  superviseProtectedCore,
780
- writeRunningUpdateHandoffNotice,
781
903
  };
@@ -14,11 +14,13 @@ const PACKAGE_NAME = 'blun-king-cli';
14
14
  const MANIFEST_URL = 'https://chat.blun.ai/blun-code-version.json';
15
15
  const REGISTRY_URL = 'https://registry.npmjs.org/blun-king-cli';
16
16
  const ACTIVE_RUNTIME_FILE = 'active-runtime.json';
17
+ const PENDING_RUNTIME_FILE = 'pending-runtime.json';
17
18
  const RUNNING_UPDATE_HANDOFF_EXIT_CODE = 76;
18
19
  const RUNNING_UPDATE_PREPARED_MESSAGE = 'blun-running-update-prepared';
19
20
  const RUNNING_UPDATE_HANDOFF_MESSAGE = 'blun-running-update-handoff';
20
21
  const RUNNING_UPDATE_MODE_MESSAGE = 'blun-running-update-mode';
21
22
  const RUNTIME_READY_MESSAGE = 'blun-runtime-session-ready';
23
+ const RUNTIME_EXIT_INTENT_MESSAGE = 'blun-runtime-exit-intent';
22
24
  const MAX_TARBALL_BYTES = 128 * 1024 * 1024;
23
25
  const AUTO_UPDATE_SETTLE_MS = 30 * 60 * 1000;
24
26
  const DEFAULT_RETAINED_RELEASE_VERSIONS = 3;
@@ -203,6 +205,10 @@ function activeRuntimePath(sharedHome) {
203
205
  return path.join(path.resolve(sharedHome), 'updates', ACTIVE_RUNTIME_FILE);
204
206
  }
205
207
 
208
+ function pendingRuntimePath(sharedHome) {
209
+ return path.join(path.resolve(sharedHome), 'updates', PENDING_RUNTIME_FILE);
210
+ }
211
+
206
212
  function pathWithin(root, candidate) {
207
213
  const relative = path.relative(root, candidate);
208
214
  return relative === '' || (relative !== '..'
@@ -229,9 +235,8 @@ function verifyRuntimePackage(packageRoot, version, options = {}) {
229
235
  }
230
236
  }
231
237
 
232
- function readActiveRuntime(sharedHome) {
238
+ function readRuntimeRecord(sharedHome, filePath) {
233
239
  try {
234
- const filePath = activeRuntimePath(sharedHome);
235
240
  const stat = fs.lstatSync(filePath);
236
241
  if (!stat.isFile() || stat.isSymbolicLink() || stat.size > 16 * 1024) return null;
237
242
  const record = JSON.parse(fs.readFileSync(filePath, 'utf8'));
@@ -245,13 +250,31 @@ function readActiveRuntime(sharedHome) {
245
250
  || !verifyRuntimePackage(packageRoot, version, { sharedHome })) {
246
251
  return null;
247
252
  }
248
- return Object.freeze({ version, packageRoot: path.resolve(packageRoot), integrity });
253
+ const mode = ownData(record, 'mode');
254
+ const sessionId = ownData(record, 'sessionId');
255
+ const cwd = ownData(record, 'cwd');
256
+ return Object.freeze({
257
+ version,
258
+ packageRoot: path.resolve(packageRoot),
259
+ integrity,
260
+ ...(mode === 'resume' || mode === 'new' ? { mode } : {}),
261
+ ...(typeof sessionId === 'string' && sessionId.length > 0 ? { sessionId } : {}),
262
+ ...(typeof cwd === 'string' && path.isAbsolute(cwd) ? { cwd: path.resolve(cwd) } : {}),
263
+ });
249
264
  } catch {
250
265
  return null;
251
266
  }
252
267
  }
253
268
 
254
- function activateRuntime(sharedHome, target) {
269
+ function readActiveRuntime(sharedHome) {
270
+ return readRuntimeRecord(sharedHome, activeRuntimePath(sharedHome));
271
+ }
272
+
273
+ function readPendingRuntime(sharedHome) {
274
+ return readRuntimeRecord(sharedHome, pendingRuntimePath(sharedHome));
275
+ }
276
+
277
+ function writeRuntimeRecord(sharedHome, fileName, target, extra = {}) {
255
278
  if (!verifyRuntimePackage(target.packageRoot, target.version, { sharedHome })
256
279
  || !validIntegrity(target.integrity)) {
257
280
  throw new Error('RUNNING_UPDATE_TARGET_INVALID');
@@ -259,14 +282,14 @@ function activateRuntime(sharedHome, target) {
259
282
  const updateRoot = path.join(path.resolve(sharedHome), 'updates');
260
283
  ensurePrivateDirectory(path.resolve(sharedHome));
261
284
  ensurePrivateDirectory(updateRoot);
262
- const filePath = activeRuntimePath(sharedHome);
263
- const temporaryPath = path.join(updateRoot, `.${ACTIVE_RUNTIME_FILE}.${process.pid}.${randomUUID()}.tmp`);
285
+ const filePath = path.join(updateRoot, fileName);
286
+ const temporaryPath = path.join(updateRoot, `.${fileName}.${process.pid}.${randomUUID()}.tmp`);
264
287
  try {
265
288
  writePrivateFile(temporaryPath, `${JSON.stringify({
266
289
  version: target.version,
267
290
  packageRoot: path.resolve(target.packageRoot),
268
291
  integrity: target.integrity,
269
- activatedAt: new Date().toISOString(),
292
+ ...extra,
270
293
  })}\n`);
271
294
  fs.renameSync(temporaryPath, filePath);
272
295
  securePrivateFile(filePath);
@@ -276,6 +299,70 @@ function activateRuntime(sharedHome, target) {
276
299
  }
277
300
  }
278
301
 
302
+ function activateRuntime(sharedHome, target) {
303
+ writeRuntimeRecord(sharedHome, ACTIVE_RUNTIME_FILE, target, {
304
+ activatedAt: new Date().toISOString(),
305
+ });
306
+ }
307
+
308
+ function stageRuntime(sharedHome, target, options = {}) {
309
+ const mode = options.mode === 'resume' || options.mode === 'new' ? options.mode : undefined;
310
+ const sessionId = typeof options.sessionId === 'string' && options.sessionId.length > 0
311
+ ? options.sessionId
312
+ : undefined;
313
+ const cwd = typeof options.cwd === 'string' && path.isAbsolute(options.cwd)
314
+ ? path.resolve(options.cwd)
315
+ : undefined;
316
+ writeRuntimeRecord(sharedHome, PENDING_RUNTIME_FILE, target, {
317
+ preparedAt: new Date((options.now || Date.now)()).toISOString(),
318
+ ...(mode === undefined ? {} : { mode }),
319
+ ...(sessionId === undefined ? {} : { sessionId }),
320
+ ...(cwd === undefined ? {} : { cwd }),
321
+ });
322
+ }
323
+
324
+ function clearRuntimeRecord(sharedHome, expected, readRecord, filePath) {
325
+ const runtime = readRecord(sharedHome);
326
+ if (runtime === null) return false;
327
+ if (expected !== undefined
328
+ && (runtime.version !== expected.version
329
+ || runtime.packageRoot !== path.resolve(expected.packageRoot))) {
330
+ return false;
331
+ }
332
+ fs.rmSync(filePath(sharedHome), { force: true });
333
+ return true;
334
+ }
335
+
336
+ function clearActiveRuntime(sharedHome, expected) {
337
+ return clearRuntimeRecord(sharedHome, expected, readActiveRuntime, activeRuntimePath);
338
+ }
339
+
340
+ function clearPendingRuntime(sharedHome, expected) {
341
+ return clearRuntimeRecord(sharedHome, expected, readPendingRuntime, pendingRuntimePath);
342
+ }
343
+
344
+ function restoreActiveRuntime(sharedHome, runtime) {
345
+ if (runtime === null || runtime === undefined) {
346
+ fs.rmSync(activeRuntimePath(sharedHome), { force: true });
347
+ return;
348
+ }
349
+ activateRuntime(sharedHome, runtime);
350
+ }
351
+
352
+ function recordRunningUpdateEvent(sharedHome, event, options = {}) {
353
+ if (typeof sharedHome !== 'string' || !path.isAbsolute(sharedHome)) return false;
354
+ const updateRoot = path.join(path.resolve(sharedHome), 'updates');
355
+ ensurePrivateDirectory(path.resolve(sharedHome));
356
+ ensurePrivateDirectory(updateRoot);
357
+ const filePath = path.join(updateRoot, 'handoff.jsonl');
358
+ fs.appendFileSync(filePath, `${JSON.stringify({
359
+ at: new Date((options.now || Date.now)()).toISOString(),
360
+ ...event,
361
+ })}\n`, { encoding: 'utf8', mode: 0o600 });
362
+ securePrivateFile(filePath);
363
+ return true;
364
+ }
365
+
279
366
  function registryRelease(registry, version) {
280
367
  const versions = ownData(registry, 'versions');
281
368
  const entry = ownData(versions, version);
@@ -469,22 +556,31 @@ async function prepareRunningUpdate(options) {
469
556
 
470
557
  module.exports = {
471
558
  ACTIVE_RUNTIME_FILE,
559
+ PENDING_RUNTIME_FILE,
472
560
  AUTO_UPDATE_SETTLE_MS,
473
561
  RUNNING_UPDATE_HANDOFF_EXIT_CODE,
474
562
  RUNNING_UPDATE_HANDOFF_MESSAGE,
475
563
  RUNNING_UPDATE_MODE_MESSAGE,
476
564
  RUNNING_UPDATE_PREPARED_MESSAGE,
565
+ RUNTIME_EXIT_INTENT_MESSAGE,
477
566
  RUNTIME_READY_MESSAGE,
478
567
  activateRuntime,
479
568
  activeRuntimePath,
569
+ clearActiveRuntime,
570
+ clearPendingRuntime,
480
571
  defaultProbeRuntime,
481
572
  handoffArgsForMode,
482
573
  handoffRuntime,
483
574
  isSafeRuntimeBoundary,
484
575
  prepareRunningUpdate,
485
576
  pruneRunningUpdateReleases,
577
+ recordRunningUpdateEvent,
486
578
  readActiveRuntime,
579
+ readPendingRuntime,
580
+ restoreActiveRuntime,
487
581
  resumeArgsForHandoff,
488
582
  selectRunningUpdateTarget,
583
+ stageRuntime,
584
+ pendingRuntimePath,
489
585
  verifyRuntimePackage,
490
586
  };
package/blun.mjs CHANGED
@@ -78012,7 +78012,10 @@ function parseLoopInterval(rawInterval) {
78012
78012
  cron: hours === 1 ? "0 * * * *" : `0 */${String(hours)} * * *`
78013
78013
  };
78014
78014
  }
78015
- throw invalidLoopInterval(rawInterval);
78015
+ return {
78016
+ interval: `${String(amount)}${unit}`,
78017
+ cron: "* * * * *"
78018
+ };
78016
78019
  }
78017
78020
  function invalidLoopInterval(rawInterval) {
78018
78021
  return new BlunError(ErrorCodes.LOOP_INTERVAL_INVALID, `Invalid loop interval ${JSON.stringify(rawInterval)}`);
@@ -422400,9 +422403,14 @@ function parseLoopCommand(rawArgs) {
422400
422403
  }
422401
422404
  if (LOOP_CREATE_FILLERS.has(tokens[startIndex]?.toLowerCase() ?? "")) startIndex += 1;
422402
422405
  const intervalToken = tokens[startIndex];
422403
- const interval = intervalToken === void 0 ? null : normalizeLoopIntervalToken(intervalToken);
422406
+ let interval = intervalToken === void 0 ? null : normalizeLoopIntervalToken(intervalToken);
422407
+ let intervalTokenCount = 1;
422408
+ if (interval === null && /^\d+$/u.test(intervalToken ?? "") && tokens[startIndex + 1] !== void 0) {
422409
+ interval = normalizeLoopIntervalToken(`${intervalToken}${tokens[startIndex + 1]}`);
422410
+ if (interval !== null) intervalTokenCount = 2;
422411
+ }
422404
422412
  if (interval !== null) {
422405
- const prompt = tokens.slice(startIndex + 1).join(" ").trim();
422413
+ const prompt = tokens.slice(startIndex + intervalTokenCount).join(" ").trim();
422406
422414
  if (prompt.length === 0) return { kind: "error" };
422407
422415
  return {
422408
422416
  kind: "create",
@@ -515041,6 +515049,7 @@ const {
515041
515049
  RUNNING_UPDATE_HANDOFF_MESSAGE,
515042
515050
  RUNNING_UPDATE_MODE_MESSAGE,
515043
515051
  RUNNING_UPDATE_PREPARED_MESSAGE,
515052
+ RUNTIME_EXIT_INTENT_MESSAGE,
515044
515053
  RUNTIME_READY_MESSAGE,
515045
515054
  isSafeRuntimeBoundary,
515046
515055
  pruneRunningUpdateReleases
@@ -515066,17 +515075,6 @@ function notifyRunningRuntimeReady(tui) {
515066
515075
  }
515067
515076
  function requestRunningUpdateAtSafeBoundary(tui) {
515068
515077
  if (tui.runningUpdatePreparedVersion === void 0 || tui.runningUpdatePreparedMode === void 0 || tui.runningUpdateHandoffStarted || !process.connected) return false;
515069
- const waitMs = tui.runningUpdateNotBeforeMs - Date.now();
515070
- if (Number.isFinite(waitMs) && waitMs > 0) {
515071
- if (tui.runningUpdateDelayTimer === void 0) {
515072
- tui.runningUpdateDelayTimer = setTimeout(() => {
515073
- tui.runningUpdateDelayTimer = void 0;
515074
- requestRunningUpdateAtSafeBoundary(tui);
515075
- }, waitMs);
515076
- tui.runningUpdateDelayTimer.unref?.();
515077
- }
515078
- return false;
515079
- }
515080
515078
  const boundaryState = () => ({
515081
515079
  isShuttingDown: tui.isShuttingDown,
515082
515080
  streamingPhase: tui.state.appState.streamingPhase,
@@ -515100,9 +515098,7 @@ function requestRunningUpdateAtSafeBoundary(tui) {
515100
515098
  }, (error) => {
515101
515099
  if (error || !isSafeRuntimeBoundary(boundaryState())) {
515102
515100
  tui.runningUpdateHandoffStarted = false;
515103
- return;
515104
515101
  }
515105
- void tui.stop(RUNNING_UPDATE_HANDOFF_EXIT_CODE);
515106
515102
  });
515107
515103
  } catch {
515108
515104
  tui.runningUpdateHandoffStarted = false;
@@ -515115,6 +515111,7 @@ function installRunningUpdateListener(tui) {
515115
515111
  if (message?.type !== RUNNING_UPDATE_PREPARED_MESSAGE || typeof message.version !== "string" || message.mode !== RUNNING_UPDATE_MODES.RESUME && message.mode !== RUNNING_UPDATE_MODES.NEW) return;
515116
515112
  tui.runningUpdatePreparedVersion = message.version;
515117
515113
  tui.runningUpdatePreparedMode = message.mode;
515114
+ tui.showStatus(`Update ${message.version} ist bereit und wird beim nächsten Start aktiviert.`, "success");
515118
515115
  requestRunningUpdateAtSafeBoundary(tui);
515119
515116
  };
515120
515117
  process.on("message", handler);
@@ -515207,8 +515204,6 @@ var BlunTUI = class {
515207
515204
  runningUpdatePreparedVersion;
515208
515205
  runningUpdatePreparedMode;
515209
515206
  runningUpdateHandoffStarted = false;
515210
- runningUpdateNotBeforeMs = Date.now() + AUTO_UPDATE_SETTLE_MS;
515211
- runningUpdateDelayTimer;
515212
515207
  runningUpdateMessageDispose;
515213
515208
  startupPhaseMs = {};
515214
515209
  lastActivityMode;
@@ -515734,6 +515729,9 @@ var BlunTUI = class {
515734
515729
  async stop(exitCode) {
515735
515730
  if (this.isShuttingDown) return;
515736
515731
  this.isShuttingDown = true;
515732
+ if (exitCode === 0 && process.connected) try {
515733
+ process.send({ type: RUNTIME_EXIT_INTENT_MESSAGE });
515734
+ } catch {}
515737
515735
  this.unregisterSignalHandlers();
515738
515736
  this.aborted = true;
515739
515737
  this.channelQueueDeadline?.dispose();
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "blun-king-cli",
3
- "version": "9.1.208",
3
+ "version": "9.1.210",
4
4
  "description": "BLUN CLI - your own AI agent with a Telegram channel. Get it done. With BLUN.",
5
5
  "license": "MIT",
6
6
  "bin": {