blun-king-cli 9.1.206 → 9.1.208

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.
@@ -37,6 +37,7 @@ const {
37
37
  handoffArgsForMode,
38
38
  handoffRuntime,
39
39
  prepareRunningUpdate,
40
+ pruneRunningUpdateReleases,
40
41
  readActiveRuntime,
41
42
  } = require('./running-update.cjs');
42
43
  const {
@@ -418,7 +419,16 @@ async function superviseProtectedCore(args, env, cwd, releaseNotice, options = {
418
419
  }
419
420
  return { ...nextCore, ready };
420
421
  },
421
- activateTarget: (target) => (options.activateRuntime || activateRuntime)(env.BLUN_SHARED_HOME, target),
422
+ activateTarget: async (target) => {
423
+ await (options.activateRuntime || activateRuntime)(env.BLUN_SHARED_HOME, target);
424
+ try {
425
+ await (options.pruneRunningUpdateReleases || pruneRunningUpdateReleases)(env.BLUN_SHARED_HOME, {
426
+ activePackageRoot: target.packageRoot,
427
+ });
428
+ } catch (error) {
429
+ options.onRunningUpdateError?.(error);
430
+ }
431
+ },
422
432
  });
423
433
  const resumed = handoff.core;
424
434
  if (resumed === undefined || resumed.ready !== true) return 1;
@@ -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';
@@ -19,6 +20,8 @@ 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';
21
22
  const MAX_TARBALL_BYTES = 128 * 1024 * 1024;
23
+ const AUTO_UPDATE_SETTLE_MS = 30 * 60 * 1000;
24
+ const DEFAULT_RETAINED_RELEASE_VERSIONS = 3;
22
25
 
23
26
  function ownData(value, key) {
24
27
  if (typeof value !== 'object' || value === null || Array.isArray(value)) return undefined;
@@ -37,22 +40,110 @@ function validIntegrity(value) {
37
40
  }
38
41
  }
39
42
 
