blun-king-cli 9.1.207 → 9.1.209

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,11 +33,15 @@ 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,
37
38
  handoffArgsForMode,
38
39
  handoffRuntime,
39
40
  prepareRunningUpdate,
41
+ pruneRunningUpdateReleases,
42
+ recordRunningUpdateEvent,
40
43
  readActiveRuntime,
44
+ restoreActiveRuntime,
41
45
  } = require('./running-update.cjs');
42
46
  const {
43
47
  RUNNING_UPDATE_MODES,
@@ -64,6 +68,7 @@ const RUNTIME_READY_TIMEOUT_MS = 60_000;
64
68
  const RUNNING_UPDATE_RECHECK_MS = 60_000;
65
69
  const RUNNING_UPDATE_RECHECK_JITTER_MS = 30_000;
66
70
  const RUNNING_UPDATE_HANDOFF_NOTICE = 'Ein Update wird geladen. Die TUI wird anschließend neu gestartet.';
71
+ const RUNNING_UPDATE_STABILIZATION_MS = 2 * 60 * 1000;
67
72
 
68
73
  function runningUpdateRecheckDelay(randomValue = Math.random()) {
69
74
  const normalized = Number.isFinite(randomValue)
@@ -267,6 +272,14 @@ function resolveHandoffCwd(candidate, fallback) {
267
272
  }
268
273
  }
269
274
 
275
+ function shouldRecoverActivatedRuntime(result, recovery, options = {}) {
276
+ if (recovery === undefined || options.runtimeExitIntent === true) return false;
277
+ const signal = result?.signal;
278
+ if (signal === 'SIGINT' || signal === 'SIGTERM' || signal === 'SIGHUP') return false;
279
+ const now = options.now ?? Date.now();
280
+ return now - recovery.activatedAt <= RUNNING_UPDATE_STABILIZATION_MS;
281
+ }
282
+
270
283
  async function superviseProtectedCore(args, env, cwd, releaseNotice, options = {}) {
271
284
  const packageRoot = path.resolve(options.packageRoot || PKG);
272
285
  let preparedTarget;
@@ -275,6 +288,7 @@ async function superviseProtectedCore(args, env, cwd, releaseNotice, options = {
275
288
  let handoffCwd = cwd;
276
289
  let updateStarted = false;
277
290
  let runtimeReady = false;
291
+ let runtimeExitIntent = false;
278
292
  let runningUpdatePollTimer;
279
293
  let runningUpdateMode = readRunningUpdateMode(env.BLUN_HOME);
280
294
  const automaticMode = () => runningUpdateMode === RUNNING_UPDATE_MODES.RESUME
@@ -305,11 +319,17 @@ async function superviseProtectedCore(args, env, cwd, releaseNotice, options = {
305
319
  };
306
320
  const announcePrepared = (child) => {
307
321
  if (preparedTarget === undefined || !automaticMode() || child.connected !== true) return;
308
- child.send({
309
- type: RUNNING_UPDATE_PREPARED_MESSAGE,
310
- version: preparedTarget.version,
311
- mode: runningUpdateMode,
312
- });
322
+ try {
323
+ child.send({
324
+ type: RUNNING_UPDATE_PREPARED_MESSAGE,
325
+ version: preparedTarget.version,
326
+ mode: runningUpdateMode,
327
+ }, (error) => {
328
+ if (error) options.onRunningUpdateError?.(error);
329
+ });
330
+ } catch (error) {
331
+ options.onRunningUpdateError?.(error);
332
+ }
313
333
  };
314
334
  const startPreparation = (child) => {
315
335
  if (updateStarted || options.runningUpdate === false || !automaticMode()) return;
@@ -340,6 +360,7 @@ async function superviseProtectedCore(args, env, cwd, releaseNotice, options = {
340
360
  });
341
361
  };
342
362
  const handleCoreMessage = (message, child) => {
363
+ if (message?.type === RUNTIME_EXIT_INTENT_MESSAGE) runtimeExitIntent = true;
343
364
  if (message?.type === RUNTIME_READY_MESSAGE) {
344
365
  runtimeReady = true;
345
366
  refreshRunningUpdateMode();
@@ -394,12 +415,13 @@ async function superviseProtectedCore(args, env, cwd, releaseNotice, options = {
394
415
  const result = await core.completed;
395
416
  clearRunningUpdatePoll();
396
417
  if (existingMessageHandler) core.child.off('message', existingMessageHandler);
397
- if (result.error) throw result.error;
398
418
  if (result.code === RUNNING_UPDATE_HANDOFF_EXIT_CODE
399
419
  && preparedTarget !== undefined
400
420
  && handoffSessionId !== undefined
401
421
  && handoffMode !== undefined) {
402
422
  writeRunningUpdateHandoffNotice(options.runningUpdateNoticeOutput || process.stdout);
423
+ const previousActiveRuntime = (options.readActiveRuntime || readActiveRuntime)(env.BLUN_SHARED_HOME);
424
+ const activatedAt = (options.nowImpl || Date.now)();
403
425
  const handoff = await handoffRuntime({
404
426
  target: preparedTarget,
405
427
  previous: { packageRoot, version: readPackageVersionAt(packageRoot) },
@@ -418,7 +440,21 @@ async function superviseProtectedCore(args, env, cwd, releaseNotice, options = {
418
440
  }
419
441
  return { ...nextCore, ready };
420
442
  },
421
- activateTarget: (target) => (options.activateRuntime || activateRuntime)(env.BLUN_SHARED_HOME, target),
443
+ activateTarget: async (target) => {
444
+ await (options.activateRuntime || activateRuntime)(env.BLUN_SHARED_HOME, target);
445
+ (options.recordRunningUpdateEvent || recordRunningUpdateEvent)(env.BLUN_SHARED_HOME, {
446
+ event: 'activated',
447
+ fromVersion: readPackageVersionAt(packageRoot),
448
+ toVersion: target.version,
449
+ }, { now: options.nowImpl });
450
+ try {
451
+ await (options.pruneRunningUpdateReleases || pruneRunningUpdateReleases)(env.BLUN_SHARED_HOME, {
452
+ activePackageRoot: target.packageRoot,
453
+ });
454
+ } catch (error) {
455
+ options.onRunningUpdateError?.(error);
456
+ }
457
+ },
422
458
  });
423
459
  const resumed = handoff.core;
424
460
  if (resumed === undefined || resumed.ready !== true) return 1;
@@ -427,8 +463,45 @@ async function superviseProtectedCore(args, env, cwd, releaseNotice, options = {
427
463
  ...options,
428
464
  existingCore: resumed,
429
465
  packageRoot: handoff.runtime.packageRoot,
466
+ recoveryRuntime: handoff.kind === 'activated' ? {
467
+ activatedAt,
468
+ activeRuntime: previousActiveRuntime,
469
+ packageRoot,
470
+ version: readPackageVersionAt(packageRoot),
471
+ } : undefined,
430
472
  });
431
473
  }
474
+ const recovery = options.recoveryRuntime;
475
+ if (shouldRecoverActivatedRuntime(result, recovery, {
476
+ now: (options.nowImpl || Date.now)(),
477
+ runtimeExitIntent,
478
+ })) {
479
+ (options.recordRunningUpdateEvent || recordRunningUpdateEvent)(env.BLUN_SHARED_HOME, {
480
+ event: 'post-activation-exit',
481
+ activeVersion: readPackageVersionAt(packageRoot),
482
+ code: Number.isInteger(result.code) ? result.code : null,
483
+ error: result.error?.message || null,
484
+ signal: result.signal || null,
485
+ recoveryVersion: recovery.version,
486
+ }, { now: options.nowImpl });
487
+ await (options.restoreActiveRuntime || restoreActiveRuntime)(env.BLUN_SHARED_HOME, recovery.activeRuntime);
488
+ const fallback = spawnCore(args, env, cwd, { packageRoot: recovery.packageRoot });
489
+ const fallbackLoaded = await fallback.loaded;
490
+ const fallbackReady = fallbackLoaded && await waitForRuntimeReady(fallback, options.readyTimeoutMs);
491
+ (options.recordRunningUpdateEvent || recordRunningUpdateEvent)(env.BLUN_SHARED_HOME, {
492
+ event: fallbackReady ? 'recovery-ready' : 'recovery-failed',
493
+ version: recovery.version,
494
+ }, { now: options.nowImpl });
495
+ if (fallbackReady) {
496
+ return superviseProtectedCore(args, env, cwd, async () => {}, {
497
+ ...options,
498
+ existingCore: fallback,
499
+ packageRoot: recovery.packageRoot,
500
+ recoveryRuntime: undefined,
501
+ });
502
+ }
503
+ }
504
+ if (result.error) throw result.error;
432
505
  return exitCodeForChild(result, loaded);
433
506
  }
434
507
 
@@ -758,12 +831,14 @@ async function runLauncher(options = {}) {
758
831
  module.exports = {
759
832
  RUNNING_UPDATE_RECHECK_JITTER_MS,
760
833
  RUNNING_UPDATE_RECHECK_MS,
834
+ RUNNING_UPDATE_STABILIZATION_MS,
761
835
  createManagedNodeEnvironment,
762
836
  resolveLauncherPrivatePaths,
763
837
  resolveHandoffCwd,
764
838
  runningUpdateRecheckDelay,
765
839
  runLauncher,
766
840
  scheduleRunningUpdateRecheck,
841
+ shouldRecoverActivatedRuntime,
767
842
  shouldDetachProtectedCore,
768
843
  spawnManagedLauncher,
769
844
  superviseProtectedCore,
@@ -7,6 +7,7 @@ const path = require('node:path');
7
7
  const { spawn } = require('node:child_process');
8
8
 
9
9
  const { compareSemver, requestTrustedJson } = require('./update-notice');
10
+ const { tryAcquireUpdateLease } = require('./update-lease');
10
11
  const { ensurePrivateDirectory, securePrivateFile, writePrivateFile } = require('./private-paths');
11
12
 
12
13
  const PACKAGE_NAME = 'blun-king-cli';
@@ -18,7 +19,10 @@ const RUNNING_UPDATE_PREPARED_MESSAGE = 'blun-running-update-prepared';
18
19
  const RUNNING_UPDATE_HANDOFF_MESSAGE = 'blun-running-update-handoff';
19
20
  const RUNNING_UPDATE_MODE_MESSAGE = 'blun-running-update-mode';
20
21
  const RUNTIME_READY_MESSAGE = 'blun-runtime-session-ready';
22
+ const RUNTIME_EXIT_INTENT_MESSAGE = 'blun-runtime-exit-intent';
21
23
  const MAX_TARBALL_BYTES = 128 * 1024 * 1024;
24
+ const AUTO_UPDATE_SETTLE_MS = 30 * 60 * 1000;
25
+ const DEFAULT_RETAINED_RELEASE_VERSIONS = 3;
22
26
 
23
27
  function ownData(value, key) {
24
28
  if (typeof value !== 'object' || value === null || Array.isArray(value)) return undefined;
@@ -37,22 +41,110 @@ function validIntegrity(value) {
37
41
  }
38
42
  }
39
43
 
40
- function selectRunningUpdateTarget(currentVersion, manifest) {
44
+ function selectRunningUpdateTarget(currentVersion, manifest, options = {}) {
41
45
  const name = ownData(manifest, 'name');
42
46
  const version = ownData(manifest, 'latest');
43
47
  const integrity = ownData(manifest, 'integrity');
44
48
  const releasedAt = ownData(manifest, 'releasedAt');
49
+ const releasedAtMs = Date.parse(releasedAt);
50
+ const now = options.now || Date.now;
45
51
  if (name !== PACKAGE_NAME
46
52
  || typeof version !== 'string'
47
53
  || compareSemver(currentVersion, version) !== -1
48
54
  || !validIntegrity(integrity)
49
55
  || typeof releasedAt !== 'string'
50
- || !Number.isFinite(Date.parse(releasedAt))) {
56
+ || !Number.isFinite(releasedAtMs)
57
+ || now() - releasedAtMs < AUTO_UPDATE_SETTLE_MS) {
51
58
  return null;
52
59
  }
53
60
  return Object.freeze({ version, integrity });
54
61
  }
55
62
 
63
+ function releaseVersionEntries(sharedHome) {
64
+ const root = releasesRoot(sharedHome);
65
+ let entries;
66
+ try {
67
+ entries = fs.readdirSync(root, { withFileTypes: true });
68
+ } catch {
69
+ return [];
70
+ }
71
+ const releases = [];
72
+ for (const entry of entries) {
73
+ if (!entry.isDirectory() || entry.isSymbolicLink()) continue;
74
+ const version = entry.name;
75
+ try {
76
+ if (compareSemver('0.0.0', version) !== -1) continue;
77
+ } catch {
78
+ continue;
79
+ }
80
+ const versionRoot = path.join(root, version);
81
+ const digestEntries = fs.readdirSync(versionRoot, { withFileTypes: true });
82
+ const packageRoots = [];
83
+ let valid = digestEntries.length > 0;
84
+ for (const digestEntry of digestEntries) {
85
+ if (!digestEntry.isDirectory() || digestEntry.isSymbolicLink()) {
86
+ valid = false;
87
+ break;
88
+ }
89
+ const packageRoot = path.join(versionRoot, digestEntry.name, 'node_modules', PACKAGE_NAME);
90
+ if (!verifyRuntimePackage(packageRoot, version, { allowedRoot: versionRoot })) {
91
+ valid = false;
92
+ break;
93
+ }
94
+ packageRoots.push(path.resolve(packageRoot));
95
+ }
96
+ if (valid && packageRoots.length > 0) releases.push({ packageRoots, version, versionRoot });
97
+ }
98
+ releases.sort((left, right) => compareSemver(right.version, left.version));
99
+ return releases;
100
+ }
101
+
102
+ async function pruneRunningUpdateReleases(sharedHome, options = {}) {
103
+ const keepVersions = Number.isInteger(options.keepVersions) && options.keepVersions >= 1
104
+ ? options.keepVersions
105
+ : DEFAULT_RETAINED_RELEASE_VERSIONS;
106
+ const activePackageRoot = typeof options.activePackageRoot === 'string'
107
+ ? path.resolve(options.activePackageRoot)
108
+ : readActiveRuntime(sharedHome)?.packageRoot;
109
+ const acquire = options.tryAcquireLease
110
+ || (({ packageRoot }) => tryAcquireUpdateLease({ packageRoot, role: 'install' }));
111
+ const releases = releaseVersionEntries(sharedHome);
112
+ const kept = new Set(releases.slice(0, keepVersions).map((release) => release.version));
113
+ for (const release of releases) {
114
+ if (release.packageRoots.includes(activePackageRoot)) kept.add(release.version);
115
+ }
116
+ const removedVersions = [];
117
+ const busyVersions = [];
118
+ for (const release of releases) {
119
+ if (kept.has(release.version)) continue;
120
+ const leases = [];
121
+ let blocked = false;
122
+ try {
123
+ for (const packageRoot of release.packageRoots) {
124
+ const result = await acquire({ packageRoot });
125
+ if (!result?.acquired || !result.lease) {
126
+ blocked = true;
127
+ break;
128
+ }
129
+ leases.push(result.lease);
130
+ }
131
+ if (blocked) {
132
+ busyVersions.push(release.version);
133
+ continue;
134
+ }
135
+ for (const lease of leases) lease.assertOwned();
136
+ fs.rmSync(release.versionRoot, { recursive: true, force: true });
137
+ removedVersions.push(release.version);
138
+ } finally {
139
+ await Promise.allSettled(leases.map((lease) => lease.release()));
140
+ }
141
+ }
142
+ return Object.freeze({
143
+ busyVersions: Object.freeze(busyVersions),
144
+ removedVersions: Object.freeze(removedVersions),
145
+ });
146
+ }
147
+
56
148
  function isSafeRuntimeBoundary(state) {
57
149
  return state?.isShuttingDown === false
58
150
  && state.streamingPhase === 'idle'
@@ -185,6 +277,28 @@ function activateRuntime(sharedHome, target) {
185
277
  }
186
278
  }
187
279
 
280
+ function restoreActiveRuntime(sharedHome, runtime) {
281
+ if (runtime === null || runtime === undefined) {
282
+ fs.rmSync(activeRuntimePath(sharedHome), { force: true });
283
+ return;
284
+ }
285
+ activateRuntime(sharedHome, runtime);
286
+ }
287
+
288
+ function recordRunningUpdateEvent(sharedHome, event, options = {}) {
289
+ if (typeof sharedHome !== 'string' || !path.isAbsolute(sharedHome)) return false;
290
+ const updateRoot = path.join(path.resolve(sharedHome), 'updates');
291
+ ensurePrivateDirectory(path.resolve(sharedHome));
292
+ ensurePrivateDirectory(updateRoot);
293
+ const filePath = path.join(updateRoot, 'handoff.jsonl');
294
+ fs.appendFileSync(filePath, `${JSON.stringify({
295
+ at: new Date((options.now || Date.now)()).toISOString(),
296
+ ...event,
297
+ })}\n`, { encoding: 'utf8', mode: 0o600 });
298
+ securePrivateFile(filePath);
299
+ return true;
300
+ }
301
+
188
302
  function registryRelease(registry, version) {
189
303
  const versions = ownData(registry, 'versions');
190
304
  const entry = ownData(versions, version);
@@ -325,7 +439,7 @@ async function prepareRunningUpdate(options) {
325
439
  const manifest = await (options.loadManifest
326
440
  ? options.loadManifest()
327
441
  : requestTrustedJson(MANIFEST_URL));
328
- const target = selectRunningUpdateTarget(options.currentVersion, manifest);
442
+ const target = selectRunningUpdateTarget(options.currentVersion, manifest, { now: options.now });
329
443
  if (target === null) return null;
330
444
  const registry = await (options.loadRegistry
331
445
  ? options.loadRegistry()
@@ -378,10 +492,12 @@ async function prepareRunningUpdate(options) {
378
492
 
379
493
  module.exports = {
380
494
  ACTIVE_RUNTIME_FILE,
495
+ AUTO_UPDATE_SETTLE_MS,
381
496
  RUNNING_UPDATE_HANDOFF_EXIT_CODE,
382
497
  RUNNING_UPDATE_HANDOFF_MESSAGE,
383
498
  RUNNING_UPDATE_MODE_MESSAGE,
384
499
  RUNNING_UPDATE_PREPARED_MESSAGE,
500
+ RUNTIME_EXIT_INTENT_MESSAGE,
385
501
  RUNTIME_READY_MESSAGE,
386
502
  activateRuntime,
387
503
  activeRuntimePath,
@@ -390,7 +506,10 @@ module.exports = {
390
506
  handoffRuntime,
391
507
  isSafeRuntimeBoundary,
392
508
  prepareRunningUpdate,
509
+ pruneRunningUpdateReleases,
510
+ recordRunningUpdateEvent,
393
511
  readActiveRuntime,
512
+ restoreActiveRuntime,
394
513
  resumeArgsForHandoff,
395
514
  selectRunningUpdateTarget,
396
515
  verifyRuntimePackage,
package/blun.mjs CHANGED
@@ -515036,12 +515036,15 @@ function turnsToTrim(turns, maxTurns, hysteresis) {
515036
515036
  //#endregion
515037
515037
  //#region src/tui/blun-tui.ts
515038
515038
  const {
515039
+ AUTO_UPDATE_SETTLE_MS,
515039
515040
  RUNNING_UPDATE_HANDOFF_EXIT_CODE,
515040
515041
  RUNNING_UPDATE_HANDOFF_MESSAGE,
515041
515042
  RUNNING_UPDATE_MODE_MESSAGE,
515042
515043
  RUNNING_UPDATE_PREPARED_MESSAGE,
515044
+ RUNTIME_EXIT_INTENT_MESSAGE,
515043
515045
  RUNTIME_READY_MESSAGE,
515044
- isSafeRuntimeBoundary
515046
+ isSafeRuntimeBoundary,
515047
+ pruneRunningUpdateReleases
515045
515048
  } = __require("./bin/running-update.cjs");
515046
515049
  const {
515047
515050
  RUNNING_UPDATE_MODES,
@@ -515049,6 +515052,9 @@ const {
515049
515052
  writeRunningUpdateMode
515050
515053
  } = __require("./bin/running-update-preference.cjs");
515051
515054
  function notifyRunningRuntimeReady(tui) {
515055
+ if (typeof process.env["BLUN_SHARED_HOME"] === "string" && process.env["BLUN_SHARED_HOME"].length > 0) void pruneRunningUpdateReleases(process.env["BLUN_SHARED_HOME"], {
515056
+ activePackageRoot: __dirname
515057
+ }).catch(() => {});
515052
515058
  if (!process.connected) return;
515053
515059
  const sessionId = tui.getCurrentSessionId();
515054
515060
  if (sessionId.length === 0) return;
@@ -515061,6 +515067,17 @@ function notifyRunningRuntimeReady(tui) {
515061
515067
  }
515062
515068
  function requestRunningUpdateAtSafeBoundary(tui) {
515063
515069
  if (tui.runningUpdatePreparedVersion === void 0 || tui.runningUpdatePreparedMode === void 0 || tui.runningUpdateHandoffStarted || !process.connected) return false;
515070
+ const waitMs = tui.runningUpdateNotBeforeMs - Date.now();
515071
+ if (Number.isFinite(waitMs) && waitMs > 0) {
515072
+ if (tui.runningUpdateDelayTimer === void 0) {
515073
+ tui.runningUpdateDelayTimer = setTimeout(() => {
515074
+ tui.runningUpdateDelayTimer = void 0;
515075
+ requestRunningUpdateAtSafeBoundary(tui);
515076
+ }, waitMs);
515077
+ tui.runningUpdateDelayTimer.unref?.();
515078
+ }
515079
+ return false;
515080
+ }
515064
515081
  const boundaryState = () => ({
515065
515082
  isShuttingDown: tui.isShuttingDown,
515066
515083
  streamingPhase: tui.state.appState.streamingPhase,
@@ -515191,6 +515208,8 @@ var BlunTUI = class {
515191
515208
  runningUpdatePreparedVersion;
515192
515209
  runningUpdatePreparedMode;
515193
515210
  runningUpdateHandoffStarted = false;
515211
+ runningUpdateNotBeforeMs = Date.now() + AUTO_UPDATE_SETTLE_MS;
515212
+ runningUpdateDelayTimer;
515194
515213
  runningUpdateMessageDispose;
515195
515214
  startupPhaseMs = {};
515196
515215
  lastActivityMode;
@@ -515716,6 +515735,9 @@ var BlunTUI = class {
515716
515735
  async stop(exitCode) {
515717
515736
  if (this.isShuttingDown) return;
515718
515737
  this.isShuttingDown = true;
515738
+ if (exitCode === 0 && process.connected) try {
515739
+ process.send({ type: RUNTIME_EXIT_INTENT_MESSAGE });
515740
+ } catch {}
515719
515741
  this.unregisterSignalHandlers();
515720
515742
  this.aborted = true;
515721
515743
  this.channelQueueDeadline?.dispose();
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "blun-king-cli",
3
- "version": "9.1.207",
3
+ "version": "9.1.209",
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": {