blun-king-cli 9.1.60 → 9.1.61

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/LIESMICH.txt CHANGED
@@ -9,7 +9,7 @@ Installation
9
9
  ------------
10
10
  Die geprüfte Version exakt global installieren:
11
11
 
12
- npm install -g blun-king-cli@9.1.59
12
+ npm install -g blun-king-cli@9.1.61
13
13
 
14
14
  Start
15
15
  -----
package/README.md CHANGED
@@ -9,7 +9,7 @@ Voraussetzung ist Node.js 24.15 oder neuer. Die geprüfte Version wird exakt
9
9
  installiert:
10
10
 
11
11
  ```powershell
12
- npm install -g blun-king-cli@9.1.59
12
+ npm install -g blun-king-cli@9.1.61
13
13
  ```
14
14
 
15
15
  ## Reproduzierbares Staging und Packen
@@ -5,6 +5,7 @@ const path = require('node:path');
5
5
  const { pathToFileURL } = require('node:url');
6
6
 
7
7
  const { acquireSharedRuntimeLease } = require('./update-lease');
8
+ const { RUNTIME_READY_MESSAGE } = require('./running-update.cjs');
8
9
 
9
10
  const CORE_LOADED_MESSAGE = 'blun-core-bootstrap-loaded';
10
11
 
@@ -31,11 +32,7 @@ async function runCoreBootstrap() {
31
32
 
32
33
  if (process.connected) {
33
34
  try {
34
- process.send({ type: CORE_LOADED_MESSAGE }, () => {
35
- try {
36
- process.disconnect();
37
- } catch {}
38
- });
35
+ process.send({ type: CORE_LOADED_MESSAGE });
39
36
  } catch {}
40
37
  }
41
38
  }
@@ -49,4 +46,4 @@ if (require.main === module) {
49
46
  });
50
47
  }
51
48
 
52
- module.exports = { CORE_LOADED_MESSAGE, runCoreBootstrap };
49
+ module.exports = { CORE_LOADED_MESSAGE, RUNTIME_READY_MESSAGE, runCoreBootstrap };
@@ -21,11 +21,21 @@ const {
21
21
  seedStandardDesignSkill,
22
22
  seedStandardTools,
23
23
  } = require('./standard-tools-bootstrap');
24
- const { CORE_LOADED_MESSAGE } = require('./core-bootstrap');
24
+ const { CORE_LOADED_MESSAGE, RUNTIME_READY_MESSAGE } = require('./core-bootstrap');
25
25
  const { prepareManagedNodeRuntime } = require('./node-runtime');
26
26
  const { repairConfiguredNativeModules } = require('./native-module-repair');
27
27
  const { acquireSharedRuntimeLease, tryAcquireUpdateLease } = require('./update-lease');
