blun-king-cli 9.1.58 → 9.1.60

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.57
12
+ npm install -g blun-king-cli@9.1.59
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.57
12
+ npm install -g blun-king-cli@9.1.59
13
13
  ```
14
14
 
15
15
  ## Reproduzierbares Staging und Packen
@@ -10,6 +10,8 @@ const LAUNCHER_HELP_TEXT = [
10
10
  'Launchers:',
11
11
  ' blun Starts the CLI without automatically attaching Telegram.',
12
12
  ' king Starts the same CLI and automatically attaches the configured Telegram channel.',
13
+ ' king <profile> starts an isolated named King profile.',
14
+ ' king -c <profile> continues the last session in that profile.',
13
15
  ' Both use the same version, account, model, and commands.',
14
16
  ' blun tools list shows optional tools; enable writes only explicit choices.',
15
17
  '',
@@ -31,9 +31,16 @@ const {
31
31
  securePrivateFile,
32
32
  writePrivateFile,
33
33
  } = require('./private-paths');
34
+ const {
35
+ parseProfileLaunchArgs,
36
+ resolveProfilePaths,
37
+ seedDefaultProfileFromLegacy,
38
+ } = require('./profile-runtime.cjs');
34
39
 
35
40
  const PKG = path.resolve(__dirname, '..');
36
- const ARGS = process.argv.slice(2);
41
+ const RAW_ARGS = process.argv.slice(2);
42
+ const PROFILE = parseProfileLaunchArgs(RAW_ARGS, launcherModeFromArgv(process.argv));
43
+ const ARGS = PROFILE.args;
37
44
  const CORE_LOAD_TIMEOUT_MS = 30_000;
38
45
 
39
46
  function normalizeWindowsPath(value) {
@@ -44,10 +51,24 @@ function resolveLauncherPrivatePaths(options = {}) {
44
51
  const env = options.env || process.env;
45
52
  const platform = options.platform || process.platform;
46
53
  const configuredHome = String(env.BLUN_HOME || '').trim();
54
+ const configuredSharedHome = String(env.BLUN_SHARED_HOME || '').trim();
55
+ const profileName = options.profileName || PROFILE.profileName;
47
56
  if (platform !== 'win32') {
48
57
  const userHome = (options.homedir || os.homedir)();
58
+ const sharedHome = configuredSharedHome
59
+ ? path.resolve(configuredSharedHome)
60
+ : configuredHome
61
+ ? path.resolve(configuredHome)
62
+ : path.join(userHome, '.blun');
63
+ const profilePaths = resolveProfilePaths(sharedHome, profileName);
64
+ if (configuredSharedHome && configuredHome
65
+ && path.resolve(configuredHome) !== path.resolve(profilePaths.home)) {
66
+ throw new Error(`BLUN_HOME passt nicht zum Profil ${profileName}: ${profilePaths.home}`);
67
+ }
49
68
  return {
50
- blunHome: configuredHome ? path.resolve(configuredHome) : path.join(userHome, '.blun'),
69
+ blunHome: profilePaths.home,
70
+ profilePaths,
71
+ sharedHome,
51
72
  userHome,
52
73
  };
53
74
  }
@@ -71,12 +92,26 @@ function resolveLauncherPrivatePaths(options = {}) {
71
92
  throw new Error('Das lokale Windows-Benutzerprofil ist kein kanonischer lokaler Pfad.');
72
93
  }
73
94
 
74
- const blunHome = path.win32.join(userHome, '.blun');
95
+ const sharedHome = path.win32.join(userHome, '.blun');
96
+ const profilePaths = resolveProfilePaths(sharedHome, profileName, path.win32);
97
+ if (configuredSharedHome
98
+ && normalizeWindowsPath(path.win32.resolve(configuredSharedHome)) !== normalizeWindowsPath(sharedHome)) {
99
+ throw new Error(`BLUN_SHARED_HOME muss unter Windows exakt im Windows-Benutzerprofil liegen: ${sharedHome}`);
100
+ }
101
+ const configuredHomeIsLegacyShared = !configuredSharedHome
102
+ && configuredHome
103
+ && normalizeWindowsPath(path.win32.resolve(configuredHome)) === normalizeWindowsPath(sharedHome);
75
104
  if (configuredHome
76
- && normalizeWindowsPath(path.win32.resolve(configuredHome)) !== normalizeWindowsPath(blunHome)) {
77
- throw new Error(`BLUN_HOME muss unter Windows exakt im Windows-Benutzerprofil liegen: ${blunHome}`);
105
+ && !configuredHomeIsLegacyShared
106
+ && normalizeWindowsPath(path.win32.resolve(configuredHome)) !== normalizeWindowsPath(profilePaths.home)) {
107
+ throw new Error(`BLUN_HOME passt nicht zum Profil ${profileName}: ${profilePaths.home}`);
78
108
  }
79
- return { blunHome, userHome };
109
+ return {
110
+ blunHome: profilePaths.home,
111
+ profilePaths,
112
+ sharedHome,
113
+ userHome,
114
+ };
80
115
  }
81
116
 
82
117
  function readPackageVersion() {
@@ -271,12 +306,12 @@ async function runLauncher(options = {}) {
271
306
  if (mcpNames.length > 0) {
272
307
  seedStandardTools({
273
308
  packageRoot: PKG,
274
- blunDir: privatePaths.blunHome,
309
+ blunDir: privatePaths.profilePaths.home,
275
310
  names: mcpNames,
276
311
  });
277
312
  }
278
313
  if (selected.includes('telegram')) {
279
- installTelegramForProfile(PKG, privatePaths.blunHome);
314
+ installTelegramForProfile(PKG, privatePaths.profilePaths.home);
280
315
  }
281
316
  process.stdout.write(`${selected.join('\n')}\n`);
282
317
  return;
@@ -298,7 +333,7 @@ async function runLauncher(options = {}) {
298
333
  stdin: process.stdin,
299
334
  stdout: process.stdout,
300
335
  stderr: process.stderr,
301
- blunDir: privatePaths.blunHome,
336
+ blunDir: privatePaths.sharedHome,
302
337
  packageRoot: PKG,
303
338
  noticeLease,
304
339
  request: ARGS[0],
@@ -307,13 +342,13 @@ async function runLauncher(options = {}) {
307
342
  return;
308
343
  }
309
344
 
310
- let privatePaths;
345
+ let privatePathsCache;
311
346
  const getPrivatePaths = () => {
312
- privatePaths ||= resolveLauncherPrivatePaths();
313
- return privatePaths;
347
+ privatePathsCache ||= resolveLauncherPrivatePaths();
348
+ return privatePathsCache;
314
349
  };
315
350
  const nodeRuntime = await prepareManagedNodeRuntime({
316
- getBlunDir: () => getPrivatePaths().blunHome,
351
+ getBlunDir: () => getPrivatePaths().sharedHome,
317
352
  });
318
353
  if (nodeRuntime.kind === 'failed') {
319
354
  process.stderr.write(`${nodeRuntime.message}\n`);
@@ -332,12 +367,24 @@ async function runLauncher(options = {}) {
332
367
  const env = createLauncherEnvironment(process.env, mode, readPackageVersion());
333
368
  const informationalEnv = { ...process.env };
334
369
  delete informationalEnv.BLUN_HOME;
335
- env.BLUN_HOME = resolveLauncherPrivatePaths({ env: informationalEnv }).blunHome;
370
+ delete informationalEnv.BLUN_SHARED_HOME;
371
+ const privatePaths = resolveLauncherPrivatePaths({ env: informationalEnv });
372
+ const profilePaths = privatePaths.profilePaths;
373
+ env.BLUN_HOME = profilePaths.home;
374
+ env.BLUN_SHARED_HOME = privatePaths.sharedHome;
375
+ env.BLUN_LOG_HOME = privatePaths.sharedHome;
376
+ env.BLUN_PROFILE = PROFILE.profileName;
336
377
  process.exitCode = await superviseProtectedCore(ARGS, env, callerCwd, releaseNotice);
337
378
  return;
338
379
  }
339
380
 
340
- const blunDir = getPrivatePaths().blunHome;
381
+ const privatePaths = getPrivatePaths();
382
+ const profilePaths = privatePaths.profilePaths;
383
+ ensurePrivateDirectory(privatePaths.sharedHome);
384
+ if (PROFILE.profileName === 'default') {
385
+ seedDefaultProfileFromLegacy(privatePaths.sharedHome);
386
+ }
387
+ const blunDir = profilePaths.home;
341
388
  ensurePrivateDirectory(blunDir);
342
389
 
343
390
  // --- 1. Oeffentlicher npm-Update-Dialog. -------------------------------
@@ -350,7 +397,7 @@ async function runLauncher(options = {}) {
350
397
  stdin: process.stdin,
351
398
  stdout: process.stdout,
352
399
  stderr: process.stderr,
353
- blunDir,
400
+ blunDir: privatePaths.sharedHome,
354
401
  packageRoot: PKG,
355
402
  noticeLease,
356
403
  });
@@ -382,7 +429,7 @@ async function runLauncher(options = {}) {
382
429
  });
383
430
 
384
431
  // --- 4. Eigenes BLUN-Grunddesign als einzigen Standard-Skill installieren
385
- seedStandardDesignSkill({ packageRoot: PKG, blunDir });
432
+ seedStandardDesignSkill({ packageRoot: PKG, blunDir: privatePaths.sharedHome });
386
433
 
387
434
  // --- 5. Telegram-Plugin + Token-Vorlage (nur King) -----------------------
388
435
  if (mode === LAUNCHER_MODES.KING) {
@@ -402,7 +449,10 @@ async function runLauncher(options = {}) {
402
449
 
403
450
  // --- 6. Start -----------------------------------------------------------
404
451
  const env = createLauncherEnvironment(process.env, mode, readPackageVersion());
405
- env.BLUN_HOME = blunDir;
452
+ env.BLUN_HOME = profilePaths.home;
453
+ env.BLUN_SHARED_HOME = privatePaths.sharedHome;
454
+ env.BLUN_LOG_HOME = privatePaths.sharedHome;
455
+ env.BLUN_PROFILE = PROFILE.profileName;
406
456
  env.BLUN_MODEL_MAX_COMPLETION_TOKENS = env.BLUN_MODEL_MAX_COMPLETION_TOKENS || '32768';
407
457
  process.exitCode = await superviseProtectedCore(
408
458
  ARGS,
@@ -0,0 +1,128 @@
1
+ 'use strict';
2
+
3
+ const fs = require('node:fs');
4
+ const path = require('node:path');
5
+
6
+ const DEFAULT_PROFILE = 'default';
7
+ const PROFILE_NAME_PATTERN = /^[a-z0-9][a-z0-9_-]{0,31}$/u;
8
+ const RESERVED_SINGLE_ARGUMENTS = new Set([
9
+ 'doctor',
10
+ 'export',
11
+ 'help',
12
+ 'mcp',
13
+ 'server',
14
+ 'tools',
15
+ 'update',
16
+ 'upgrade',
17
+ 'version',
18
+ ]);
19
+ const LEGACY_PROFILE_ENTRIES = Object.freeze([
20
+ 'channels',
21
+ 'config.toml',
22
+ 'mcp.json',
23
+ 'plugins',
24
+ 'session_index.jsonl',
25
+ 'sessions',
26
+ 'tui.toml',
27
+ ]);
28
+ const MIGRATION_MARKER = '.legacy-profile-migrated-v1';
29
+
30
+ function normalizeProfileName(value) {
31
+ const normalized = String(value || '').trim().toLowerCase();
32
+ if (!PROFILE_NAME_PATTERN.test(normalized)) {
33
+ throw new Error('Profilnamen duerfen nur Buchstaben, Zahlen, Unterstriche und Bindestriche enthalten.');
34
+ }
35
+ return normalized;
36
+ }
37
+
38
+ function parseProfileLaunchArgs(inputArgs, mode = 'blun') {
39
+ const args = Array.isArray(inputArgs) ? [...inputArgs] : [];
40
+ if (String(mode).toLowerCase() !== 'king') {
41
+ return { args, profileName: DEFAULT_PROFILE };
42
+ }
43
+
44
+ const profileFlagIndex = args.indexOf('--profile');
45
+ if (profileFlagIndex >= 0) {
46
+ if (profileFlagIndex + 1 >= args.length) {
47
+ throw new Error('--profile braucht einen Profilnamen.');
48
+ }
49
+ const profileName = normalizeProfileName(args[profileFlagIndex + 1]);
50
+ args.splice(profileFlagIndex, 2);
51
+ return { args, profileName };
52
+ }
53
+
54
+ if (args.length === 1) {
55
+ const candidate = String(args[0] || '');
56
+ if (!candidate.startsWith('-') && !RESERVED_SINGLE_ARGUMENTS.has(candidate.toLowerCase())) {
57
+ return { args: [], profileName: normalizeProfileName(candidate) };
58
+ }
59
+ }
60
+
61
+ if (args.length === 2 && args.includes('-c')) {
62
+ const candidate = args.find((arg) => arg !== '-c');
63
+ if (candidate && !candidate.startsWith('-')) {
64
+ return { args: ['-c'], profileName: normalizeProfileName(candidate) };
65
+ }
66
+ }
67
+
68
+ return { args, profileName: DEFAULT_PROFILE };
69
+ }
70
+
71
+ function resolveProfilePaths(sharedHome, profileName, pathImpl = path) {
72
+ const normalized = normalizeProfileName(profileName);
73
+ const home = pathImpl.join(sharedHome, 'profile', normalized);
74
+ return Object.freeze({
75
+ profileName: normalized,
76
+ sharedHome,
77
+ home,
78
+ channels: pathImpl.join(home, 'channels'),
79
+ sessions: pathImpl.join(home, 'sessions'),
80
+ plugins: pathImpl.join(home, 'plugins'),
81
+ config: pathImpl.join(home, 'config.toml'),
82
+ mcp: pathImpl.join(home, 'mcp.json'),
83
+ sessionIndex: pathImpl.join(home, 'session_index.jsonl'),
84
+ credentials: pathImpl.join(sharedHome, 'credentials'),
85
+ oauth: pathImpl.join(sharedHome, 'oauth'),
86
+ deviceId: pathImpl.join(sharedHome, 'device_id'),
87
+ skills: pathImpl.join(sharedHome, 'skills'),
88
+ secure: pathImpl.join(sharedHome, 'secure'),
89
+ persona: pathImpl.join(sharedHome, 'persona.json'),
90
+ globalLog: pathImpl.join(sharedHome, 'logs', 'blun.log'),
91
+ });
92
+ }
93
+
94
+ function copyLegacyEntry(source, target, fsImpl) {
95
+ if (!fsImpl.existsSync(source) || fsImpl.existsSync(target)) return;
96
+ const stat = fsImpl.lstatSync(source);
97
+ if (stat.isSymbolicLink()) return;
98
+ if (stat.isDirectory()) {
99
+ fsImpl.cpSync(source, target, { recursive: true, errorOnExist: false });
100
+ return;
101
+ }
102
+ if (stat.isFile()) fsImpl.copyFileSync(source, target);
103
+ }
104
+
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;
109
+
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);
113
+ }
114
+ try {
115
+ fsImpl.writeFileSync(marker, 'legacy profile state copied once\n', { encoding: 'utf8', flag: 'wx' });
116
+ } catch (error) {
117
+ if (error?.code !== 'EEXIST') throw error;
118
+ }
119
+ return paths;
120
+ }
121
+
122
+ module.exports = {
123
+ DEFAULT_PROFILE,
124
+ normalizeProfileName,
125
+ parseProfileLaunchArgs,
126
+ resolveProfilePaths,
127
+ seedDefaultProfileFromLegacy,
128
+ };
@@ -0,0 +1,34 @@
1
+ "use strict";
2
+
3
+ const MAX_AUTOMATIC_RATE_LIMIT_RESUMES = 3;
4
+ const RATE_LIMIT_RETRY_BASE_MS = 60_000;
5
+ const RATE_LIMIT_COMPACTION_THRESHOLD_TOKENS = 64_000;
6
+
7
+ function isImmediateGenerateRetryAllowed(statusCode) {
8
+ return statusCode !== 429 && [500, 502, 503, 504].includes(statusCode);
9
+ }
10
+
11
+ function rateLimitRetryDelayMs(retryCount) {
12
+ if (!Number.isInteger(retryCount) || retryCount < 1) {
13
+ throw new Error("retryCount must be a positive integer.");
14
+ }
15
+ return RATE_LIMIT_RETRY_BASE_MS * (2 ** (retryCount - 1));
16
+ }
17
+
18
+ function shouldStopAutomaticRateLimitResume(retryCount) {
19
+ return retryCount >= MAX_AUTOMATIC_RATE_LIMIT_RESUMES;
20
+ }
21
+
22
+ function shouldCompactBeforeRateLimitResume(estimatedRequestTokens) {
23
+ return Number.isFinite(estimatedRequestTokens)
24
+ && estimatedRequestTokens >= RATE_LIMIT_COMPACTION_THRESHOLD_TOKENS;
25
+ }
26
+
27
+ module.exports = {
28
+ MAX_AUTOMATIC_RATE_LIMIT_RESUMES,
29
+ RATE_LIMIT_RETRY_BASE_MS,
30
+ isImmediateGenerateRetryAllowed,
31
+ rateLimitRetryDelayMs,
32
+ shouldCompactBeforeRateLimitResume,
33
+ shouldStopAutomaticRateLimitResume,
34
+ };
@@ -74,6 +74,41 @@ function hasEquivalentServer(servers, name, tool) {
74
74
  });
75
75
  }
76
76
 
77
+ function sameStringArray(left, right) {
78
+ return Array.isArray(left)
79
+ && Array.isArray(right)
80
+ && left.length === right.length
81
+ && left.every((value, index) => value === right[index]);
82
+ }
83
+
84
+ function recognizedBundledServer(current, expected) {
85
+ return isObject(current)
86
+ && current.transport === expected.transport
87
+ && current.command === expected.command
88
+ && sameStringArray(current.args, expected.args);
89
+ }
90
+
91
+ function migrateBundledTimeouts(name, current, expected) {
92
+ if (!recognizedBundledServer(current, expected)) return false;
93
+ let changed = false;
94
+ if (name === 'agent-browser') {
95
+ if (current.startupTimeoutMs === 30_000) {
96
+ current.startupTimeoutMs = expected.startupTimeoutMs;
97
+ changed = true;
98
+ }
99
+ if (current.toolTimeoutMs === 60_000) {
100
+ current.toolTimeoutMs = expected.toolTimeoutMs;
101
+ changed = true;
102
+ }
103
+ }
104
+ if (name === 'blun-language-guard'
105
+ && (current.toolTimeoutMs === 30_000 || current.toolTimeoutMs === 60_000)) {
106
+ current.toolTimeoutMs = expected.toolTimeoutMs;
107
+ changed = true;
108
+ }
109
+ return changed;
110
+ }
111
+
77
112
  function writeMcpDocument(configPath, document) {
78
113
  const temporaryPath = path.join(
79
114
  path.dirname(configPath),
@@ -160,21 +195,27 @@ function seedStandardTools({
160
195
  const document = readMcpDocument(configPath);
161
196
  const servers = { ...document.mcpServers };
162
197
  const added = [];
198
+ const updated = [];
163
199
 
164
200
  for (const name of selectedNames) {
165
201
  const tool = catalogue.tools[name];
166
- if (hasEquivalentServer(servers, name, tool)) continue;
167
- servers[name] = bundledToolConfig(name, tool, {
202
+ const expected = bundledToolConfig(name, tool, {
168
203
  packageRoot,
169
204
  blunDir,
170
205
  platform,
171
206
  env,
172
207
  });
208
+ if (Object.hasOwn(servers, name)) {
209
+ if (migrateBundledTimeouts(name, servers[name], expected)) updated.push(name);
210
+ continue;
211
+ }
212
+ if (hasEquivalentServer(servers, name, tool)) continue;
213
+ servers[name] = expected;
173
214
  added.push(name);
174
215
  }
175
216
 
176
- if (added.length > 0) writeMcpDocument(configPath, { ...document, mcpServers: servers });
177
- return { added, configPath };
217
+ if (added.length > 0 || updated.length > 0) writeMcpDocument(configPath, { ...document, mcpServers: servers });
218
+ return { added, updated, configPath };
178
219
  }
179
220
 
180
221
  function seedStandardDesignSkill({ packageRoot, blunDir }) {
@@ -0,0 +1,79 @@
1
+ "use strict";
2
+
3
+ const DEFAULT_SUBAGENT_TIMEOUT_MS = 4 * 60 * 60 * 1000;
4
+ const MAX_SUBAGENT_TIMEOUT_MINUTES = 7 * 24 * 60;
5
+ const SUBAGENT_TIMEOUT_ENV = "BLUN_SUBAGENT_TIMEOUT_MINUTES";
6
+
7
+ function resolveSubagentTimeoutMs({ perRunMinutes, env = process.env } = {}) {
8
+ const raw = perRunMinutes ?? env[SUBAGENT_TIMEOUT_ENV];
9
+ if (raw === undefined || raw === null || raw === "") {
10
+ return DEFAULT_SUBAGENT_TIMEOUT_MS;
11
+ }
12
+
13
+ const minutes = typeof raw === "number" ? raw : Number(raw);
14
+ if (!Number.isInteger(minutes) || minutes < 0 || minutes > MAX_SUBAGENT_TIMEOUT_MINUTES) {
15
+ throw new Error(
16
+ `${SUBAGENT_TIMEOUT_ENV} must be an integer from 0 to ${MAX_SUBAGENT_TIMEOUT_MINUTES} minutes.`,
17
+ );
18
+ }
19
+ return minutes === 0 ? undefined : minutes * 60 * 1000;
20
+ }
21
+
22
+ function abortError(reason) {
23
+ if (reason instanceof Error) return reason;
24
+ return new Error(reason === undefined ? "Subagent interrupted." : String(reason));
25
+ }
26
+
27
+ async function runAgentWithTimeoutContinuation({
28
+ handle,
29
+ timeoutMs,
30
+ signal,
31
+ abortCurrent,
32
+ retrySameAgent,
33
+ }) {
34
+ let currentHandle = handle;
35
+
36
+ while (true) {
37
+ let timeout;
38
+ let removeAbortListener = () => {};
39
+ const completion = Promise.resolve(currentHandle.completion).then(
40
+ (outcome) => ({ kind: "completed", outcome }),
41
+ (error) => ({ kind: "failed", error }),
42
+ );
43
+ const timedOut = timeoutMs === undefined
44
+ ? new Promise(() => {})
45
+ : new Promise((resolve) => {
46
+ timeout = setTimeout(() => resolve({ kind: "timed_out" }), timeoutMs);
47
+ });
48
+ const interrupted = signal.aborted
49
+ ? Promise.resolve({ kind: "interrupted", reason: signal.reason })
50
+ : new Promise((resolve) => {
51
+ const onAbort = () => resolve({ kind: "interrupted", reason: signal.reason });
52
+ signal.addEventListener("abort", onAbort, { once: true });
53
+ removeAbortListener = () => signal.removeEventListener("abort", onAbort);
54
+ });
55
+
56
+ const result = await Promise.race([completion, timedOut, interrupted]);
57
+ if (timeout !== undefined) clearTimeout(timeout);
58
+ removeAbortListener();
59
+
60
+ if (result.kind === "completed") return { handle: currentHandle, outcome: result.outcome };
61
+ if (result.kind === "failed") throw result.error;
62
+ if (result.kind === "interrupted") {
63
+ abortCurrent(result.reason);
64
+ await Promise.resolve(currentHandle.completion).catch(() => {});
65
+ throw abortError(result.reason);
66
+ }
67
+
68
+ const reason = new Error("Subagent continuation interval elapsed.");
69
+ abortCurrent(reason);
70
+ await Promise.resolve(currentHandle.completion).catch(() => {});
71
+ currentHandle = await retrySameAgent(currentHandle.agentId);
72
+ }
73
+ }
74
+
75
+ module.exports = {
76
+ DEFAULT_SUBAGENT_TIMEOUT_MS,
77
+ resolveSubagentTimeoutMs,
78
+ runAgentWithTimeoutContinuation,
79
+ };
package/blun.mjs CHANGED
@@ -1539,7 +1539,6 @@ function isRetryableGenerateError(error) {
1539
1539
  if (error instanceof APIConnectionError || error instanceof APITimeoutError) return true;
1540
1540
  if (error instanceof APIEmptyResponseError) return true;
1541
1541
  return error instanceof APIStatusError && [
1542
- 429,
1543
1542
  500,
1544
1543
  502,
1545
1544
  503,
@@ -10263,6 +10262,11 @@ function resolveBlunHome$1(homeDir) {
10263
10262
  if (env !== void 0 && env.length > 0) return env;
10264
10263
  return join$4(homedir(), ".blun");
10265
10264
  }
10265
+ function resolveSharedBlunHome(homeDir) {
10266
+ const shared = process.env["BLUN_SHARED_HOME"];
10267
+ if (shared !== void 0 && shared.length > 0) return shared;
10268
+ return homeDir ?? resolveBlunHome$1();
10269
+ }
10266
10270
  function resolveConfigPath(input) {
10267
10271
  return input.configPath ?? join$4(resolveBlunHome$1(input.homeDir), "config.toml");
10268
10272
  }
@@ -16884,11 +16888,16 @@ function resolvePayload(payload) {
16884
16888
  };
16885
16889
  }
16886
16890
  function mergeCtx(payloadCtx, boundCtx) {
16887
- if (!(Object.keys(boundCtx).length > 0)) return payloadCtx;
16888
- if (payloadCtx === void 0) return { ...boundCtx };
16891
+ const runtimeCtx = process.env["BLUN_PROFILE"] ? { profile: process.env["BLUN_PROFILE"] } : {};
16892
+ if (!(Object.keys(boundCtx).length > 0) && Object.keys(runtimeCtx).length === 0) return payloadCtx;
16893
+ if (payloadCtx === void 0) return {
16894
+ ...boundCtx,
16895
+ ...runtimeCtx
16896
+ };
16889
16897
  return {
16890
16898
  ...payloadCtx,
16891
- ...boundCtx
16899
+ ...boundCtx,
16900
+ ...runtimeCtx
16892
16901
  };
16893
16902
  }
16894
16903
  function resolveGlobalLogPath(homeDir) {
@@ -28180,6 +28189,8 @@ var init_conduct = __esmMin((() => {}));
28180
28189
  * read/write the same location: `BLUN_HOME` env override, else `~/.blun`.
28181
28190
  */
28182
28191
  function resolveBlunHome(env = process.env) {
28192
+ const shared = env["BLUN_SHARED_HOME"]?.trim();
28193
+ if (shared && shared.length > 0) return shared;
28183
28194
  const override = env["BLUN_HOME"]?.trim();
28184
28195
  return override && override.length > 0 ? override : join$4(homedir(), ".blun");
28185
28196
  }
@@ -29016,26 +29027,46 @@ var init_agent_task = __esmMin((() => {
29016
29027
  description;
29017
29028
  subagentHost;
29018
29029
  abortController;
29030
+ continuationTimeoutMs;
29031
+ runInBackground;
29032
+ parentToolCallId;
29019
29033
  kind = "agent";
29020
29034
  idPrefix = "agent";
29021
29035
  agentId;
29022
29036
  subagentType;
29023
- constructor(handle, description, subagentHost, abortController) {
29037
+ constructor(handle, description, subagentHost, abortController, options) {
29024
29038
  this.handle = handle;
29025
29039
  this.description = description;
29026
29040
  this.subagentHost = subagentHost;
29027
29041
  this.abortController = abortController;
29042
+ this.continuationTimeoutMs = options.timeoutMs;
29043
+ this.runInBackground = options.runInBackground;
29044
+ this.parentToolCallId = options.parentToolCallId;
29028
29045
  this.agentId = handle.agentId;
29029
29046
  this.subagentType = handle.profileName;
29030
29047
  }
29031
29048
  async start(sink) {
29032
- const requestAbort = () => {
29033
- this.abortController.abort(sink.signal.reason);
29034
- };
29035
- if (sink.signal.aborted) requestAbort();
29036
- else sink.signal.addEventListener("abort", requestAbort, { once: true });
29037
29049
  try {
29038
- const outcome = await this.handle.completion;
29050
+ const { handle, outcome } = await runAgentWithTimeoutContinuation({
29051
+ handle: this.handle,
29052
+ timeoutMs: this.continuationTimeoutMs,
29053
+ signal: sink.signal,
29054
+ abortCurrent: (reason) => {
29055
+ this.abortController.abort(reason);
29056
+ },
29057
+ retrySameAgent: async (agentId) => {
29058
+ this.abortController = new AbortController();
29059
+ this.handle = await this.subagentHost.retry(agentId, {
29060
+ parentToolCallId: this.parentToolCallId,
29061
+ prompt: "Continue from the preserved context after the wall-clock interval.",
29062
+ description: this.description,
29063
+ runInBackground: this.runInBackground,
29064
+ signal: this.abortController.signal
29065
+ });
29066
+ return this.handle;
29067
+ }
29068
+ });
29069
+ this.handle = handle;
29039
29070
  sink.appendOutput(outcome.result);
29040
29071
  await sink.settle({ status: "completed" });
29041
29072
  } catch (error) {
@@ -29047,16 +29078,16 @@ var init_agent_task = __esmMin((() => {
29047
29078
  status: "failed",
29048
29079
  stopReason: errorMessage$12(error)
29049
29080
  });
29050
- } finally {
29051
- sink.signal.removeEventListener("abort", requestAbort);
29052
29081
  }
29053
29082
  }
29054
29083
  onDetach() {
29084
+ this.runInBackground = true;
29055
29085
  this.subagentHost.markActiveChildDetached(this.agentId);
29056
29086
  }
29057
29087
  toInfo(base) {
29058
29088
  return {
29059
29089
  ...base,
29090
+ timeoutMs: this.continuationTimeoutMs,
29060
29091
  kind: "agent",
29061
29092
  agentId: this.agentId,
29062
29093
  subagentType: this.subagentType
@@ -235026,7 +235057,7 @@ async function resolveSkillRoots(options) {
235026
235057
  const roots = [];
235027
235058
  const mergeAllAvailableSkills = options.mergeAllAvailableSkills ?? true;
235028
235059
  const { userHomeDir, workDir } = options.paths;
235029
- const brandHomeDir = options.paths.brandHomeDir ?? process.env["BLUN_HOME"] ?? posix$2.join(userHomeDir, ".blun");
235060
+ const brandHomeDir = options.paths.brandHomeDir ?? process.env["BLUN_SHARED_HOME"] ?? process.env["BLUN_HOME"] ?? posix$2.join(userHomeDir, ".blun");
235030
235061
  const projectRoot = await findProjectRoot$1(workDir);
235031
235062
  if (options.explicitDirs !== void 0 && options.explicitDirs.length > 0) await pushConfiguredDirs(roots, options.explicitDirs, projectRoot, userHomeDir, "user", isDir, realpath);
235032
235063
  else {
@@ -251078,14 +251109,14 @@ function resolveSwarmMaxConcurrency(env = process.env) {
251078
251109
  if (!Number.isInteger(value) || value <= 0) throw new Error(`${AGENT_SWARM_MAX_CONCURRENCY_ENV} must be a positive integer, got ${JSON.stringify(raw)}.`);
251079
251110
  return value;
251080
251111
  }
251081
- var import_retry, INITIAL_LAUNCH_LIMIT, INITIAL_LAUNCH_INTERVAL_MS, RATE_LIMIT_RETRY_BASE_MS, RATE_LIMIT_RETRY_FACTOR, RATE_LIMIT_CAPACITY_SHRINK_INTERVAL_MS, RATE_LIMIT_CAPACITY_RECOVERY_INTERVAL_MS, RATE_LIMIT_SUSPENDED_REASON, AGENT_SWARM_MAX_CONCURRENCY_ENV, SubagentBatch;
251112
+ var import_retry, INITIAL_LAUNCH_LIMIT, INITIAL_LAUNCH_INTERVAL_MS, RATE_LIMIT_RETRY_BASE_MS, RATE_LIMIT_RETRY_FACTOR, RATE_LIMIT_CAPACITY_SHRINK_INTERVAL_MS, RATE_LIMIT_CAPACITY_RECOVERY_INTERVAL_MS, RATE_LIMIT_SUSPENDED_REASON, AGENT_SWARM_MAX_CONCURRENCY_ENV, MAX_AUTOMATIC_RATE_LIMIT_RESUMES, rateLimitRetryDelayMs, shouldCompactBeforeRateLimitResume, shouldStopAutomaticRateLimitResume, SubagentBatch;
251082
251113
  var init_subagent_batch = __esmMin((() => {
251083
251114
  init_src$4();
251084
251115
  import_retry = /* @__PURE__ */ __toESM(require_retry$1(), 1);
251085
251116
  init_abort();
251117
+ ({ MAX_AUTOMATIC_RATE_LIMIT_RESUMES, RATE_LIMIT_RETRY_BASE_MS, rateLimitRetryDelayMs, shouldCompactBeforeRateLimitResume, shouldStopAutomaticRateLimitResume } = createRequire(import.meta.url)("./bin/rate-limit-recovery-policy.cjs"));
251086
251118
  INITIAL_LAUNCH_LIMIT = 5;
251087
251119
  INITIAL_LAUNCH_INTERVAL_MS = 700;
251088
- RATE_LIMIT_RETRY_BASE_MS = 3e3;
251089
251120
  RATE_LIMIT_RETRY_FACTOR = 2;
251090
251121
  RATE_LIMIT_CAPACITY_SHRINK_INTERVAL_MS = 2e3;
251091
251122
  RATE_LIMIT_CAPACITY_RECOVERY_INTERVAL_MS = 180 * 1e3;
@@ -251226,6 +251257,7 @@ var init_subagent_batch = __esmMin((() => {
251226
251257
  description: task.description,
251227
251258
  swarmIndex: task.swarmIndex,
251228
251259
  runInBackground: task.runInBackground,
251260
+ compactBeforeRetry: attempt.state.retryCount > 0,
251229
251261
  signal: attempt.controller.signal,
251230
251262
  onReady: () => {
251231
251263
  this.markAttemptReady(attempt);
@@ -251291,17 +251323,27 @@ var init_subagent_batch = __esmMin((() => {
251291
251323
  handleAttemptOutcome(attempt, outcome) {
251292
251324
  if (!this.releaseAttempt(attempt)) return;
251293
251325
  if (this.finished) return;
251294
- if ("status" in outcome) this.results[attempt.state.index] = outcome;
251295
- else if (this.isOnlyUnfinishedTask(attempt.state)) this.results[attempt.state.index] = {
251326
+ if ("status" in outcome && attempt.timedOut && outcome.agentId !== void 0) {
251327
+ this.requeueTimedOut(attempt, outcome.agentId);
251328
+ }
251329
+ else if ("status" in outcome) this.results[attempt.state.index] = outcome;
251330
+ else if (shouldStopAutomaticRateLimitResume(attempt.state.retryCount)) this.results[attempt.state.index] = {
251296
251331
  task: attempt.state.task,
251297
251332
  agentId: outcome.agentId,
251298
251333
  status: "failed",
251299
251334
  state: "started",
251300
- error: outcome.error
251335
+ error: `${outcome.error} Automatic continuation stopped after ${String(MAX_AUTOMATIC_RATE_LIMIT_RESUMES)} attempts; agent ${outcome.agentId} remains resumable.`
251301
251336
  };
251302
251337
  else this.requeueRateLimited(attempt, outcome.agentId);
251303
251338
  this.schedule();
251304
251339
  }
251340
+ requeueTimedOut(attempt, agentId) {
251341
+ const state = attempt.state;
251342
+ state.agentId = agentId;
251343
+ state.retryAgentId = agentId;
251344
+ state.retryReadyAt = 0;
251345
+ this.pending.unshift(state);
251346
+ }
251305
251347
  handleAttemptError(attempt, error) {
251306
251348
  if (!this.releaseAttempt(attempt)) return;
251307
251349
  if (this.finished) return;
@@ -251330,12 +251372,7 @@ var init_subagent_batch = __esmMin((() => {
251330
251372
  const now = Date.now();
251331
251373
  this.lastRateLimitAt = now;
251332
251374
  state.retryCount += 1;
251333
- const retryDelay = import_retry.createTimeout(Math.max(0, state.retryCount - 1), {
251334
- minTimeout: RATE_LIMIT_RETRY_BASE_MS,
251335
- maxTimeout: Number.POSITIVE_INFINITY,
251336
- factor: RATE_LIMIT_RETRY_FACTOR,
251337
- randomize: false
251338
- });
251375
+ const retryDelay = rateLimitRetryDelayMs(state.retryCount);
251339
251376
  state.retryReadyAt = now + retryDelay;
251340
251377
  this.pending.unshift(state);
251341
251378
  this.enterRateLimitMode(now);
@@ -251509,7 +251546,7 @@ function shouldSuppressQueuedAttemptFailureEvent(options, error) {
251509
251546
  if (isProviderRateLimitError(error)) return true;
251510
251547
  return isAbortError$4(error) || options.signal.aborted;
251511
251548
  }
251512
- var DEFAULT_SUBAGENT_TIMEOUT_MS, DEFAULT_SUBAGENT_TIMEOUT_DESCRIPTION, SUMMARY_MIN_LENGTH, SUMMARY_CONTINUATION_ATTEMPTS, HOOK_TEXT_PREVIEW_LENGTH, SUBAGENT_MAX_TOKENS_ERROR, TOOL_CALL_DISABLED_MESSAGE, SUBAGENT_PROMPT_ORIGIN, SIDE_QUESTION_SYSTEM_REMINDER, SessionSubagentHost;
251549
+ var DEFAULT_SUBAGENT_TIMEOUT_MS, resolveSubagentTimeoutMs, runAgentWithTimeoutContinuation, SUMMARY_MIN_LENGTH, SUMMARY_CONTINUATION_ATTEMPTS, HOOK_TEXT_PREVIEW_LENGTH, SUBAGENT_MAX_TOKENS_ERROR, TOOL_CALL_DISABLED_MESSAGE, SUBAGENT_PROMPT_ORIGIN, SIDE_QUESTION_SYSTEM_REMINDER, SessionSubagentHost;
251513
251550
  var init_subagent_host = __esmMin((() => {
251514
251551
  init_src$4();
251515
251552
  init_errors$8();
@@ -251521,8 +251558,7 @@ var init_subagent_host = __esmMin((() => {
251521
251558
  init_git_context();
251522
251559
  init_subagent_batch();
251523
251560
  init_summary_continuation();
251524
- DEFAULT_SUBAGENT_TIMEOUT_MS = 1800 * 1e3;
251525
- DEFAULT_SUBAGENT_TIMEOUT_DESCRIPTION = "30 minutes";
251561
+ ({ DEFAULT_SUBAGENT_TIMEOUT_MS, resolveSubagentTimeoutMs, runAgentWithTimeoutContinuation } = createRequire(import.meta.url)("./bin/subagent-timeout-policy.cjs"));
251526
251562
  SUMMARY_MIN_LENGTH = 200;
251527
251563
  SUMMARY_CONTINUATION_ATTEMPTS = 1;
251528
251564
  HOOK_TEXT_PREVIEW_LENGTH = 500;
@@ -251634,6 +251670,13 @@ IMPORTANT:
251634
251670
  modelAlias: parent.config.modelAlias,
251635
251671
  actionStyle: parent.config.actionStyle
251636
251672
  });
251673
+ if (runOptions.compactBeforeRetry === true && shouldCompactBeforeRateLimitResume(child.fullCompaction.estimateCurrentRequestTokens())) {
251674
+ child.fullCompaction.begin({
251675
+ source: "manual",
251676
+ instruction: "Preserve completed work, current state, exact evidence, and the next action so this same agent can continue after the provider rate limit."
251677
+ });
251678
+ await child.fullCompaction.block(runOptions.signal);
251679
+ }
251637
251680
  this.emitSubagentStarted(parent, agentId);
251638
251681
  if (child.turn.retry("agent-host") === null) throw new Error(`Agent instance "${agentId}" could not start a retry turn`);
251639
251682
  this.observeFirstRequest(child, runOptions);
@@ -251865,7 +251908,7 @@ var init_agent_background_enabled = __esmMin((() => {
251865
251908
  //#region ../../packages/agent-core/src/tools/builtin/collaboration/agent.md?raw
251866
251909
  var agent_default;
251867
251910
  var init_agent$2 = __esmMin((() => {
251868
- agent_default = "Launch a subagent to handle a task. The subagent runs as a same-process loop instance with its own context and wire file. Delegating also keeps the bulk of intermediate file contents out of your own context — you get a conclusion back instead of a pile of dumps.\n\nWriting the prompt:\n- The subagent starts with zero context — it has not seen this conversation. Brief it like a colleague who just walked into the room: state the goal, list what you already know, hand over the specifics.\n- Lookups (read this file, run that test): put the exact path or command in the prompt. The subagent should not have to search for things you already know.\n- Investigations (figure out X, find why Y): give the question, not prescribed steps — fixed steps become dead weight when the premise is wrong.\n- Do not delegate understanding. If the task hinges on a file path or line number, find it yourself first and write it into the prompt.\n\nUsage notes:\n- When the task continues earlier work a subagent already did, prefer resuming that agent (pass its `resume` id) over spawning a fresh instance — the resumed agent keeps its prior context.\n- A subagent's result is only visible to you, not to the user. When the user needs to see what a subagent produced, summarize the relevant parts yourself in your own reply.\n- Subagents use a fixed 30-minute timeout. If one times out, resume the same agent instead of starting over.\n\nWhen NOT to use Agent: skip delegation for trivial work you can do directly — reading a file whose path you already know, searching a small known set of files, or any task that takes only a step or two. Delegation has a context-handoff cost; it pays off only when the task is substantial enough to outweigh it.\n\nOnce a subagent is running, leave that scope to it: do not redo its searches or reads in parallel, and do not abandon it midway and finish the job manually. Both undo the context savings the delegation was meant to buy.\n";
251911
+ agent_default = "Launch a subagent to handle a task. The subagent runs as a same-process loop instance with its own context and wire file. Delegating also keeps the bulk of intermediate file contents out of your own context — you get a conclusion back instead of a pile of dumps.\n\nWriting the prompt:\n- The subagent starts with zero context — it has not seen this conversation. Brief it like a colleague who just walked into the room: state the goal, list what you already know, hand over the specifics.\n- Lookups (read this file, run that test): put the exact path or command in the prompt. The subagent should not have to search for things you already know.\n- Investigations (figure out X, find why Y): give the question, not prescribed steps — fixed steps become dead weight when the premise is wrong.\n- Do not delegate understanding. If the task hinges on a file path or line number, find it yourself first and write it into the prompt.\n\nUsage notes:\n- When the task continues earlier work a subagent already did, prefer resuming that agent (pass its `resume` id) over spawning a fresh instance — the resumed agent keeps its prior context.\n- A subagent's result is only visible to you, not to the user. When the user needs to see what a subagent produced, summarize the relevant parts yourself in your own reply.\n- The default wall-clock interval is four hours; when it elapses, the same agent continues automatically with its preserved context. Set `timeout_minutes=0` to disable the interval, or provide another minute value for this run.\n\nWhen NOT to use Agent: skip delegation for trivial work you can do directly — reading a file whose path you already know, searching a small known set of files, or any task that takes only a step or two. Delegation has a context-handoff cost; it pays off only when the task is substantial enough to outweigh it.\n\nOnce a subagent is running, leave that scope to it: do not redo its searches or reads in parallel, and do not abandon it midway and finish the job manually. Both undo the context savings the delegation was meant to buy.\n";
251869
251912
  }));
251870
251913
  //#endregion
251871
251914
  //#region ../../packages/agent-core/src/tools/builtin/collaboration/agent.ts
@@ -251944,7 +251987,8 @@ var init_agent$1 = __esmMin((() => {
251944
251987
  description: string().describe("Short task description (3-5 words) for UI display"),
251945
251988
  subagent_type: string().optional().describe("One of the available agent types (see \"Available agent types\" in this tool description). Defaults to \"coder\" when omitted."),
251946
251989
  resume: string().optional().describe("Optional agent ID to resume instead of creating a new instance. When set, do not also pass subagent_type — the resumed agent keeps its own type, and supplying both is rejected."),
251947
- run_in_background: boolean$1().optional().describe("If true, return immediately without waiting for completion. Prefer false unless the task can run independently and there is a clear benefit to not waiting.")
251990
+ run_in_background: boolean$1().optional().describe("If true, return immediately without waiting for completion. Prefer false unless the task can run independently and there is a clear benefit to not waiting."),
251991
+ timeout_minutes: number$1().int().min(0).max(10080).optional().describe("Wall-clock interval before the same agent is continued automatically. Defaults to 240 minutes. Set 0 to disable the interval for this run.")
251948
251992
  }));
251949
251993
  object({
251950
251994
  result: string().describe("Aggregated text output from the subagent"),
@@ -252039,9 +252083,12 @@ var init_agent$1 = __esmMin((() => {
252039
252083
  }
252040
252084
  let taskId;
252041
252085
  try {
252042
- taskId = this.backgroundManager.registerTask(new AgentBackgroundTask(handle, args.description, this.subagentHost, controller), {
252086
+ taskId = this.backgroundManager.registerTask(new AgentBackgroundTask(handle, args.description, this.subagentHost, controller, {
252087
+ timeoutMs: resolveSubagentTimeoutMs({ perRunMinutes: args.timeout_minutes }),
252088
+ runInBackground,
252089
+ parentToolCallId: toolCallId
252090
+ }), {
252043
252091
  detached: runInBackground,
252044
- timeoutMs: DEFAULT_SUBAGENT_TIMEOUT_MS,
252045
252092
  signal: runInBackground ? void 0 : signal
252046
252093
  });
252047
252094
  signal.removeEventListener("abort", abortBeforeRegister);
@@ -252075,7 +252122,7 @@ var init_agent$1 = __esmMin((() => {
252075
252122
  if (info?.status === "completed") return { output: formatForegroundAgentSuccess(handle, await this.backgroundManager.readOutput(taskId)) };
252076
252123
  const timedOut = info?.status === "timed_out";
252077
252124
  return {
252078
- output: formatForegroundAgentFailure(handle, timedOut ? `Agent timed out after ${DEFAULT_SUBAGENT_TIMEOUT_DESCRIPTION}.` : info?.stopReason === "Interrupted by user" ? USER_INTERRUPTED_SUBAGENT_MESSAGE : info?.stopReason !== void 0 ? info.stopReason : "The subagent was stopped before it finished.", timedOut),
252125
+ output: formatForegroundAgentFailure(handle, timedOut ? "Agent timed out before automatic continuation could start." : info?.stopReason === "Interrupted by user" ? USER_INTERRUPTED_SUBAGENT_MESSAGE : info?.stopReason !== void 0 ? info.stopReason : "The subagent was stopped before it finished.", timedOut),
252079
252126
  isError: true
252080
252127
  };
252081
252128
  }
@@ -252184,7 +252231,8 @@ var init_agent_swarm = __esmMin((() => {
252184
252231
  subagent_type: string().trim().min(1).optional().describe("Subagent type used for every new subagent spawned from items; defaults to coder when omitted. Resumed subagents always keep their original type, so passing subagent_type together with resume_agent_ids is allowed — it only affects the item-based spawns."),
252185
252232
  prompt_template: string().trim().min(1).optional().describe(`Prompt template for each subagent. The ${PROMPT_TEMPLATE_PLACEHOLDER} placeholder is replaced with each item value.`),
252186
252233
  items: array(string().trim().min(1)).max(MAX_AGENT_SWARM_SUBAGENTS).optional().describe(`Values used to fill ${PROMPT_TEMPLATE_PLACEHOLDER}. Each item launches one new subagent.`),
252187
- resume_agent_ids: record(string().trim().min(1), string().trim().min(1)).optional().describe("Map of existing subagent agent_id to the prompt used to resume that subagent. These resumed subagents are launched before new item-based subagents.")
252234
+ resume_agent_ids: record(string().trim().min(1), string().trim().min(1)).optional().describe("Map of existing subagent agent_id to the prompt used to resume that subagent. These resumed subagents are launched before new item-based subagents."),
252235
+ timeout_minutes: number$1().int().min(0).max(10080).optional().describe("Wall-clock interval before each same swarm agent is continued automatically. Defaults to 240 minutes. Set 0 to disable the interval for this run.")
252188
252236
  }).strict();
252189
252237
  AgentSwarmTool = class {
252190
252238
  subagentHost;
@@ -252235,7 +252283,7 @@ var init_agent_swarm = __esmMin((() => {
252235
252283
  runInBackground: false,
252236
252284
  swarmItem: spec.item,
252237
252285
  signal,
252238
- timeout: DEFAULT_SUBAGENT_TIMEOUT_MS
252286
+ timeout: resolveSubagentTimeoutMs({ perRunMinutes: args.timeout_minutes })
252239
252287
  };
252240
252288
  if (spec.kind === "resume") return {
252241
252289
  ...common,
@@ -257129,7 +257177,7 @@ function rgBinaryName() {
257129
257177
  return process.platform === "win32" ? "rg.exe" : "rg";
257130
257178
  }
257131
257179
  function getShareDir() {
257132
- const override = process.env["BLUN_HOME"];
257180
+ const override = process.env["BLUN_SHARED_HOME"] ?? process.env["BLUN_HOME"];
257133
257181
  if (override !== void 0 && override !== "") return override;
257134
257182
  return join$4(homedir(), ".blun");
257135
257183
  }
@@ -295369,7 +295417,7 @@ var init_session$1 = __esmMin((() => {
295369
295417
  this.currentMcpConfig = options.mcpConfig;
295370
295418
  this.skills = new SessionSkillRegistry({ sessionId: options.id });
295371
295419
  this.mcp = new McpConnectionManager({
295372
- oauthService: new McpOAuthService({ blunHomeDir: options.blunHomeDir }),
295420
+ oauthService: new McpOAuthService({ blunHomeDir: resolveSharedBlunHome(options.blunHomeDir) }),
295373
295421
  log: this.log,
295374
295422
  stdioCwd: options.kaos.getcwd()
295375
295423
  });
@@ -295805,7 +295853,7 @@ var init_session$1 = __esmMin((() => {
295805
295853
  const roots = await resolveSkillRoots({
295806
295854
  paths: {
295807
295855
  userHomeDir: this.options.skills?.userHomeDir ?? homedir(),
295808
- brandHomeDir: this.options.skills?.brandHomeDir ?? this.options.blunHomeDir,
295856
+ brandHomeDir: this.options.skills?.brandHomeDir ?? resolveSharedBlunHome(this.options.blunHomeDir),
295809
295857
  workDir: this.options.kaos.getcwd()
295810
295858
  },
295811
295859
  explicitDirs: this.options.skills?.explicitDirs,
@@ -315180,7 +315228,7 @@ var init_core_impl = __esmMin((() => {
315180
315228
  const explicitDirs = this.skillDirs.length > 0 ? this.skillDirs : void 0;
315181
315229
  return {
315182
315230
  userHomeDir: this.userHomeDir,
315183
- brandHomeDir: this.homeDir,
315231
+ brandHomeDir: resolveSharedBlunHome(this.homeDir),
315184
315232
  explicitDirs,
315185
315233
  extraDirs: config.extraSkillDirs,
315186
315234
  pluginSkillRoots: this.plugins.pluginSkillRoots(),
@@ -315364,7 +315412,7 @@ function resolveLoggingConfig(input) {
315364
315412
  const env = input.env ?? process.env;
315365
315413
  return {
315366
315414
  level: parseLevel(env["BLUN_LOG_LEVEL"]) ?? "info",
315367
- globalLogPath: resolveGlobalLogPath(input.homeDir),
315415
+ globalLogPath: resolveGlobalLogPath(env["BLUN_LOG_HOME"] ?? input.homeDir),
315368
315416
  globalMaxBytes: parsePositiveInt(env["BLUN_LOG_GLOBAL_MAX_BYTES"]) ?? 6291456,
315369
315417
  globalFiles: parsePositiveInt(env["BLUN_LOG_GLOBAL_FILES"]) ?? 5,
315370
315418
  sessionMaxBytes: parsePositiveInt(env["BLUN_LOG_SESSION_MAX_BYTES"]) ?? 5242880,
@@ -316463,7 +316511,7 @@ var init_managedAuth = __esmMin((() => {
316463
316511
  constructor(options) {
316464
316512
  this.options = options;
316465
316513
  this.toolkit = new BlunOAuthToolkit({
316466
- homeDir: options.homeDir,
316514
+ homeDir: resolveSharedBlunHome(options.homeDir),
316467
316515
  configAdapter: {
316468
316516
  configPath: options.configPath,
316469
316517
  read: () => readConfigFile(options.configPath),
@@ -325309,9 +325357,9 @@ var BlunAuthFacade = class {
325309
325357
  managedApiKeyRevocations;
325310
325358
  constructor(options) {
325311
325359
  this.options = options;
325312
- this.managedApiKeyRevocations = new FileManagedApiKeyRevocationStore(options.homeDir);
325360
+ this.managedApiKeyRevocations = new FileManagedApiKeyRevocationStore(resolveSharedBlunHome(options.homeDir));
325313
325361
  this.toolkit = new BlunOAuthToolkit({
325314
- homeDir: options.homeDir,
325362
+ homeDir: resolveSharedBlunHome(options.homeDir),
325315
325363
  identity: options.identity,
325316
325364
  onRefresh: options.onRefresh,
325317
325365
  configAdapter: {
@@ -340448,7 +340496,7 @@ function createCliTelemetryBootstrap() {
340448
340496
  const homeDir = resolveBlunHome$1();
340449
340497
  return {
340450
340498
  homeDir,
340451
- deviceId: createBlunDeviceId(homeDir, { onFirstLaunch: () => {
340499
+ deviceId: createBlunDeviceId(resolveSharedBlunHome(homeDir), { onFirstLaunch: () => {
340452
340500
  firstLaunch = true;
340453
340501
  } }),
340454
340502
  firstLaunch
@@ -420638,12 +420686,20 @@ const LOOP_CREATE_FILLERS = new Set([
420638
420686
  "every",
420639
420687
  "each"
420640
420688
  ]);
420641
- const LOOP_INTERVAL_TOKEN = /^\d+(?:m|h|d)$/i;
420689
+ const LOOP_INTERVAL_TOKEN = /^(\d+)(m|min(?:ute)?n?|minutes?|h|std|stunden?|hours?|d|tage?|days?)$/i;
420642
420690
  const LOOP_NATURAL_INTERVAL = /\b(?:alle|jede(?:n|r|s)?|every|each)\s+(\d+)\s*(min(?:ute)?n?|minutes?|stunden?|hours?|tage?|days?)\b/iu;
420643
420691
  const LOOP_NATURAL_HALF_HOUR = /\b(?:alle|jede(?:n|r|s)?|every|each)\s+(?:halbe|half(?:\s+an)?)\s+(?:stunde|hour)\b/iu;
420644
420692
  const LOOP_NATURAL_SINGLE_INTERVAL = /\b(?:jede(?:n|r|s)?|every|each)\s+(minute|stunde|hour|tag|day)\b/iu;
420645
420693
  const LOOP_NATURAL_ADVERB_INTERVAL = /\b(stündlich|stuendlich|hourly|täglich|taeglich|daily)\b/iu;
420646
420694
  const LOOP_SELF_CONTROLLED = /\b(?:selbst\s+entscheiden|selbstgesteuert)\b/iu;
420695
+ function normalizeLoopIntervalToken(token) {
420696
+ const match = LOOP_INTERVAL_TOKEN.exec(token);
420697
+ if (match === null) return null;
420698
+ const amount = match[1];
420699
+ const unit = match[2]?.toLowerCase() ?? "";
420700
+ const suffix = unit === "h" || unit === "std" || unit.startsWith("stund") || unit.startsWith("hour") ? "h" : unit === "d" || unit.startsWith("tag") || unit.startsWith("day") ? "d" : "m";
420701
+ return `${amount}${suffix}`;
420702
+ }
420647
420703
  function naturalLoopIntervalMatch(args) {
420648
420704
  const numeric = LOOP_NATURAL_INTERVAL.exec(args);
420649
420705
  if (numeric !== null) {
@@ -420706,8 +420762,9 @@ function loopInputHint(rawArgs) {
420706
420762
  if (/^(?:status|pause|paused|resume|stop)$/i.test(args)) return "";
420707
420763
  const natural = naturalLoopIntervalMatch(args);
420708
420764
  if (natural !== null) return naturalLoopIntervalText(natural.interval);
420709
- const explicit = /^(?:start\s+)?([1-9]\d*(?:m|h|d))\b/i.exec(args);
420710
- if (explicit?.[1] !== void 0) return naturalLoopIntervalText(explicit[1].toLowerCase());
420765
+ const explicitToken = /^(?:start\s+)?(\S+)/i.exec(args)?.[1];
420766
+ const explicit = explicitToken === void 0 ? null : normalizeLoopIntervalToken(explicitToken);
420767
+ if (explicit !== null) return naturalLoopIntervalText(explicit);
420711
420768
  return "Selbstgesteuert";
420712
420769
  }
420713
420770
  function parseLoopCommand(rawArgs) {
@@ -420735,8 +420792,9 @@ function parseLoopCommand(rawArgs) {
420735
420792
  };
420736
420793
  }
420737
420794
  if (LOOP_CREATE_FILLERS.has(tokens[startIndex]?.toLowerCase() ?? "")) startIndex += 1;
420738
- const interval = tokens[startIndex];
420739
- if (interval !== void 0 && LOOP_INTERVAL_TOKEN.test(interval)) {
420795
+ const intervalToken = tokens[startIndex];
420796
+ const interval = intervalToken === void 0 ? null : normalizeLoopIntervalToken(intervalToken);
420797
+ if (interval !== null) {
420740
420798
  const prompt = tokens.slice(startIndex + 1).join(" ").trim();
420741
420799
  if (prompt.length === 0) return { kind: "error" };
420742
420800
  return {
@@ -489065,6 +489123,14 @@ function formatLiveElapsed(seconds) {
489065
489123
  const remainingMinutes = minutes % 60;
489066
489124
  return `${String(hours)}h ${String(remainingMinutes).padStart(2, "0")}m`;
489067
489125
  }
489126
+ function formatLiveStartedAt(startedAtMs) {
489127
+ const startedAt = new Date(startedAtMs > 0 ? startedAtMs : Date.now());
489128
+ const day = String(startedAt.getDate()).padStart(2, "0");
489129
+ const month = String(startedAt.getMonth() + 1).padStart(2, "0");
489130
+ const hour = String(startedAt.getHours()).padStart(2, "0");
489131
+ const minute = String(startedAt.getMinutes()).padStart(2, "0");
489132
+ return `${day}.${month}. · ${hour}:${minute}`;
489133
+ }
489068
489134
  var ThinkingComponent = class {
489069
489135
  text;
489070
489136
  showMarker;
@@ -489142,14 +489208,15 @@ var ThinkingComponent = class {
489142
489208
  const frame = BLUN_SPINNER_FRAMES[this.spinnerFrame] ?? BLUN_SPINNER_FRAMES[0];
489143
489209
  const spinner = currentTheme.fg("primary", `${frame} `);
489144
489210
  const elapsed = formatLiveElapsed(this.formatElapsedSeconds());
489211
+ const startedAt = formatLiveStartedAt(this.thinkStartMs ?? Date.now());
489145
489212
  const roundedTokenCount = Math.max(0, Math.round(this.estimatedOutputTokens));
489146
489213
  const tokenCount = formatLiveTokenCount(roundedTokenCount);
489147
489214
  const abortMark = this.thinkAborted ? ` (${uiText("thinking.aborted")})` : "";
489148
489215
  const tokenKey = roundedTokenCount === 1 ? "thinking.tokens.one" : "thinking.tokens.other";
489149
489216
  const thinkingLabel = uiText("thinking.label", { name: this.persona });
489150
- const fullLabel = `${thinkingLabel}… (${elapsed} · ↓ ~${uiText(tokenKey, { count: tokenCount })})${abortMark}`;
489151
- const compactLabel = `${thinkingLabel}… (${elapsed} · ↓${tokenCount})${abortMark}`;
489152
- const metricsOnly = `(${elapsed} · ↓${tokenCount})${abortMark}`;
489217
+ const fullLabel = `${thinkingLabel}… (${elapsed} · ↓ ~${uiText(tokenKey, { count: tokenCount })} · ${startedAt})${abortMark}`;
489218
+ const compactLabel = `${thinkingLabel}… (${elapsed} · ↓${tokenCount} · ${startedAt})${abortMark}`;
489219
+ const metricsOnly = `(${elapsed} · ${startedAt})${abortMark}`;
489153
489220
  const availableWidth = Math.max(1, width - visibleWidth(spinner));
489154
489221
  const label = visibleWidth(fullLabel) <= availableWidth ? fullLabel : visibleWidth(compactLabel) <= availableWidth ? compactLabel : metricsOnly;
489155
489222
  rendered = ["", spinner + currentTheme.fg("textDim", truncateToWidth(label, availableWidth))];
@@ -500355,7 +500422,8 @@ var BtwPanelComponent = class {
500355
500422
  const outputTokens = estimateLiveOutputTokens(turn.thinking + turn.answer);
500356
500423
  const frame = BLUN_SPINNER_FRAMES[this.spinnerFrame] ?? BLUN_SPINNER_FRAMES[0];
500357
500424
  const label = uiText("blunTui.activity.thinking", { name: "BTW" });
500358
- const metrics = `(${formatLiveElapsed(elapsedSeconds)} · ↓ ~${formatLiveTokenCount(outputTokens)} ${uiText("blunTui.activity.tokens")})`;
500425
+ const startedAt = formatLiveStartedAt(turn.startedAtMs);
500426
+ const metrics = `(${formatLiveElapsed(elapsedSeconds)} · ↓ ~${formatLiveTokenCount(outputTokens)} ${uiText("blunTui.activity.tokens")} · ${startedAt})`;
500359
500427
  return chalk.hex(currentTheme.palette.accent)(`${frame} `) + chalk.hex(currentTheme.palette.accent).bold(label) + " " + chalk.hex(currentTheme.palette.text)(metrics);
500360
500428
  }
500361
500429
  startLiveStatusTimer() {
@@ -502143,7 +502211,6 @@ var EditorKeyboardController = class {
502143
502211
  }
502144
502212
  if (host.state.appState.isCompacting) {
502145
502213
  this.clearPendingExit();
502146
- if (this.clearEditorTextIfPresent()) return;
502147
502214
  this.cancelCurrentCompaction();
502148
502215
  return;
502149
502216
  }
@@ -502157,7 +502224,6 @@ var EditorKeyboardController = class {
502157
502224
  }
502158
502225
  if (host.state.appState.streamingPhase !== "idle") {
502159
502226
  this.clearPendingExit();
502160
- if (this.clearEditorTextIfPresent()) return;
502161
502227
  this.cancelCurrentStream();
502162
502228
  return;
502163
502229
  }
@@ -502177,8 +502243,15 @@ var EditorKeyboardController = class {
502177
502243
  }
502178
502244
  this.armPendingExit("ctrl-d", ctrlDHint());
502179
502245
  };
502180
- editor.onEscape = () => {
502246
+ editor.onEscape = (autocompleteCancelled = false) => {
502181
502247
  if (this.pendingExit) this.clearPendingExit();
502248
+ if (host.cancelInFlight !== void 0) {
502249
+ const cancel = host.cancelInFlight;
502250
+ host.cancelInFlight = void 0;
502251
+ this.clearPendingUndoEsc();
502252
+ cancel();
502253
+ return;
502254
+ }
502182
502255
  if (host.state.activeDialog === "session-picker") {
502183
502256
  host.hideSessionPicker();
502184
502257
  this.clearPendingUndoEsc();
@@ -502198,6 +502271,10 @@ var EditorKeyboardController = class {
502198
502271
  this.clearPendingUndoEsc();
502199
502272
  return;
502200
502273
  }
502274
+ if (autocompleteCancelled) {
502275
+ this.clearPendingUndoEsc();
502276
+ return;
502277
+ }
502201
502278
  if (this.pendingUndoEsc !== null) {
502202
502279
  this.clearPendingUndoEsc();
502203
502280
  host.openUndoSelector();
@@ -504257,6 +504334,13 @@ function mcpServerStatusKey(server) {
504257
504334
  server.error
504258
504335
  ]);
504259
504336
  }
504337
+ function mcpStartupInventory(servers) {
504338
+ const connected = servers.filter((server) => server.status === "connected");
504339
+ return {
504340
+ serverCount: connected.length,
504341
+ toolCount: connected.reduce((total, server) => total + Math.max(0, Number(server.toolCount) || 0), 0)
504342
+ };
504343
+ }
504260
504344
  //#endregion
504261
504345
  //#region src/utils/usage/debug-timing.ts
504262
504346
  const MIN_STREAM_MS_FOR_TPS = 50;
@@ -504916,6 +505000,7 @@ var SessionEventHandler = class {
504916
505000
  renderedMcpServerStatusKeys = /* @__PURE__ */ new Map();
504917
505001
  mcpServerStatusSpinners = /* @__PURE__ */ new Map();
504918
505002
  mcpServers = /* @__PURE__ */ new Map();
505003
+ mcpInventoryLogged = false;
504919
505004
  goalCompletionAwaitingClear = false;
504920
505005
  goalCompletionTurnEnded = false;
504921
505006
  currentTurnHasAssistantText = false;
@@ -504935,6 +505020,7 @@ var SessionEventHandler = class {
504935
505020
  this.renderedPluginCommandActivationIds.clear();
504936
505021
  this.renderedMcpServerStatusKeys.clear();
504937
505022
  this.mcpServers.clear();
505023
+ this.mcpInventoryLogged = false;
504938
505024
  this.goalCompletionAwaitingClear = false;
504939
505025
  this.goalCompletionTurnEnded = false;
504940
505026
  this.currentTurnHasAssistantText = false;
@@ -504986,6 +505072,7 @@ var SessionEventHandler = class {
504986
505072
  }
504987
505073
  if (host.session !== session || host.state.appState.sessionId !== session.id) return;
504988
505074
  const startupServers = servers.filter((server) => !isManagedTelegramMissingTokenFailure(server));
505075
+ this.logMcpInventoryIfReady(startupServers);
504989
505076
  const visible = selectMcpStartupStatusRows(startupServers);
504990
505077
  const visibleNames = new Set(visible.map((server) => server.name));
504991
505078
  for (const server of visible) {
@@ -505656,6 +505743,7 @@ var SessionEventHandler = class {
505656
505743
  this.mcpServers.set(server.name, server);
505657
505744
  const summary = formatMcpStartupStatusSummary([...this.mcpServers.values()]);
505658
505745
  this.host.setAppState({ mcpServersSummary: summary || null });
505746
+ this.logMcpInventoryIfReady([...this.mcpServers.values()]);
505659
505747
  switch (server.status) {
505660
505748
  case "connected": {
505661
505749
  const tools = uiText(server.toolCount === 1 ? "sessionEvent.mcp.tool.one" : "sessionEvent.mcp.tool.other", { count: server.toolCount });
@@ -505688,6 +505776,12 @@ var SessionEventHandler = class {
505688
505776
  return;
505689
505777
  }
505690
505778
  }
505779
+ logMcpInventoryIfReady(servers) {
505780
+ if (this.mcpInventoryLogged || servers.some((server) => server.status === "pending")) return;
505781
+ const inventory = mcpStartupInventory(servers);
505782
+ log.info("mcp startup inventory", inventory);
505783
+ this.mcpInventoryLogged = true;
505784
+ }
505691
505785
  showMcpServerStatusSpinner(name) {
505692
505786
  const { state } = this.host;
505693
505787
  const label = uiText("sessionEvent.mcp.connecting", { name });
@@ -507601,7 +507695,7 @@ function formatGoalBadge(goal, colors, wallClockMs) {
507601
507695
  const label = `${uiText(`goal.panel.status.${goal.status}`)} · ${formatBadgeElapsed(wallClockMs ?? goal.wallClockMs)} · ${turns}`;
507602
507696
  return chalk.hex(colors.textMuted)(`[${uiText("footer.goal.label")} `) + chalk.hex(dotColor)("●") + chalk.hex(colors.textMuted)(` ${label}]`);
507603
507697
  }
507604
- function formatLoopBadge(loop, colors, nowMs = Date.now()) {
507698
+ function formatLoopChatIndicator(loop, colors, nowMs = Date.now()) {
507605
507699
  if (loop === null || loop === void 0 || loop.status !== "active" || !Number.isFinite(loop.nextFireAt)) return null;
507606
507700
  const remainingMs = Math.max(0, loop.nextFireAt - nowMs);
507607
507701
  let value;
@@ -507623,7 +507717,7 @@ function formatLoopBadge(loop, colors, nowMs = Date.now()) {
507623
507717
  numeric: "always",
507624
507718
  style: "short"
507625
507719
  }).format(value, unit);
507626
- return chalk.hex(colors.textMuted)(`[/loop · ${relative}]`);
507720
+ return chalk.hex(colors.primary)("\u25cf") + chalk.hex(colors.textMuted)(` Loop ${relative}`);
507627
507721
  }
507628
507722
  function formatBadgeElapsed(ms) {
507629
507723
  const totalSeconds = Math.round(ms / 1e3);
@@ -507794,6 +507888,41 @@ function formatFooterGitBadge(status, colors) {
507794
507888
  if (status.pullRequest === null) return base;
507795
507889
  return `${base} ${chalk.hex(colors.primary)(formatPullRequestBadge(status.pullRequest, { linkPullRequest: true }))}`;
507796
507890
  }
507891
+ var LoopChatIndicatorComponent = class {
507892
+ state;
507893
+ onRefresh;
507894
+ loopTimer = null;
507895
+ constructor(state, onRefresh = () => {}) {
507896
+ this.state = state;
507897
+ this.onRefresh = onRefresh;
507898
+ this.syncTimer(state.loop);
507899
+ }
507900
+ setState(state) {
507901
+ this.state = state;
507902
+ this.syncTimer(state.loop);
507903
+ }
507904
+ render(width) {
507905
+ const indicator = formatLoopChatIndicator(this.state.loop, currentTheme.palette);
507906
+ return indicator === null ? [] : [truncateToWidth(indicator, width, "…")];
507907
+ }
507908
+ syncTimer(loop) {
507909
+ if (loop?.status === "active" && Number.isFinite(loop.nextFireAt)) {
507910
+ if (this.loopTimer !== null) return;
507911
+ this.loopTimer = setInterval(() => {
507912
+ this.onRefresh();
507913
+ }, GOAL_TIMER_INTERVAL_MS);
507914
+ this.loopTimer.unref?.();
507915
+ return;
507916
+ }
507917
+ this.dispose();
507918
+ }
507919
+ dispose() {
507920
+ if (this.loopTimer !== null) {
507921
+ clearInterval(this.loopTimer);
507922
+ this.loopTimer = null;
507923
+ }
507924
+ }
507925
+ };
507797
507926
  var FooterComponent = class {
507798
507927
  state;
507799
507928
  onRefresh;
@@ -507803,7 +507932,6 @@ var FooterComponent = class {
507803
507932
  goalSnapshotKey = null;
507804
507933
  goalObservedAtMs = Date.now();
507805
507934
  goalTimer = null;
507806
- loopTimer = null;
507807
507935
  compactionAttemptStartedAtMs = null;
507808
507936
  compactionEstimatedInputTokens;
507809
507937
  compactionProgress;
@@ -507825,7 +507953,6 @@ var FooterComponent = class {
507825
507953
  this.gitCache = createGitStatusCache(state.workDir, { onChange: this.onRefresh });
507826
507954
  this.syncGoalClock(state.goal);
507827
507955
  this.syncGoalTimer(state.goal);
507828
- this.syncLoopTimer(state.loop);
507829
507956
  if (state.isCompacting) this.startCompaction();
507830
507957
  }
507831
507958
  setState(state) {
@@ -507835,7 +507962,6 @@ var FooterComponent = class {
507835
507962
  }
507836
507963
  this.syncGoalClock(state.goal);
507837
507964
  this.syncGoalTimer(state.goal);
507838
- this.syncLoopTimer(state.loop);
507839
507965
  this.state = state;
507840
507966
  if (state.isCompacting && this.compactionAttemptStartedAtMs === null) this.startCompaction();
507841
507967
  else if (!state.isCompacting) this.finishCompaction();
@@ -507902,13 +508028,13 @@ var FooterComponent = class {
507902
508028
  if (modes.length > 0) left.push(modes.join(" "));
507903
508029
  const goalBadge = formatGoalBadge(state.goal, colors, this.goalWallClockMs(state.goal));
507904
508030
  if (goalBadge !== null) left.push(goalBadge);
507905
- const loopBadge = formatLoopBadge(state.loop, colors);
507906
- if (loopBadge !== null) left.push(loopBadge);
507907
508031
  const model = modelDisplayName(state);
507908
508032
  if (model) {
507909
508033
  const sep = chalk.hex(colors.textMuted)(" · ");
507910
508034
  const persona = personaName();
507911
- const brand = chalk.hex(colors.primary).bold("● BLUN") + sep + (persona !== void 0 ? chalk.hex(colors.text)(persona) + sep : "");
508035
+ const profileLabel = process.env["BLUN_PROFILE"] ?? "default";
508036
+ 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 : "");
507912
508038
  const modelLabel = model;
507913
508039
  const modeLabel = uiText(state.permissionMode === "yolo" ? "footer.mode.god" : state.permissionMode === "auto" ? "footer.mode.auto" : "footer.mode.manual");
507914
508040
  const modeSuffix = sep + chalk.hex(colors.textDim)(uiText("footer.mode", { mode: modeLabel }));
@@ -507968,29 +508094,11 @@ var FooterComponent = class {
507968
508094
  this.goalTimer = null;
507969
508095
  }
507970
508096
  }
507971
- syncLoopTimer(loop) {
507972
- if (loop?.status === "active" && Number.isFinite(loop.nextFireAt)) {
507973
- if (this.loopTimer !== null) return;
507974
- this.loopTimer = setInterval(() => {
507975
- this.onRefresh();
507976
- }, GOAL_TIMER_INTERVAL_MS);
507977
- this.loopTimer.unref?.();
507978
- return;
507979
- }
507980
- if (this.loopTimer !== null) {
507981
- clearInterval(this.loopTimer);
507982
- this.loopTimer = null;
507983
- }
507984
- }
507985
508097
  dispose() {
507986
508098
  if (this.goalTimer !== null) {
507987
508099
  clearInterval(this.goalTimer);
507988
508100
  this.goalTimer = null;
507989
508101
  }
507990
- if (this.loopTimer !== null) {
507991
- clearInterval(this.loopTimer);
507992
- this.loopTimer = null;
507993
- }
507994
508102
  this.finishCompaction();
507995
508103
  }
507996
508104
  goalWallClockMs(goal) {
@@ -511361,11 +511469,9 @@ var CustomEditor = class extends Editor {
511361
511469
  }
511362
511470
  }
511363
511471
  if (matchesKey(normalized, Key.escape)) {
511364
- if (this.hasAutocompleteActivity()) {
511365
- this.cancelAutocompleteActivity();
511366
- return;
511367
- }
511368
- this.onEscape?.();
511472
+ const autocompleteCancelled = this.hasAutocompleteActivity();
511473
+ if (autocompleteCancelled) this.cancelAutocompleteActivity();
511474
+ this.onEscape?.(autocompleteCancelled);
511369
511475
  return;
511370
511476
  }
511371
511477
  if (matchesKey(normalized, Key.tab) && !this.isShowingAutocomplete()) return;
@@ -511736,6 +511842,11 @@ function createTUIState(options) {
511736
511842
  const quotaWarningContainer = new GutterContainer(1, 1);
511737
511843
  const quotaWarning = new ManagedQuotaWarningComponent();
511738
511844
  quotaWarningContainer.addChild(quotaWarning);
511845
+ const loopIndicatorContainer = new GutterContainer(1, 1);
511846
+ const loopIndicator = new LoopChatIndicatorComponent(appState, () => {
511847
+ ui.requestRender();
511848
+ });
511849
+ loopIndicatorContainer.addChild(loopIndicator);
511739
511850
  return {
511740
511851
  ui,
511741
511852
  terminal,
@@ -511747,6 +511858,8 @@ function createTUIState(options) {
511747
511858
  btwPanelContainer,
511748
511859
  quotaWarningContainer,
511749
511860
  quotaWarning,
511861
+ loopIndicatorContainer,
511862
+ loopIndicator,
511750
511863
  editorContainer: new GutterContainer(1, 1),
511751
511864
  editor: new GhostSuggestEditor(ui, {
511752
511865
  disablePasteBurst: initialAppState.disablePasteBurst ?? DEFAULT_TUI_CONFIG.disablePasteBurst,
@@ -513624,6 +513737,7 @@ var BlunTUI = class {
513624
513737
  this.editorKeyboard.dispose();
513625
513738
  this.scrollbackController.dispose();
513626
513739
  this.managedQuotaWarningController.dispose();
513740
+ this.state.loopIndicator.dispose();
513627
513741
  this.state.footer.dispose();
513628
513742
  for (const dispose of this.reverseRpcDisposers) dispose();
513629
513743
  this.reverseRpcDisposers.length = 0;
@@ -513704,6 +513818,7 @@ var BlunTUI = class {
513704
513818
  ui.addChild(this.state.queueContainer);
513705
513819
  ui.addChild(this.state.quotaWarningContainer);
513706
513820
  ui.addChild(this.state.btwPanelContainer);
513821
+ ui.addChild(this.state.loopIndicatorContainer);
513707
513822
  ui.addChild(this.state.editorContainer);
513708
513823
  if (ui instanceof BottomPinnedTUI) {
513709
513824
  ui.pinFromIndex = ui.children.indexOf(this.state.quotaWarningContainer);
@@ -514686,6 +514801,7 @@ var BlunTUI = class {
514686
514801
  const busyChanged = "streamingPhase" in effectivePatch || "isCompacting" in effectivePatch;
514687
514802
  Object.assign(this.state.appState, effectivePatch);
514688
514803
  if ("planMode" in effectivePatch) this.updateEditorBorderHighlight();
514804
+ this.state.loopIndicator.setState(this.state.appState);
514689
514805
  this.state.footer.setState(this.state.appState);
514690
514806
  if ("sessionId" in effectivePatch || "managedQuotaWindows" in effectivePatch) this.managedQuotaWarningController.update(this.state.appState.sessionId, this.state.appState.managedQuotaWindows);
514691
514807
  this.updateActivityPane();
@@ -515707,10 +515823,11 @@ var BlunTUI = class {
515707
515823
  const startedAtMs = metrics.startedAtMs ?? this.activitySpinnerFallbackStartMs;
515708
515824
  const elapsed = formatLiveElapsed(startedAtMs > 0 ? Math.max(0, Math.floor((Date.now() - startedAtMs) / 1e3)) : 0);
515709
515825
  const tokens = formatLiveTokenCount(metrics.estimatedOutputTokens);
515826
+ const startedAt = formatLiveStartedAt(startedAtMs);
515710
515827
  return {
515711
- full: `${this.activitySpinnerBaseLabel} (${elapsed} · ↓ ~${tokens} ${uiText("blunTui.activity.tokens")})`,
515712
- compact: `${this.activitySpinnerBaseLabel} (${elapsed} · ↓${tokens})`,
515713
- minimal: `(${elapsed} · ↓${tokens})`
515828
+ full: `${this.activitySpinnerBaseLabel} (${elapsed} · ↓ ~${tokens} ${uiText("blunTui.activity.tokens")} · ${startedAt})`,
515829
+ compact: `${this.activitySpinnerBaseLabel} (${elapsed} · ↓${tokens} · ${startedAt})`,
515830
+ minimal: `(${elapsed} · ${startedAt})`
515714
515831
  };
515715
515832
  }
515716
515833
  ensureActivitySpinner(style, label = "", colorFn) {
@@ -516860,7 +516977,7 @@ async function appendRolloutDecisionLog(entry, filePath = getUpdateRolloutLogFil
516860
516977
  * creates the telemetry device_id before telemetry can emit first_launch.
516861
516978
  */
516862
516979
  function resolveUpdateDeviceId() {
516863
- return readBlunDeviceId(resolveBlunHome$1()) ?? randomUUID();
516980
+ return readBlunDeviceId(resolveSharedBlunHome(resolveBlunHome$1())) ?? randomUUID();
516864
516981
  }
516865
516982
  /**
516866
516983
  * The experimental master switch opts a device out of staged rollouts: the
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "blun-king-cli",
3
- "version": "9.1.58",
3
+ "version": "9.1.60",
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": {
@@ -11,8 +11,8 @@
11
11
  "command": "npx",
12
12
  "args": ["-y", "agent-browser@0.31.2", "mcp"],
13
13
  "enabled": false,
14
- "startupTimeoutMs": 30000,
15
- "toolTimeoutMs": 60000
14
+ "startupTimeoutMs": 120000,
15
+ "toolTimeoutMs": 120000
16
16
  }
17
17
  },
18
18
  "blun-language-guard": {
@@ -28,7 +28,7 @@
28
28
  "inheritEnv": false,
29
29
  "enabled": false,
30
30
  "startupTimeoutMs": 20000,
31
- "toolTimeoutMs": 60000
31
+ "toolTimeoutMs": 120000
32
32
  }
33
33
  },
34
34
  "telegram": {