40
- function selectRunningUpdateTarget(currentVersion, manifest) {
43
+ function selectRunningUpdateTarget(currentVersion, manifest, options = {}) {
41
44
  const name = ownData(manifest, 'name');
42
45
  const version = ownData(manifest, 'latest');
43
46
  const integrity = ownData(manifest, 'integrity');
44
47
  const releasedAt = ownData(manifest, 'releasedAt');
48
+ const releasedAtMs = Date.parse(releasedAt);
49
+ const now = options.now || Date.now;
45
50
  if (name !== PACKAGE_NAME
46
51
  || typeof version !== 'string'
47
52
  || compareSemver(currentVersion, version) !== -1
48
53
  || !validIntegrity(integrity)
49
54
  || typeof releasedAt !== 'string'
50
- || !Number.isFinite(Date.parse(releasedAt))) {
55
+ || !Number.isFinite(releasedAtMs)
56
+ || now() - releasedAtMs < AUTO_UPDATE_SETTLE_MS) {
51
57
  return null;
52
58
  }
53
59
  return Object.freeze({ version, integrity });
54
60
  }
55
61
 
62
+ function releaseVersionEntries(sharedHome) {
63
+ const root = releasesRoot(sharedHome);
64
+ let entries;
65
+ try {
66
+ entries = fs.readdirSync(root, { withFileTypes: true });
67
+ } catch {
68
+ return [];
69
+ }
70
+ const releases = [];
71
+ for (const entry of entries) {
72
+ if (!entry.isDirectory() || entry.isSymbolicLink()) continue;
73
+ const version = entry.name;
74
+ try {
75
+ if (compareSemver('0.0.0', version) !== -1) continue;
76
+ } catch {
77
+ continue;
78
+ }
79
+ const versionRoot = path.join(root, version);
80
+ const digestEntries = fs.readdirSync(versionRoot, { withFileTypes: true });
81
+ const packageRoots = [];
82
+ let valid = digestEntries.length > 0;
83
+ for (const digestEntry of digestEntries) {
84
+ if (!digestEntry.isDirectory() || digestEntry.isSymbolicLink()) {
85
+ valid = false;
86
+ break;
87
+ }
88
+ const packageRoot = path.join(versionRoot, digestEntry.name, 'node_modules', PACKAGE_NAME);
89
+ if (!verifyRuntimePackage(packageRoot, version, { allowedRoot: versionRoot })) {
90
+ valid = false;
91
+ break;
92
+ }
93
+ packageRoots.push(path.resolve(packageRoot));
94
+ }
95
+ if (valid && packageRoots.length > 0) releases.push({ packageRoots, version, versionRoot });
96
+ }
97
+ releases.sort((left, right) => compareSemver(right.version, left.version));
98
+ return releases;
99
+ }
100
+
101
+ async function pruneRunningUpdateReleases(sharedHome, options = {}) {
102
+ const keepVersions = Number.isInteger(options.keepVersions) && options.keepVersions >= 1
103
+ ? options.keepVersions
104
+ : DEFAULT_RETAINED_RELEASE_VERSIONS;
105
+ const activePackageRoot = typeof options.activePackageRoot === 'string'
106
+ ? path.resolve(options.activePackageRoot)
107
+ : readActiveRuntime(sharedHome)?.packageRoot;
108
+ const acquire = options.tryAcquireLease
109
+ || (({ packageRoot }) => tryAcquireUpdateLease({ packageRoot, role: 'install' }));
110
+ const releases = releaseVersionEntries(sharedHome);
111
+ const kept = new Set(releases.slice(0, keepVersions).map((release) => release.version));
112
+ for (const release of releases) {
113
+ if (release.packageRoots.includes(activePackageRoot)) kept.add(release.version);
114
+ }
115
+ const removedVersions = [];
116
+ const busyVersions = [];
117
+ for (const release of releases) {
118
+ if (kept.has(release.version)) continue;
119
+ const leases = [];
120
+ let blocked = false;
121
+ try {
122
+ for (const packageRoot of release.packageRoots) {
123
+ const result = await acquire({ packageRoot });
124
+ if (!result?.acquired || !result.lease) {
125
+ blocked = true;
126
+ break;
127
+ }
128
+ leases.push(result.lease);
129
+ }
130
+ if (blocked) {
131
+ busyVersions.push(release.version);
132
+ continue;
133
+ }
134
+ for (const lease of leases) lease.assertOwned();
135
+ fs.rmSync(release.versionRoot, { recursive: true, force: true });
136
+ removedVersions.push(release.version);
137
+ } finally {
138
+ await Promise.allSettled(leases.map((lease) => lease.release()));
139
+ }
140
+ }
141
+ return Object.freeze({
142
+ busyVersions: Object.freeze(busyVersions),
143
+ removedVersions: Object.freeze(removedVersions),
144
+ });
145
+ }
146
+
56
147
  function isSafeRuntimeBoundary(state) {
57
148
  return state?.isShuttingDown === false
58
149
  && state.streamingPhase === 'idle'
@@ -325,7 +416,7 @@ async function prepareRunningUpdate(options) {
325
416
  const manifest = await (options.loadManifest
326
417
  ? options.loadManifest()
327
418
  : requestTrustedJson(MANIFEST_URL));
328
- const target = selectRunningUpdateTarget(options.currentVersion, manifest);
419
+ const target = selectRunningUpdateTarget(options.currentVersion, manifest, { now: options.now });
329
420
  if (target === null) return null;
330
421
  const registry = await (options.loadRegistry
331
422
  ? options.loadRegistry()
@@ -378,6 +469,7 @@ async function prepareRunningUpdate(options) {
378
469
 
379
470
  module.exports = {
380
471
  ACTIVE_RUNTIME_FILE,
472
+ AUTO_UPDATE_SETTLE_MS,
381
473
  RUNNING_UPDATE_HANDOFF_EXIT_CODE,
382
474
  RUNNING_UPDATE_HANDOFF_MESSAGE,
383
475
  RUNNING_UPDATE_MODE_MESSAGE,
@@ -390,6 +482,7 @@ module.exports = {
390
482
  handoffRuntime,
391
483
  isSafeRuntimeBoundary,
392
484
  prepareRunningUpdate,
485
+ pruneRunningUpdateReleases,
393
486
  readActiveRuntime,
394
487
  resumeArgsForHandoff,
395
488
  selectRunningUpdateTarget,
@@ -0,0 +1,57 @@
1
+ 'use strict';
2
+
3
+ const TELEMETRY_RETRY_MAX_FILES = 16;
4
+ const TELEMETRY_SPOOL_MAX_FILES = 512;
5
+ const TELEMETRY_SPOOL_MAX_BYTES = 16 * 1024 * 1024;
6
+ const TELEMETRY_SPOOL_MAX_AGE_MS = 7 * 24 * 60 * 60 * 1000;
7
+
8
+ function isFailedTelemetryEntry(entry) {
9
+ return entry !== null
10
+ && typeof entry === 'object'
11
+ && /^failed_[^.]+\.jsonl$/u.test(entry.name ?? '')
12
+ && Number.isFinite(entry.mtimeMs)
13
+ && Number.isFinite(entry.size)
14
+ && entry.size >= 0;
15
+ }
16
+
17
+ function oldestFirst(left, right) {
18
+ return left.mtimeMs - right.mtimeMs || left.name.localeCompare(right.name);
19
+ }
20
+
21
+ function newestFirst(left, right) {
22
+ return right.mtimeMs - left.mtimeMs || right.name.localeCompare(left.name);
23
+ }
24
+
25
+ function selectTelemetryRetryEntries(entries, maxFiles = TELEMETRY_RETRY_MAX_FILES) {
26
+ if (!Number.isSafeInteger(maxFiles) || maxFiles < 0) return [];
27
+ return entries.filter(isFailedTelemetryEntry).sort(oldestFirst).slice(0, maxFiles);
28
+ }
29
+
30
+ function selectTelemetrySpoolPruneEntries(entries, now = Date.now()) {
31
+ const failed = entries.filter(isFailedTelemetryEntry);
32
+ const expired = failed.filter((entry) => now - entry.mtimeMs > TELEMETRY_SPOOL_MAX_AGE_MS);
33
+ const expiredNames = new Set(expired.map((entry) => entry.name));
34
+ const live = failed.filter((entry) => !expiredNames.has(entry.name)).sort(newestFirst);
35
+ const keptNames = new Set();
36
+ let keptBytes = 0;
37
+
38
+ for (const entry of live) {
39
+ if (keptNames.size >= TELEMETRY_SPOOL_MAX_FILES) continue;
40
+ if (keptBytes + entry.size > TELEMETRY_SPOOL_MAX_BYTES) continue;
41
+ keptNames.add(entry.name);
42
+ keptBytes += entry.size;
43
+ }
44
+
45
+ return failed
46
+ .filter((entry) => expiredNames.has(entry.name) || !keptNames.has(entry.name))
47
+ .sort(oldestFirst);
48
+ }
49
+
50
+ module.exports = {
51
+ TELEMETRY_RETRY_MAX_FILES,
52
+ TELEMETRY_SPOOL_MAX_AGE_MS,
53
+ TELEMETRY_SPOOL_MAX_BYTES,
54
+ TELEMETRY_SPOOL_MAX_FILES,
55
+ selectTelemetryRetryEntries,
56
+ selectTelemetrySpoolPruneEntries,
57
+ };
package/blun.mjs CHANGED
@@ -328163,6 +328163,10 @@ const RETRY_BACKOFFS_MS = [
328163
328163
  16e3
328164
328164
  ];
328165
328165
  const DEFAULT_REQUEST_TIMEOUT_MS = 1e4;
328166
+ const {
328167
+ selectTelemetryRetryEntries,
328168
+ selectTelemetrySpoolPruneEntries
328169
+ } = createRequire(import.meta.url)("./bin/telemetry-spool-policy.cjs");
328166
328170
  var AsyncTransport = class {
328167
328171
  homeDir;
328168
328172
  deviceId;
@@ -328173,6 +328177,7 @@ var AsyncTransport = class {
328173
328177
  requestTimeoutMs;
328174
328178
  sleepImpl;
328175
328179
  now;
328180
+ spoolWritesSincePrune = 0;
328176
328181
  constructor(options) {
328177
328182
  this.homeDir = options.homeDir;
328178
328183
  this.deviceId = options.deviceId;
@@ -328235,26 +328240,44 @@ var AsyncTransport = class {
328235
328240
  try {
328236
328241
  chmodSync(path, 384);
328237
328242
  } catch {}
328243
+ this.spoolWritesSincePrune += 1;
328244
+ if (this.spoolWritesSincePrune === 1 || this.spoolWritesSincePrune >= 64) {
328245
+ this.pruneDiskSpool();
328246
+ this.spoolWritesSincePrune = 0;
328247
+ }
328238
328248
  }
328239
- async retryDiskEvents() {
328240
- let entries;
328249
+ readSpoolEntries() {
328250
+ let names;
328241
328251
  try {
328242
- entries = readdirSync(this.telemetryDir());
328252
+ names = readdirSync(this.telemetryDir());
328243
328253
  } catch {
328244
- return;
328254
+ return [];
328245
328255
  }
328246
- const now = this.now();
328247
- for (const entry of entries) {
328248
- if (!entry.startsWith("failed_") || !entry.endsWith(".jsonl")) continue;
328249
- const path = join(this.telemetryDir(), entry);
328256
+ const entries = [];
328257
+ for (const name of names) {
328258
+ if (!name.startsWith("failed_") || !name.endsWith(".jsonl")) continue;
328259
+ const path = join(this.telemetryDir(), name);
328250
328260
  try {
328251
- if (now - statSync(path).mtimeMs > 6048e5) {
328252
- unlinkSync(path);
328253
- continue;
328254
- }
328255
- } catch {
328256
- continue;
328257
- }
328261
+ const stats = statSync(path);
328262
+ entries.push({ name, mtimeMs: stats.mtimeMs, size: stats.size });
328263
+ } catch {}
328264
+ }
328265
+ return entries;
328266
+ }
328267
+ pruneDiskSpool(spoolEntries = this.readSpoolEntries()) {
328268
+ const pruned = selectTelemetrySpoolPruneEntries(spoolEntries, this.now());
328269
+ for (const entry of pruned) try {
328270
+ unlinkSync(join(this.telemetryDir(), entry.name));
328271
+ } catch {}
328272
+ return new Set(pruned.map((entry) => entry.name));
328273
+ }
328274
+ async retryDiskEvents() {
328275
+ const spoolEntries = this.readSpoolEntries();
328276
+ const prunedNames = this.pruneDiskSpool(spoolEntries);
328277
+ const retryEntries = selectTelemetryRetryEntries(spoolEntries
328278
+ .filter((entry) => !prunedNames.has(entry.name)));
328279
+ for (const entry of retryEntries) {
328280
+ const path = join(this.telemetryDir(), entry.name);
328258
328281
  let events;
328259
328282
  let payload;
328260
328283
  try {
@@ -328270,7 +328293,7 @@ var AsyncTransport = class {
328270
328293
  await this.sendHttp(payload);
328271
328294
  unlinkSync(path);
328272
328295
  } catch (error) {
328273
- if (error instanceof TransientTelemetryError) continue;
328296
+ if (error instanceof TransientTelemetryError) break;
328274
328297
  }
328275
328298
  }
328276
328299
  }
@@ -515013,12 +515036,14 @@ function turnsToTrim(turns, maxTurns, hysteresis) {
515013
515036
  //#endregion
515014
515037
  //#region src/tui/blun-tui.ts
515015
515038
  const {
515039
+ AUTO_UPDATE_SETTLE_MS,
515016
515040
  RUNNING_UPDATE_HANDOFF_EXIT_CODE,
515017
515041
  RUNNING_UPDATE_HANDOFF_MESSAGE,
515018
515042
  RUNNING_UPDATE_MODE_MESSAGE,
515019
515043
  RUNNING_UPDATE_PREPARED_MESSAGE,
515020
515044
  RUNTIME_READY_MESSAGE,
515021
- isSafeRuntimeBoundary
515045
+ isSafeRuntimeBoundary,
515046
+ pruneRunningUpdateReleases
515022
515047
  } = __require("./bin/running-update.cjs");
515023
515048
  const {
515024
515049
  RUNNING_UPDATE_MODES,
@@ -515026,6 +515051,9 @@ const {
515026
515051
  writeRunningUpdateMode
515027
515052
  } = __require("./bin/running-update-preference.cjs");
515028
515053
  function notifyRunningRuntimeReady(tui) {
515054
+ if (typeof process.env["BLUN_SHARED_HOME"] === "string" && process.env["BLUN_SHARED_HOME"].length > 0) void pruneRunningUpdateReleases(process.env["BLUN_SHARED_HOME"], {
515055
+ activePackageRoot: __dirname
515056
+ }).catch(() => {});
515029
515057
  if (!process.connected) return;
515030
515058
  const sessionId = tui.getCurrentSessionId();
515031
515059
  if (sessionId.length === 0) return;
@@ -515038,6 +515066,17 @@ function notifyRunningRuntimeReady(tui) {
515038
515066
  }
515039
515067
  function requestRunningUpdateAtSafeBoundary(tui) {
515040
515068
  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
+ }
515041
515080
  const boundaryState = () => ({
515042
515081
  isShuttingDown: tui.isShuttingDown,
515043
515082
  streamingPhase: tui.state.appState.streamingPhase,
@@ -515168,6 +515207,8 @@ var BlunTUI = class {
515168
515207
  runningUpdatePreparedVersion;
515169
515208
  runningUpdatePreparedMode;
515170
515209
  runningUpdateHandoffStarted = false;
515210
+ runningUpdateNotBeforeMs = Date.now() + AUTO_UPDATE_SETTLE_MS;
515211
+ runningUpdateDelayTimer;
515171
515212
  runningUpdateMessageDispose;
515172
515213
  startupPhaseMs = {};
515173
515214
  lastActivityMode;
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "blun-king-cli",
3
- "version": "9.1.206",
3
+ "version": "9.1.208",
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": {