28
- const { runExplicitUpdate, runUpdateNotice } = require('./update-notice');
28
+ const { compareSemver, runExplicitUpdate, runUpdateNotice } = require('./update-notice');
29
+ const {
30
+ RUNNING_UPDATE_HANDOFF_EXIT_CODE,
31
+ RUNNING_UPDATE_HANDOFF_MESSAGE,
32
+ RUNNING_UPDATE_PREPARED_MESSAGE,
33
+ activateRuntime,
34
+ handoffRuntime,
35
+ prepareRunningUpdate,
36
+ readActiveRuntime,
37
+ resumeArgsForHandoff,
38
+ } = require('./running-update.cjs');
29
39
  const {
30
40
  ensurePrivateDirectory,
31
41
  securePrivateFile,
@@ -42,6 +52,7 @@ const RAW_ARGS = process.argv.slice(2);
42
52
  const PROFILE = parseProfileLaunchArgs(RAW_ARGS, launcherModeFromArgv(process.argv));
43
53
  const ARGS = PROFILE.args;
44
54
  const CORE_LOAD_TIMEOUT_MS = 30_000;
55
+ const RUNTIME_READY_TIMEOUT_MS = 60_000;
45
56
 
46
57
  function normalizeWindowsPath(value) {
47
58
  return path.win32.normalize(value).replace(/\\+$/u, '').toLowerCase();
@@ -130,10 +141,11 @@ function shouldDetachProtectedCore(options = {}) {
130
141
  return platform !== 'win32' || stdinIsTTY !== true;
131
142
  }
132
143
 
133
- function spawnProtectedCore(args, env, cwd) {
144
+ function spawnProtectedCore(args, env, cwd, options = {}) {
145
+ const packageRoot = path.resolve(options.packageRoot || PKG);
134
146
  const child = spawn(
135
147
  process.execPath,
136
- [path.join(PKG, 'bin', 'core-bootstrap.js'), ...args],
148
+ [path.join(packageRoot, 'bin', 'core-bootstrap.js'), ...args],
137
149
  {
138
150
  cwd,
139
151
  detached: shouldDetachProtectedCore(),
@@ -143,8 +155,10 @@ function spawnProtectedCore(args, env, cwd) {
143
155
  },
144
156
  );
145
157
  let loadedSettled = false;
158
+ let readySettled = false;
146
159
  let completedSettled = false;
147
160
  let resolveLoaded;
161
+ let resolveReady;
148
162
  let resolveCompleted;
149
163
  const loadTimer = setTimeout(() => {
150
164
  try {
@@ -152,6 +166,7 @@ function spawnProtectedCore(args, env, cwd) {
152
166
  } catch {}
153
167
  }, CORE_LOAD_TIMEOUT_MS);
154
168
  const loaded = new Promise((resolve) => { resolveLoaded = resolve; });
169
+ const ready = new Promise((resolve) => { resolveReady = resolve; });
155
170
  const completed = new Promise((resolve) => { resolveCompleted = resolve; });
156
171
  const settleLoaded = (value) => {
157
172
  if (loadedSettled) return;
@@ -159,11 +174,17 @@ function spawnProtectedCore(args, env, cwd) {
159
174
  clearTimeout(loadTimer);
160
175
  resolveLoaded(value);
161
176
  };
177
+ const settleReady = (value) => {
178
+ if (readySettled) return;
179
+ readySettled = true;
180
+ resolveReady(value);
181
+ };
162
182
  const settleCompleted = (result) => {
163
183
  if (completedSettled) return;
164
184
  completedSettled = true;
165
185
  for (const [signal, handler] of signalHandlers) process.removeListener(signal, handler);
166
186
  settleLoaded(false);
187
+ settleReady(false);
167
188
  resolveCompleted(result);
168
189
  };
169
190
  const signalHandlers = new Map();
@@ -177,23 +198,93 @@ function spawnProtectedCore(args, env, cwd) {
177
198
  process.once(signal, handler);
178
199
  }
179
200
  child.on('message', (message) => {
180
- if (message?.type !== CORE_LOADED_MESSAGE) return;
181
- settleLoaded(true);
182
- try {
183
- child.disconnect();
184
- } catch {}
201
+ if (message?.type === CORE_LOADED_MESSAGE) settleLoaded(true);
202
+ if (message?.type === RUNTIME_READY_MESSAGE) settleReady(true);
203
+ options.onMessage?.(message, child);
185
204
  });
186
205
  child.once('error', (error) => settleCompleted({ error }));
187
206
  child.once('exit', (code, signal) => settleCompleted({ code, signal }));
188
- return { child, completed, loaded };
207
+ return { child, completed, loaded, packageRoot, ready };
208
+ }
209
+
210
+ async function waitForRuntimeReady(core, timeoutMs = RUNTIME_READY_TIMEOUT_MS) {
211
+ let timer;
212
+ const timedOut = new Promise((resolve) => {
213
+ timer = setTimeout(() => resolve(false), timeoutMs);
214
+ timer.unref?.();
215
+ });
216
+ try {
217
+ return await Promise.race([core.ready, timedOut]);
218
+ } finally {
219
+ clearTimeout(timer);
220
+ }
189
221
  }
190
222
 
191
- async function superviseProtectedCore(args, env, cwd, releaseNotice) {
192
- const core = spawnProtectedCore(args, env, cwd);
223
+ async function superviseProtectedCore(args, env, cwd, releaseNotice, options = {}) {
224
+ const packageRoot = path.resolve(options.packageRoot || PKG);
225
+ let preparedTarget;
226
+ let handoffSessionId;
227
+ let updateStarted = false;
228
+ const startPreparation = (child) => {
229
+ if (updateStarted || options.runningUpdate === false) return;
230
+ updateStarted = true;
231
+ const sharedHome = env.BLUN_SHARED_HOME;
232
+ if (typeof sharedHome !== 'string' || sharedHome.length === 0) return;
233
+ Promise.resolve((options.prepareRunningUpdate || prepareRunningUpdate)({
234
+ currentVersion: JSON.parse(fs.readFileSync(path.join(packageRoot, 'package.json'), 'utf8')).version,
235
+ sharedHome,
236
+ env,
237
+ })).then((target) => {
238
+ if (target === null || child.connected !== true) return;
239
+ preparedTarget = target;
240
+ child.send({ type: RUNNING_UPDATE_PREPARED_MESSAGE, version: target.version });
241
+ }).catch((error) => {
242
+ options.onRunningUpdateError?.(error);
243
+ });
244
+ };
245
+ const core = spawnProtectedCore(args, env, cwd, {
246
+ packageRoot,
247
+ onMessage(message, child) {
248
+ if (message?.type === RUNTIME_READY_MESSAGE) startPreparation(child);
249
+ if (message?.type === RUNNING_UPDATE_HANDOFF_MESSAGE
250
+ && typeof message.sessionId === 'string'
251
+ && message.sessionId.length > 0) {
252
+ handoffSessionId = message.sessionId;
253
+ }
254
+ },
255
+ });
193
256
  const loaded = await core.loaded;
194
257
  await releaseNotice();
195
258
  const result = await core.completed;
196
259
  if (result.error) throw result.error;
260
+ if (result.code === RUNNING_UPDATE_HANDOFF_EXIT_CODE
261
+ && preparedTarget !== undefined
262
+ && handoffSessionId !== undefined) {
263
+ const handoff = await handoffRuntime({
264
+ target: preparedTarget,
265
+ previous: { packageRoot, version: readPackageVersion() },
266
+ sessionId: handoffSessionId,
267
+ args,
268
+ probeTarget: options.probeRunningUpdate || (() => true),
269
+ stopOld: async () => {},
270
+ startCore: async ({ packageRoot: nextRoot, args: nextArgs }) => {
271
+ const nextCore = spawnProtectedCore(nextArgs, env, cwd, { packageRoot: nextRoot });
272
+ const nextLoaded = await nextCore.loaded;
273
+ const ready = nextLoaded && await waitForRuntimeReady(nextCore, options.readyTimeoutMs);
274
+ if (!ready) {
275
+ try { nextCore.child.kill(); } catch {}
276
+ await nextCore.completed;
277
+ }
278
+ return { ...nextCore, ready };
279
+ },
280
+ activateTarget: (target) => (options.activateRuntime || activateRuntime)(env.BLUN_SHARED_HOME, target),
281
+ });
282
+ const resumed = handoff.core;
283
+ if (resumed === undefined || resumed.ready !== true) return 1;
284
+ const resumedResult = await resumed.completed;
285
+ if (resumedResult.error) throw resumedResult.error;
286
+ return exitCodeForChild(resumedResult, true);
287
+ }
197
288
  return exitCodeForChild(result, loaded);
198
289
  }
199
290
 
@@ -246,8 +337,40 @@ function spawnManagedLauncher(binary, cwd) {
246
337
  });
247
338
  }
248
339
 
340
+ function spawnActiveLauncher(packageRoot, cwd) {
341
+ const entryName = path.basename(process.argv[1] || 'king.js').toLowerCase() === 'blun.js'
342
+ ? 'blun.js'
343
+ : 'king.js';
344
+ return new Promise((resolve, reject) => {
345
+ const child = spawn(process.execPath, [path.join(packageRoot, 'bin', entryName), ...RAW_ARGS], {
346
+ cwd,
347
+ env: { ...process.env, BLUN_ACTIVE_RUNTIME_ROOT: packageRoot },
348
+ stdio: 'inherit',
349
+ windowsHide: true,
350
+ });
351
+ child.once('error', reject);
352
+ child.once('exit', (code, signal) => {
353
+ if (Number.isInteger(code)) resolve(code);
354
+ else if (signal === 'SIGINT') resolve(130);
355
+ else if (signal === 'SIGTERM') resolve(143);
356
+ else resolve(1);
357
+ });
358
+ });
359
+ }
360
+
249
361
  async function runLauncher(options = {}) {
250
362
  const callerCwd = process.cwd();
363
+ if (options.skipActiveRuntime !== true) {
364
+ const activeHome = resolveLauncherPrivatePaths().sharedHome;
365
+ const activeRuntime = readActiveRuntime(activeHome);
366
+ const currentVersion = readPackageVersion();
367
+ if (activeRuntime !== null
368
+ && path.resolve(activeRuntime.packageRoot) !== PKG
369
+ && compareSemver(currentVersion, activeRuntime.version) === -1) {
370
+ process.exitCode = await spawnActiveLauncher(activeRuntime.packageRoot, callerCwd);
371
+ return;
372
+ }
373
+ }
251
374
  const mode = options.mode || launcherModeFromArgv(process.argv);
252
375
  const explicitUpdateRequest = isNpmPackageUpdateRequest(ARGS);
253
376
  const explicitToolsRequest = String(ARGS[0] || '').toLowerCase() === 'tools';
@@ -26,6 +26,8 @@ const LEGACY_PROFILE_ENTRIES = Object.freeze([
26
26
  'tui.toml',
27
27
  ]);
28
28
  const MIGRATION_MARKER = '.legacy-profile-migrated-v1';
29
+ const SESSION_PATH_MIGRATION_MARKER = '.legacy-profile-session-paths-v2';
30
+ const SESSION_PATH_BACKUP_DIR = '.legacy-profile-session-paths-v2-backup';
29
31
 
30
32
  function normalizeProfileName(value) {
31
33
  const normalized = String(value || '').trim().toLowerCase();
@@ -102,27 +104,161 @@ function copyLegacyEntry(source, target, fsImpl) {
102
104
  if (stat.isFile()) fsImpl.copyFileSync(source, target);
103
105
  }
104
106
 
105
- function seedDefaultProfileFromLegacy(sharedHome, fsImpl = fs) {
106
- const paths = resolveProfilePaths(sharedHome, DEFAULT_PROFILE);
107
- const marker = path.join(paths.home, MIGRATION_MARKER);
108
- if (fsImpl.existsSync(marker)) return paths;
107
+ function relativePathInside(parent, child) {
108
+ const relative = path.relative(path.resolve(parent), path.resolve(child));
109
+ if (!relative || relative === '..' || relative.startsWith(`..${path.sep}`) || path.isAbsolute(relative)) {
110
+ return null;
111
+ }
112
+ return relative;
113
+ }
109
114
 
110
- fsImpl.mkdirSync(paths.home, { recursive: true });
111
- for (const entry of LEGACY_PROFILE_ENTRIES) {
112
- copyLegacyEntry(path.join(sharedHome, entry), path.join(paths.home, entry), fsImpl);
115
+ function replaceTextFileIfUnchanged(target, original, replacement, fsImpl) {
116
+ if (fsImpl.readFileSync(target, 'utf8') !== original) {
117
+ throw new Error(`Sitzungsdatei wurde waehrend der Migration geaendert: ${target}`);
113
118
  }
119
+ const temporary = `${target}.migration-${process.pid}-${Date.now()}.tmp`;
120
+ fsImpl.writeFileSync(temporary, replacement, 'utf8');
114
121
  try {
115
- fsImpl.writeFileSync(marker, 'legacy profile state copied once\n', { encoding: 'utf8', flag: 'wx' });
122
+ fsImpl.renameSync(temporary, target);
116
123
  } catch (error) {
117
- if (error?.code !== 'EEXIST') throw error;
124
+ try {
125
+ fsImpl.rmSync(temporary, { force: true });
126
+ } catch {}
127
+ throw error;
118
128
  }
129
+ }
130
+
131
+ function backupFile(source, target, fsImpl) {
132
+ if (fsImpl.existsSync(target)) return;
133
+ fsImpl.mkdirSync(path.dirname(target), { recursive: true });
134
+ fsImpl.copyFileSync(source, target);
135
+ }
136
+
137
+ function rewriteSessionState(statePath, legacySessionDir, profileSessionDir, backupRoot, relativeSessionDir, fsImpl) {
138
+ if (!fsImpl.existsSync(statePath)) return false;
139
+ const original = fsImpl.readFileSync(statePath, 'utf8');
140
+ let state;
141
+ try {
142
+ state = JSON.parse(original);
143
+ } catch {
144
+ return false;
145
+ }
146
+ if (!state || typeof state !== 'object' || Array.isArray(state) || !state.agents || typeof state.agents !== 'object') {
147
+ return false;
148
+ }
149
+
150
+ let changed = false;
151
+ const agents = {};
152
+ for (const [agentId, value] of Object.entries(state.agents)) {
153
+ if (!value || typeof value !== 'object' || Array.isArray(value) || typeof value.homedir !== 'string') {
154
+ agents[agentId] = value;
155
+ continue;
156
+ }
157
+ const relativeAgentPath = relativePathInside(legacySessionDir, value.homedir);
158
+ if (relativeAgentPath === null) {
159
+ agents[agentId] = value;
160
+ continue;
161
+ }
162
+ agents[agentId] = {
163
+ ...value,
164
+ homedir: path.join(profileSessionDir, relativeAgentPath),
165
+ };
166
+ changed = true;
167
+ }
168
+ if (!changed) return false;
169
+
170
+ backupFile(statePath, path.join(backupRoot, 'sessions', relativeSessionDir, 'state.json'), fsImpl);
171
+ const replacement = `${JSON.stringify({ ...state, agents }, null, 2)}\n`;
172
+ replaceTextFileIfUnchanged(statePath, original, replacement, fsImpl);
173
+ return true;
174
+ }
175
+
176
+ function repairDefaultProfileSessionPaths(sharedHome, fsImpl = fs, options = {}) {
177
+ const paths = resolveProfilePaths(sharedHome, DEFAULT_PROFILE);
178
+ const marker = path.join(paths.home, SESSION_PATH_MIGRATION_MARKER);
179
+ const filteredWorkDir = options.workDir === undefined ? null : path.resolve(options.workDir);
180
+ if (filteredWorkDir === null && fsImpl.existsSync(marker)) return paths;
181
+ if (!fsImpl.existsSync(paths.sessionIndex)) {
182
+ if (filteredWorkDir === null) fsImpl.writeFileSync(marker, 'legacy session paths repaired once\n', 'utf8');
183
+ return paths;
184
+ }
185
+
186
+ const legacySessions = path.join(sharedHome, 'sessions');
187
+ const backupRoot = path.join(paths.home, SESSION_PATH_BACKUP_DIR);
188
+ const original = fsImpl.readFileSync(paths.sessionIndex, 'utf8');
189
+ const output = [];
190
+ let changed = false;
191
+ for (const line of original.split(/\r?\n/u)) {
192
+ if (line.trim() === '') {
193
+ output.push(line);
194
+ continue;
195
+ }
196
+ let entry;
197
+ try {
198
+ entry = JSON.parse(line);
199
+ } catch {
200
+ output.push(line);
201
+ continue;
202
+ }
203
+ if (!entry || typeof entry !== 'object' || typeof entry.sessionId !== 'string'
204
+ || typeof entry.sessionDir !== 'string' || typeof entry.workDir !== 'string'
205
+ || (filteredWorkDir !== null && path.resolve(entry.workDir) !== filteredWorkDir)) {
206
+ output.push(line);
207
+ continue;
208
+ }
209
+ const relativeSessionDir = relativePathInside(legacySessions, entry.sessionDir);
210
+ if (relativeSessionDir === null || path.basename(relativeSessionDir) !== entry.sessionId) {
211
+ output.push(line);
212
+ continue;
213
+ }
214
+ const profileSessionDir = path.join(paths.sessions, relativeSessionDir);
215
+ if (!fsImpl.existsSync(profileSessionDir)) {
216
+ output.push(line);
217
+ continue;
218
+ }
219
+
220
+ rewriteSessionState(
221
+ path.join(profileSessionDir, 'state.json'),
222
+ path.resolve(entry.sessionDir),
223
+ profileSessionDir,
224
+ backupRoot,
225
+ relativeSessionDir,
226
+ fsImpl,
227
+ );
228
+ output.push(JSON.stringify({ ...entry, sessionDir: profileSessionDir }));
229
+ changed = true;
230
+ }
231
+
232
+ if (changed) {
233
+ backupFile(paths.sessionIndex, path.join(backupRoot, 'session_index.jsonl'), fsImpl);
234
+ replaceTextFileIfUnchanged(paths.sessionIndex, original, output.join('\n'), fsImpl);
235
+ }
236
+ if (filteredWorkDir === null) fsImpl.writeFileSync(marker, 'legacy session paths repaired once\n', 'utf8');
119
237
  return paths;
120
238
  }
121
239
 
240
+ function seedDefaultProfileFromLegacy(sharedHome, fsImpl = fs) {
241
+ const paths = resolveProfilePaths(sharedHome, DEFAULT_PROFILE);
242
+ const marker = path.join(paths.home, MIGRATION_MARKER);
243
+ fsImpl.mkdirSync(paths.home, { recursive: true });
244
+ if (!fsImpl.existsSync(marker)) {
245
+ for (const entry of LEGACY_PROFILE_ENTRIES) {
246
+ copyLegacyEntry(path.join(sharedHome, entry), path.join(paths.home, entry), fsImpl);
247
+ }
248
+ try {
249
+ fsImpl.writeFileSync(marker, 'legacy profile state copied once\n', { encoding: 'utf8', flag: 'wx' });
250
+ } catch (error) {
251
+ if (error?.code !== 'EEXIST') throw error;
252
+ }
253
+ }
254
+ return repairDefaultProfileSessionPaths(sharedHome, fsImpl);
255
+ }
256
+
122
257
  module.exports = {
123
258
  DEFAULT_PROFILE,
124
259
  normalizeProfileName,
125
260
  parseProfileLaunchArgs,
261
+ repairDefaultProfileSessionPaths,
126
262
  resolveProfilePaths,
127
263
  seedDefaultProfileFromLegacy,
128
264
  };
@@ -0,0 +1,389 @@
1
+ 'use strict';
2
+
3
+ const { createHash, randomUUID } = require('node:crypto');
4
+ const fs = require('node:fs');
5
+ const https = require('node:https');
6
+ const path = require('node:path');
7
+ const { spawn } = require('node:child_process');
8
+
9
+ const { compareSemver, requestTrustedJson } = require('./update-notice');
10
+ const { ensurePrivateDirectory, securePrivateFile, writePrivateFile } = require('./private-paths');
11
+
12
+ const PACKAGE_NAME = 'blun-king-cli';
13
+ const MANIFEST_URL = 'https://chat.blun.ai/blun-code-version.json';
14
+ const REGISTRY_URL = 'https://registry.npmjs.org/blun-king-cli';
15
+ const ACTIVE_RUNTIME_FILE = 'active-runtime.json';
16
+ const RUNNING_UPDATE_HANDOFF_EXIT_CODE = 76;
17
+ const RUNNING_UPDATE_PREPARED_MESSAGE = 'blun-running-update-prepared';
18
+ const RUNNING_UPDATE_HANDOFF_MESSAGE = 'blun-running-update-handoff';
19
+ const RUNTIME_READY_MESSAGE = 'blun-runtime-session-ready';
20
+ const MAX_TARBALL_BYTES = 128 * 1024 * 1024;
21
+
22
+ function ownData(value, key) {
23
+ if (typeof value !== 'object' || value === null || Array.isArray(value)) return undefined;
24
+ const descriptor = Object.getOwnPropertyDescriptor(value, key);
25
+ return descriptor && Object.hasOwn(descriptor, 'value') ? descriptor.value : undefined;
26
+ }
27
+
28
+ function validIntegrity(value) {
29
+ if (typeof value !== 'string') return false;
30
+ const match = /^sha512-([A-Za-z0-9+/]+={0,2})$/u.exec(value);
31
+ if (!match) return false;
32
+ try {
33
+ return Buffer.from(match[1], 'base64').length === 64;
34
+ } catch {
35
+ return false;
36
+ }
37
+ }
38
+
39
+ function selectRunningUpdateTarget(currentVersion, manifest) {
40
+ const name = ownData(manifest, 'name');
41
+ const version = ownData(manifest, 'latest');
42
+ const integrity = ownData(manifest, 'integrity');
43
+ const releasedAt = ownData(manifest, 'releasedAt');
44
+ if (name !== PACKAGE_NAME
45
+ || typeof version !== 'string'
46
+ || compareSemver(currentVersion, version) !== -1
47
+ || !validIntegrity(integrity)
48
+ || typeof releasedAt !== 'string'
49
+ || !Number.isFinite(Date.parse(releasedAt))) {
50
+ return null;
51
+ }
52
+ return Object.freeze({ version, integrity });
53
+ }
54
+
55
+ function isSafeRuntimeBoundary(state) {
56
+ return state?.isShuttingDown === false
57
+ && state.streamingPhase === 'idle'
58
+ && state.isCompacting === false
59
+ && state.queuedMessages === 0
60
+ && state.activeToolCalls === 0
61
+ && state.shellCommands === 0
62
+ && state.queueCommandRunning === false;
63
+ }
64
+
65
+ function resumeArgsForHandoff(args, sessionId) {
66
+ const filtered = [];
67
+ for (let index = 0; index < args.length; index += 1) {
68
+ const arg = args[index];
69
+ if (arg === '-c' || arg === '--continue') continue;
70
+ if (arg === '-r' || arg === '--resume' || arg === '--session') {
71
+ index += 1;
72
+ continue;
73
+ }
74
+ if (/^(?:--resume|--session)=/u.test(arg)) continue;
75
+ filtered.push(arg);
76
+ }
77
+ return [...filtered, '--resume', sessionId];
78
+ }
79
+
80
+ async function handoffRuntime(options) {
81
+ const handoffArgs = resumeArgsForHandoff(options.args, options.sessionId);
82
+ if (!await options.probeTarget(options.target)) {
83
+ return { kind: 'probe-failed', runtime: options.previous };
84
+ }
85
+ await options.stopOld();
86
+ const targetCore = await options.startCore({
87
+ packageRoot: options.target.packageRoot,
88
+ args: handoffArgs,
89
+ });
90
+ if (targetCore.ready === true) {
91
+ await options.activateTarget(options.target);
92
+ return { kind: 'activated', runtime: options.target, core: targetCore };
93
+ }
94
+ const fallbackCore = await options.startCore({
95
+ packageRoot: options.previous.packageRoot,
96
+ args: handoffArgs,
97
+ });
98
+ return { kind: 'rolled-back', runtime: options.previous, core: fallbackCore };
99
+ }
100
+
101
+ function releasesRoot(sharedHome) {
102
+ return path.join(path.resolve(sharedHome), 'updates', 'releases');
103
+ }
104
+
105
+ function activeRuntimePath(sharedHome) {
106
+ return path.join(path.resolve(sharedHome), 'updates', ACTIVE_RUNTIME_FILE);
107
+ }
108
+
109
+ function pathWithin(root, candidate) {
110
+ const relative = path.relative(root, candidate);
111
+ return relative === '' || (relative !== '..'
112
+ && !relative.startsWith(`..${path.sep}`)
113
+ && !path.isAbsolute(relative));
114
+ }
115
+
116
+ function verifyRuntimePackage(packageRoot, version, options = {}) {
117
+ try {
118
+ const root = path.resolve(packageRoot);
119
+ const allowedRoot = path.resolve(options.allowedRoot || releasesRoot(options.sharedHome));
120
+ if (!pathWithin(allowedRoot, root)) return false;
121
+ const manifestPath = path.join(root, 'package.json');
122
+ const bundlePath = path.join(root, 'blun.mjs');
123
+ const bootstrapPath = path.join(root, 'bin', 'core-bootstrap.js');
124
+ for (const filePath of [manifestPath, bundlePath, bootstrapPath]) {
125
+ const stat = fs.lstatSync(filePath);
126
+ if (!stat.isFile() || stat.isSymbolicLink()) return false;
127
+ }
128
+ const manifest = JSON.parse(fs.readFileSync(manifestPath, 'utf8'));
129
+ return ownData(manifest, 'name') === PACKAGE_NAME && ownData(manifest, 'version') === version;
130
+ } catch {
131
+ return false;
132
+ }
133
+ }
134
+
135
+ function readActiveRuntime(sharedHome) {
136
+ try {
137
+ const filePath = activeRuntimePath(sharedHome);
138
+ const stat = fs.lstatSync(filePath);
139
+ if (!stat.isFile() || stat.isSymbolicLink() || stat.size > 16 * 1024) return null;
140
+ const record = JSON.parse(fs.readFileSync(filePath, 'utf8'));
141
+ const version = ownData(record, 'version');
142
+ const packageRoot = ownData(record, 'packageRoot');
143
+ const integrity = ownData(record, 'integrity');
144
+ if (typeof version !== 'string'
145
+ || typeof packageRoot !== 'string'
146
+ || !path.isAbsolute(packageRoot)
147
+ || !validIntegrity(integrity)
148
+ || !verifyRuntimePackage(packageRoot, version, { sharedHome })) {
149
+ return null;
150
+ }
151
+ return Object.freeze({ version, packageRoot: path.resolve(packageRoot), integrity });
152
+ } catch {
153
+ return null;
154
+ }
155
+ }
156
+
157
+ function activateRuntime(sharedHome, target) {
158
+ if (!verifyRuntimePackage(target.packageRoot, target.version, { sharedHome })
159
+ || !validIntegrity(target.integrity)) {
160
+ throw new Error('RUNNING_UPDATE_TARGET_INVALID');
161
+ }
162
+ const updateRoot = path.join(path.resolve(sharedHome), 'updates');
163
+ ensurePrivateDirectory(path.resolve(sharedHome));
164
+ ensurePrivateDirectory(updateRoot);
165
+ const filePath = activeRuntimePath(sharedHome);
166
+ const temporaryPath = path.join(updateRoot, `.${ACTIVE_RUNTIME_FILE}.${process.pid}.${randomUUID()}.tmp`);
167
+ try {
168
+ writePrivateFile(temporaryPath, `${JSON.stringify({
169
+ version: target.version,
170
+ packageRoot: path.resolve(target.packageRoot),
171
+ integrity: target.integrity,
172
+ activatedAt: new Date().toISOString(),
173
+ })}\n`);
174
+ fs.renameSync(temporaryPath, filePath);
175
+ securePrivateFile(filePath);
176
+ } catch (error) {
177
+ fs.rmSync(temporaryPath, { force: true });
178
+ throw error;
179
+ }
180
+ }
181
+
182
+ function registryRelease(registry, version) {
183
+ const versions = ownData(registry, 'versions');
184
+ const entry = ownData(versions, version);
185
+ const dist = ownData(entry, 'dist');
186
+ const integrity = ownData(dist, 'integrity');
187
+ const tarball = ownData(dist, 'tarball');
188
+ if (!validIntegrity(integrity) || typeof tarball !== 'string') return null;
189
+ let url;
190
+ try {
191
+ url = new URL(tarball);
192
+ } catch {
193
+ return null;
194
+ }
195
+ if (url.protocol !== 'https:'
196
+ || url.hostname !== 'registry.npmjs.org'
197
+ || url.username !== ''
198
+ || url.password !== '') {
199
+ return null;
200
+ }
201
+ return { integrity, tarball: url.href };
202
+ }
203
+
204
+ function downloadTarball(url, destination, options = {}) {
205
+ const httpsImpl = options.httpsImpl || https;
206
+ return new Promise((resolve, reject) => {
207
+ const request = httpsImpl.get(url, {
208
+ headers: { 'user-agent': 'blun-king-running-update' },
209
+ timeout: options.timeoutMs || 30_000,
210
+ }, (response) => {
211
+ if (response.statusCode !== 200) {
212
+ response.resume();
213
+ reject(new Error(`RUNNING_UPDATE_DOWNLOAD_${String(response.statusCode)}`));
214
+ return;
215
+ }
216
+ let bytes = 0;
217
+ const handle = fs.openSync(destination, 'wx', 0o600);
218
+ let settled = false;
219
+ const fail = (error) => {
220
+ if (settled) return;
221
+ settled = true;
222
+ try { fs.closeSync(handle); } catch {}
223
+ fs.rmSync(destination, { force: true });
224
+ reject(error);
225
+ };
226
+ response.on('data', (chunk) => {
227
+ bytes += chunk.length;
228
+ if (bytes > MAX_TARBALL_BYTES) {
229
+ response.destroy(new Error('RUNNING_UPDATE_TARBALL_TOO_LARGE'));
230
+ return;
231
+ }
232
+ try {
233
+ fs.writeSync(handle, chunk);
234
+ } catch (error) {
235
+ response.destroy(error);
236
+ }
237
+ });
238
+ response.once('error', fail);
239
+ response.once('end', () => {
240
+ if (settled) return;
241
+ settled = true;
242
+ fs.fsyncSync(handle);
243
+ fs.closeSync(handle);
244
+ resolve();
245
+ });
246
+ });
247
+ request.once('timeout', () => request.destroy(new Error('RUNNING_UPDATE_DOWNLOAD_TIMEOUT')));
248
+ request.once('error', reject);
249
+ });
250
+ }
251
+
252
+ function resolveNpmCliPath(execPath = process.execPath) {
253
+ const executableDirectory = path.dirname(path.resolve(execPath));
254
+ const candidates = [
255
+ path.join(executableDirectory, 'node_modules', 'npm', 'bin', 'npm-cli.js'),
256
+ path.resolve(executableDirectory, '..', 'lib', 'node_modules', 'npm', 'bin', 'npm-cli.js'),
257
+ path.resolve(executableDirectory, '..', 'node_modules', 'npm', 'bin', 'npm-cli.js'),
258
+ path.resolve(__dirname, '..', '..', 'npm', 'bin', 'npm-cli.js'),
259
+ ];
260
+ for (const candidate of candidates) {
261
+ try {
262
+ const resolved = fs.realpathSync(candidate);
263
+ if (fs.statSync(resolved).isFile() && path.basename(resolved) === 'npm-cli.js') return resolved;
264
+ } catch {}
265
+ }
266
+ throw new Error('TRUSTED_NPM_CLI_NOT_FOUND');
267
+ }
268
+
269
+ function runProcess(command, args, options = {}) {
270
+ return new Promise((resolve, reject) => {
271
+ const child = (options.spawnImpl || spawn)(command, args, {
272
+ cwd: options.cwd,
273
+ env: options.env,
274
+ stdio: options.stdio || 'ignore',
275
+ windowsHide: true,
276
+ });
277
+ child.once('error', reject);
278
+ child.once('exit', (code, signal) => {
279
+ if (code === 0) resolve();
280
+ else reject(new Error(`RUNNING_UPDATE_PROCESS_FAILED:${code ?? signal ?? 'unknown'}`));
281
+ });
282
+ });
283
+ }
284
+
285
+ async function defaultInstallTarball(tarballPath, installPrefix, options = {}) {
286
+ const npmCliPath = resolveNpmCliPath(options.execPath);
287
+ const env = {
288
+ ...process.env,
289
+ TEMP: installPrefix,
290
+ TMP: installPrefix,
291
+ TMPDIR: installPrefix,
292
+ npm_config_registry: 'https://registry.npmjs.org/',
293
+ npm_config_strict_ssl: 'true',
294
+ };
295
+ await runProcess(options.execPath || process.execPath, [
296
+ npmCliPath,
297
+ 'install',
298
+ `--prefix=${installPrefix}`,
299
+ '--ignore-scripts=false',
300
+ '--package-lock=false',
301
+ '--audit=false',
302
+ '--fund=false',
303
+ tarballPath,
304
+ ], { cwd: installPrefix, env, spawnImpl: options.spawnImpl });
305
+ }
306
+
307
+ async function defaultProbeRuntime(packageRoot, options = {}) {
308
+ try {
309
+ await runProcess(options.execPath || process.execPath, ['--check', path.join(packageRoot, 'blun.mjs')], options);
310
+ await runProcess(options.execPath || process.execPath, ['--check', path.join(packageRoot, 'bin', 'core-bootstrap.js')], options);
311
+ return true;
312
+ } catch {
313
+ return false;
314
+ }
315
+ }
316
+
317
+ async function prepareRunningUpdate(options) {
318
+ if (String(options.env?.BLUN_NO_AUTO_UPDATE || '') === '1') return null;
319
+ const manifest = await (options.loadManifest
320
+ ? options.loadManifest()
321
+ : requestTrustedJson(MANIFEST_URL));
322
+ const target = selectRunningUpdateTarget(options.currentVersion, manifest);
323
+ if (target === null) return null;
324
+ const registry = await (options.loadRegistry
325
+ ? options.loadRegistry()
326
+ : requestTrustedJson(REGISTRY_URL));
327
+ const release = registryRelease(registry, target.version);
328
+ if (release === null || release.integrity !== target.integrity) {
329
+ throw new Error('RUNNING_UPDATE_INTEGRITY_SOURCE_MISMATCH');
330
+ }
331
+ const digest = createHash('sha256').update(target.integrity).digest('hex').slice(0, 16);
332
+ const releaseDirectory = path.join(releasesRoot(options.sharedHome), target.version, digest);
333
+ const packageRoot = path.join(releaseDirectory, 'node_modules', PACKAGE_NAME);
334
+ if (verifyRuntimePackage(packageRoot, target.version, { sharedHome: options.sharedHome })) {
335
+ return Object.freeze({ ...target, packageRoot });
336
+ }
337
+
338
+ const updateRoot = path.join(path.resolve(options.sharedHome), 'updates');
339
+ const stagingRoot = path.join(updateRoot, 'staging');
340
+ ensurePrivateDirectory(path.resolve(options.sharedHome));
341
+ ensurePrivateDirectory(updateRoot);
342
+ ensurePrivateDirectory(stagingRoot);
343
+ const temporaryRoot = fs.mkdtempSync(path.join(stagingRoot, 'runtime-'));
344
+ ensurePrivateDirectory(temporaryRoot);
345
+ const tarballPath = path.join(temporaryRoot, `${PACKAGE_NAME}.tgz`);
346
+ const installPrefix = path.join(temporaryRoot, 'install');
347
+ ensurePrivateDirectory(installPrefix);
348
+ try {
349
+ await (options.downloadTarball || downloadTarball)(release.tarball, tarballPath);
350
+ const actualIntegrity = `sha512-${createHash('sha512').update(fs.readFileSync(tarballPath)).digest('base64')}`;
351
+ if (actualIntegrity !== target.integrity) throw new Error('RUNNING_UPDATE_TARBALL_INTEGRITY_FAILED');
352
+ await (options.installTarball || defaultInstallTarball)(tarballPath, installPrefix, options);
353
+ const installedRoot = path.join(installPrefix, 'node_modules', PACKAGE_NAME);
354
+ if (!verifyRuntimePackage(installedRoot, target.version, { allowedRoot: installPrefix })) {
355
+ throw new Error('RUNNING_UPDATE_PACKAGE_INVALID');
356
+ }
357
+ if (!await (options.probeRuntime || defaultProbeRuntime)(installedRoot, options)) {
358
+ throw new Error('RUNNING_UPDATE_PROBE_FAILED');
359
+ }
360
+ ensurePrivateDirectory(releasesRoot(options.sharedHome));
361
+ ensurePrivateDirectory(path.dirname(releaseDirectory));
362
+ if (fs.existsSync(releaseDirectory)) fs.rmSync(releaseDirectory, { recursive: true, force: true });
363
+ fs.renameSync(installPrefix, releaseDirectory);
364
+ if (!verifyRuntimePackage(packageRoot, target.version, { sharedHome: options.sharedHome })) {
365
+ throw new Error('RUNNING_UPDATE_ACTIVATION_PACKAGE_INVALID');
366
+ }
367
+ return Object.freeze({ ...target, packageRoot });
368
+ } finally {
369
+ fs.rmSync(temporaryRoot, { recursive: true, force: true });
370
+ }
371
+ }
372
+
373
+ module.exports = {
374
+ ACTIVE_RUNTIME_FILE,
375
+ RUNNING_UPDATE_HANDOFF_EXIT_CODE,
376
+ RUNNING_UPDATE_HANDOFF_MESSAGE,
377
+ RUNNING_UPDATE_PREPARED_MESSAGE,
378
+ RUNTIME_READY_MESSAGE,
379
+ activateRuntime,
380
+ activeRuntimePath,
381
+ defaultProbeRuntime,
382
+ handoffRuntime,
383
+ isSafeRuntimeBoundary,
384
+ prepareRunningUpdate,
385
+ readActiveRuntime,
386
+ resumeArgsForHandoff,
387
+ selectRunningUpdateTarget,
388
+ verifyRuntimePackage,
389
+ };
@@ -54,8 +54,18 @@ const LEGACY_MANIFEST_KEYS = Object.freeze([
54
54
  'notesUrl',
55
55
  'releasedAt',
56
56
  ]);
57
+ const RELEASE_NOTES_MANIFEST_KEYS = Object.freeze([
58
+ 'install',
59
+ 'latest',
60
+ 'minSupported',
61
+ 'name',
62
+ 'notesUrl',
63
+ 'releaseNotes',
64
+ 'releasedAt',
65
+ ]);
57
66
  const MANIFEST_KEYS = Object.freeze([
58
67
  'install',
68
+ 'integrity',
59
69
  'latest',
60
70
  'minSupported',
61
71
  'name',
@@ -214,8 +224,20 @@ function parseReleaseNotes(value) {
214
224
  return Object.freeze(notes);
215
225
  }
216
226
 
227
+ function validIntegrity(value) {
228
+ if (typeof value !== 'string') return false;
229
+ const match = /^sha512-([A-Za-z0-9+/]+={0,2})$/u.exec(value);
230
+ if (!match) return false;
231
+ try {
232
+ return Buffer.from(match[1], 'base64').length === 64;
233
+ } catch {
234
+ return false;
235
+ }
236
+ }
237
+
217
238
  function parseFallbackManifest(value) {
218
239
  const record = exactDataRecord(value, MANIFEST_KEYS)
240
+ || exactDataRecord(value, RELEASE_NOTES_MANIFEST_KEYS)
219
241
  || exactDataRecord(value, LEGACY_MANIFEST_KEYS);
220
242
  const notesUrl = record ? canonicalOfficialNotesUrl(record.notesUrl) : undefined;
221
243
  const releaseNotes = record && Object.hasOwn(record, 'releaseNotes')
@@ -228,6 +250,7 @@ function parseFallbackManifest(value) {
228
250
  || compareSemver(record.minSupported, record.latest) === 1
229
251
  || !isIsoTimestamp(record.releasedAt)
230
252
  || !notesUrl
253
+ || (Object.hasOwn(record, 'integrity') && !validIntegrity(record.integrity))
231
254
  || (Object.hasOwn(record, 'releaseNotes') && releaseNotes === undefined)
232
255
  || !isBoundedText(record.install, 256)) {
233
256
  return undefined;
@@ -238,6 +261,7 @@ function parseFallbackManifest(value) {
238
261
  minSupported: record.minSupported,
239
262
  releasedAt: record.releasedAt,
240
263
  notesUrl,
264
+ ...(Object.hasOwn(record, 'integrity') ? { integrity: record.integrity } : {}),
241
265
  ...(releaseNotes === undefined ? {} : { releaseNotes }),
242
266
  install: record.install,
243
267
  });
package/blun.mjs CHANGED
@@ -503099,8 +503099,8 @@ registerUiCatalogFragment({
503099
503099
  * 0 = most recent clipped lines visible
503100
503100
  * N = N lines up from the bottom
503101
503101
  */
503102
- /** Maximum lines retained before the oldest are evicted. */
503103
- const MAX_BUFFER_LINES = 1e4;
503102
+ /** Retain the complete rendered session unless the process itself ends. */
503103
+ const MAX_BUFFER_LINES = Number.POSITIVE_INFINITY;
503104
503104
  var ScrollbackBuffer = class {
503105
503105
  lines = [];
503106
503106
  scrollOffset = 0;
@@ -505264,6 +505264,7 @@ var SessionEventHandler = class {
505264
505264
  this.goalCompletionTurnEnded = true;
505265
505265
  this.scheduleQueuedGoalPromotion();
505266
505266
  if (event.reason === "completed") this.host.refreshManagedQuotaWindows();
505267
+ requestRunningUpdateAtSafeBoundary(this.host);
505267
505268
  }
505268
505269
  handleStepBegin(event) {
505269
505270
  this.host.commitQueuedSteerAtStepStart(event.turnId);
@@ -505311,12 +505312,13 @@ var SessionEventHandler = class {
505311
505312
  const duration = durationMs === void 0 ? "—" : formatResponseDuration(durationMs);
505312
505313
  const tokens = this.currentTurnTokenCount === void 0 ? "—" : formatTokenCount(this.currentTurnTokenCount);
505313
505314
  const tokenKey = this.currentTurnTokenCount === 1 ? "thinking.tokens.one" : "thinking.tokens.other";
505315
+ const startedAt = formatLiveStartedAt(this.currentTurnStartedAtMs ?? Date.now());
505314
505316
  this.host.appendTranscriptEntry({
505315
505317
  id: nextTranscriptId(),
505316
505318
  kind: "status",
505317
505319
  turnId: String(event.turnId),
505318
505320
  renderMode: "plain",
505319
- content: `${duration} · ${uiText(tokenKey, { count: tokens })}`
505321
+ content: `${duration} · ${uiText(tokenKey, { count: tokens })} · ${startedAt}`
505320
505322
  });
505321
505323
  }
505322
505324
  markActiveAgentSwarmsCancelled() {
@@ -506297,12 +506299,6 @@ function createReplayRenderContext() {
506297
506299
  suppressNextPlanModeOffNotice: false
506298
506300
  };
506299
506301
  }
506300
- function limitReplayRecordsByTurn(records, maxTurns) {
506301
- if (maxTurns <= 0) return [];
506302
- const turnStarts = records.flatMap((record, index) => isReplayUserTurnRecord(record) ? [index] : []);
506303
- if (turnStarts.length <= maxTurns) return records;
506304
- return records.slice(turnStarts[turnStarts.length - maxTurns]);
506305
- }
506306
506302
  function replayEntry(context, kind, content, renderMode, extras = {}) {
506307
506303
  return {
506308
506304
  id: nextTranscriptId(),
@@ -506630,7 +506626,7 @@ var SessionReplayRenderer = class {
506630
506626
  }
506631
506627
  renderRecords(agent) {
506632
506628
  const context = createReplayRenderContext();
506633
- for (const record of limitReplayRecordsByTurn(agent.replay, 10)) this.renderRecord(context, record);
506629
+ for (const record of agent.replay) this.renderRecord(context, record);
506634
506630
  this.flushAssistant(context);
506635
506631
  this.cleanupRuntime(context);
506636
506632
  }
@@ -508034,7 +508030,7 @@ var FooterComponent = class {
508034
508030
  const persona = personaName();
508035
508031
  const profileLabel = process.env["BLUN_PROFILE"] ?? "default";
508036
508032
  const profile = chalk.hex(colors.textDim)(profileLabel) + sep;
508037
- const brand = chalk.hex(colors.primary).bold("● BLUN") + sep + profile + (persona !== void 0 ? chalk.hex(colors.text)(persona) + sep : "");
508033
+ const brand = chalk.hex(colors.primary).bold("● BL") + chalk.hex(colors.text)("UN") + sep + profile + (persona !== void 0 ? chalk.hex(colors.text)(persona) + sep : "");
508038
508034
  const modelLabel = model;
508039
508035
  const modeLabel = uiText(state.permissionMode === "yolo" ? "footer.mode.god" : state.permissionMode === "auto" ? "footer.mode.auto" : "footer.mode.manual");
508040
508036
  const modeSuffix = sep + chalk.hex(colors.textDim)(uiText("footer.mode", { mode: modeLabel }));
@@ -511396,12 +511392,18 @@ var CustomEditor = class extends Editor {
511396
511392
  if (isKeyRelease(normalized)) return;
511397
511393
  if (!matchesKey(normalized, Key.escape)) this.onNonEscapeInput?.();
511398
511394
  if (this.consumingPaste) {
511399
- this.consumeBuffer += normalized;
511400
- if (this.consumeBuffer.includes(BRACKET_PASTE_END)) {
511395
+ const cancelPaste = matchesKey(normalized, Key.ctrl("c")) || matchesKey(normalized, Key.escape);
511396
+ if (cancelPaste) {
511401
511397
  this.consumingPaste = false;
511402
511398
  this.consumeBuffer = "";
511399
+ } else {
511400
+ this.consumeBuffer += normalized;
511401
+ if (this.consumeBuffer.includes(BRACKET_PASTE_END)) {
511402
+ this.consumingPaste = false;
511403
+ this.consumeBuffer = "";
511404
+ }
511405
+ return;
511403
511406
  }
511404
- return;
511405
511407
  }
511406
511408
  if (normalized.includes(BRACKET_PASTE_START) && this.expandPasteMarkerAtCursor()) {
511407
511409
  if (!normalized.includes(BRACKET_PASTE_END)) this.consumingPaste = true;
@@ -511456,6 +511458,7 @@ var CustomEditor = class extends Editor {
511456
511458
  if (this.inputMode === "bash" && this.getText().length === 0 && (matchesKey(normalized, Key.escape) || matchesKey(normalized, Key.backspace))) {
511457
511459
  this.inputMode = "prompt";
511458
511460
  this.onInputModeChange?.("prompt");
511461
+ if (matchesKey(normalized, Key.escape)) this.onEscape?.(true);
511459
511462
  return;
511460
511463
  }
511461
511464
  if (matchesKey(normalized, Key.up)) {
@@ -511742,6 +511745,7 @@ var GhostSuggestEditor = class extends CustomEditor {
511742
511745
  if (matchesKey(normalized, Key.escape)) {
511743
511746
  this.ghostSuggestion = null;
511744
511747
  this.onGhostDismissed?.();
511748
+ this.onEscape?.(true);
511745
511749
  return;
511746
511750
  }
511747
511751
  if (this.isCursorAtTextEnd() && !this.isShowingAutocomplete()) {
@@ -513049,13 +513053,13 @@ function readEnvInt(name, fallback) {
513049
513053
  return value;
513050
513054
  }
513051
513055
  /** Keep the most recent N turns. `0` disables trimming. */
513052
- const TRANSCRIPT_MAX_TURNS = readEnvInt("BLUN_TUI_MAX_TURNS", 15);
513056
+ const TRANSCRIPT_MAX_TURNS = readEnvInt("BLUN_TUI_MAX_TURNS", 0);
513053
513057
  /** Only the most recent E turns are allowed to expand (Ctrl+O). `0` disables expanding. */
513054
513058
  const TRANSCRIPT_EXPAND_TURNS = readEnvInt("BLUN_TUI_EXPAND_TURNS", 3);
513055
513059
  /** Only trim once the window exceeds maxTurns by this much (avoids churn). */
513056
513060
  const TRANSCRIPT_HYSTERESIS = readEnvInt("BLUN_TUI_HYSTERESIS", 5);
513057
513061
  /** Keep this many recent steps untouched inside a turn; older steps are merged into a summary. `0` disables merging. */
513058
- const TRANSCRIPT_KEEP_RECENT_STEPS = readEnvInt("BLUN_TUI_KEEP_RECENT_STEPS", 30);
513062
+ const TRANSCRIPT_KEEP_RECENT_STEPS = readEnvInt("BLUN_TUI_KEEP_RECENT_STEPS", 0);
513059
513063
  /**
513060
513064
  * Group consecutive entries into turns by `turnId`. Entries with the same
513061
513065
  * non-undefined `turnId` that are adjacent belong to the same turn.
@@ -513112,6 +513116,60 @@ function turnsToTrim(turns, maxTurns, hysteresis) {
513112
513116
  }
513113
513117
  //#endregion
513114
513118
  //#region src/tui/blun-tui.ts
513119
+ const {
513120
+ RUNNING_UPDATE_HANDOFF_EXIT_CODE,
513121
+ RUNNING_UPDATE_HANDOFF_MESSAGE,
513122
+ RUNNING_UPDATE_PREPARED_MESSAGE,
513123
+ RUNTIME_READY_MESSAGE,
513124
+ isSafeRuntimeBoundary
513125
+ } = __require("./bin/running-update.cjs");
513126
+ function notifyRunningRuntimeReady(tui) {
513127
+ if (!process.connected) return;
513128
+ const sessionId = tui.getCurrentSessionId();
513129
+ if (sessionId.length === 0) return;
513130
+ try {
513131
+ process.send({
513132
+ type: RUNTIME_READY_MESSAGE,
513133
+ sessionId
513134
+ });
513135
+ } catch {}
513136
+ }
513137
+ function requestRunningUpdateAtSafeBoundary(tui) {
513138
+ if (tui.runningUpdatePreparedVersion === void 0 || tui.runningUpdateHandoffStarted || !process.connected) return false;
513139
+ if (!isSafeRuntimeBoundary({
513140
+ isShuttingDown: tui.isShuttingDown,
513141
+ streamingPhase: tui.state.appState.streamingPhase,
513142
+ isCompacting: tui.state.appState.isCompacting,
513143
+ queuedMessages: tui.state.queuedMessages.length,
513144
+ activeToolCalls: tui.streamingUI.hasActiveToolCalls() ? 1 : 0,
513145
+ shellCommands: tui.shellOutputStreams.size,
513146
+ queueCommandRunning: tui.queueCommandRunning
513147
+ })) return false;
513148
+ const sessionId = tui.getCurrentSessionId();
513149
+ if (sessionId.length === 0) return false;
513150
+ tui.runningUpdateHandoffStarted = true;
513151
+ try {
513152
+ process.send({
513153
+ type: RUNNING_UPDATE_HANDOFF_MESSAGE,
513154
+ sessionId,
513155
+ version: tui.runningUpdatePreparedVersion
513156
+ });
513157
+ } catch {
513158
+ tui.runningUpdateHandoffStarted = false;
513159
+ return false;
513160
+ }
513161
+ void tui.stop(RUNNING_UPDATE_HANDOFF_EXIT_CODE);
513162
+ return true;
513163
+ }
513164
+ function installRunningUpdateListener(tui) {
513165
+ const handler = (message) => {
513166
+ if (message?.type !== RUNNING_UPDATE_PREPARED_MESSAGE || typeof message.version !== "string") return;
513167
+ tui.runningUpdatePreparedVersion = message.version;
513168
+ requestRunningUpdateAtSafeBoundary(tui);
513169
+ };
513170
+ process.on("message", handler);
513171
+ return () => process.off("message", handler);
513172
+ }
513115
513173
  function loadingTipKind(mode) {
513116
513174
  if (mode === "waiting" || mode === "tool") return "blun";
513117
513175
  if (mode === "composing") return "composing";
@@ -513196,6 +513254,9 @@ var BlunTUI = class {
513196
513254
  startupLoginRequired = false;
513197
513255
  startupWorkspaceSelectionPending = false;
513198
513256
  startupGoalPromptedSessionId;
513257
+ runningUpdatePreparedVersion;
513258
+ runningUpdateHandoffStarted = false;
513259
+ runningUpdateMessageDispose;
513199
513260
  startupPhaseMs = {};
513200
513261
  lastActivityMode;
513201
513262
  currentLoadingTip = void 0;
@@ -513309,6 +513370,7 @@ var BlunTUI = class {
513309
513370
  this.editorKeyboard.install();
513310
513371
  this.scrollbackController = new ScrollbackController(this.state);
513311
513372
  this.scrollbackController.install();
513373
+ this.runningUpdateMessageDispose = installRunningUpdateListener(this);
513312
513374
  this.buildLayout();
513313
513375
  }
513314
513376
  getSlashCommands() {
@@ -513739,6 +513801,8 @@ var BlunTUI = class {
513739
513801
  this.managedQuotaWarningController.dispose();
513740
513802
  this.state.loopIndicator.dispose();
513741
513803
  this.state.footer.dispose();
513804
+ this.runningUpdateMessageDispose?.();
513805
+ this.runningUpdateMessageDispose = void 0;
513742
513806
  for (const dispose of this.reverseRpcDisposers) dispose();
513743
513807
  this.reverseRpcDisposers.length = 0;
513744
513808
  this.disposeTerminalTracking();
@@ -513962,6 +514026,7 @@ var BlunTUI = class {
513962
514026
  if (this.shellOutputStreams.size === 0) {
513963
514027
  this.setAppState({ streamingPhase: "idle" });
513964
514028
  this.drainOneQueuedMessage();
514029
+ requestRunningUpdateAtSafeBoundary(this);
513965
514030
  }
513966
514031
  }
513967
514032
  queueDrainTimer;
@@ -516409,22 +516474,27 @@ async function runShell(opts, version, updateStartupNotice) {
516409
516474
  tui.onExit = async (exitCode = 0) => {
516410
516475
  const sessionId = tui.getCurrentSessionId();
516411
516476
  const hasContent = tui.hasSessionContent();
516477
+ const runningUpdateHandoff = exitCode === RUNNING_UPDATE_HANDOFF_EXIT_CODE;
516412
516478
  setCrashPhase("shutdown");
516413
516479
  trackLifecycle("exit", { duration_ms: Date.now() - startedAt });
516414
516480
  await shutdownTelemetry({ timeoutMs: CLI_SHUTDOWN_TIMEOUT_MS });
516415
516481
  const gutter = " ".repeat(1);
516416
- process.stdout.write(`${gutter}Bye!\n`);
516482
+ if (!runningUpdateHandoff) process.stdout.write(`${gutter}Bye!\n`);
516417
516483
  const hints = [];
516418
- if (sessionId !== "" && hasContent) hints.push(`${gutter}To resume this session: blun -r ${sessionId}`);
516419
- if (tui.exitOpenUrl !== void 0) hints.push(`${gutter}open ${toTerminalHyperlink$1(tui.exitOpenUrl, tui.exitOpenUrl)}`);
516484
+ if (!runningUpdateHandoff && sessionId !== "" && hasContent) hints.push(`${gutter}To resume this session: blun -r ${sessionId}`);
516485
+ if (!runningUpdateHandoff && tui.exitOpenUrl !== void 0) hints.push(`${gutter}open ${toTerminalHyperlink$1(tui.exitOpenUrl, tui.exitOpenUrl)}`);
516420
516486
  if (hints.length > 0) process.stderr.write(`\n${hints.join("\n")}\n`);
516421
516487
  removeCrashHandlers();
516422
516488
  restoreStty();
516489
+ if (process.connected) try {
516490
+ process.disconnect();
516491
+ } catch {}
516423
516492
  process.exit(exitCode);
516424
516493
  };
516425
516494
  try {
516426
516495
  const initStartedAt = Date.now();
516427
516496
  await tui.start();
516497
+ notifyRunningRuntimeReady(tui);
516428
516498
  const initMs = Date.now() - initStartedAt;
516429
516499
  const startupSessionId = tui.getCurrentSessionId();
516430
516500
  const mcpMs = await tui.getStartupMcpMs();
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "blun-king-cli",
3
- "version": "9.1.60",
3
+ "version": "9.1.61",
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": {