blun-king-cli 9.1.58 → 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/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();
@@ -503022,8 +503099,8 @@ registerUiCatalogFragment({
503022
503099
  * 0 = most recent clipped lines visible
503023
503100
  * N = N lines up from the bottom
503024
503101
  */
503025
- /** Maximum lines retained before the oldest are evicted. */
503026
- const MAX_BUFFER_LINES = 1e4;
503102
+ /** Retain the complete rendered session unless the process itself ends. */
503103
+ const MAX_BUFFER_LINES = Number.POSITIVE_INFINITY;
503027
503104
  var ScrollbackBuffer = class {
503028
503105
  lines = [];
503029
503106
  scrollOffset = 0;
@@ -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) {
@@ -505177,6 +505264,7 @@ var SessionEventHandler = class {
505177
505264
  this.goalCompletionTurnEnded = true;
505178
505265
  this.scheduleQueuedGoalPromotion();
505179
505266
  if (event.reason === "completed") this.host.refreshManagedQuotaWindows();
505267
+ requestRunningUpdateAtSafeBoundary(this.host);
505180
505268
  }
505181
505269
  handleStepBegin(event) {
505182
505270
  this.host.commitQueuedSteerAtStepStart(event.turnId);
@@ -505224,12 +505312,13 @@ var SessionEventHandler = class {
505224
505312
  const duration = durationMs === void 0 ? "—" : formatResponseDuration(durationMs);
505225
505313
  const tokens = this.currentTurnTokenCount === void 0 ? "—" : formatTokenCount(this.currentTurnTokenCount);
505226
505314
  const tokenKey = this.currentTurnTokenCount === 1 ? "thinking.tokens.one" : "thinking.tokens.other";
505315
+ const startedAt = formatLiveStartedAt(this.currentTurnStartedAtMs ?? Date.now());
505227
505316
  this.host.appendTranscriptEntry({
505228
505317
  id: nextTranscriptId(),
505229
505318
  kind: "status",
505230
505319
  turnId: String(event.turnId),
505231
505320
  renderMode: "plain",
505232
- content: `${duration} · ${uiText(tokenKey, { count: tokens })}`
505321
+ content: `${duration} · ${uiText(tokenKey, { count: tokens })} · ${startedAt}`
505233
505322
  });
505234
505323
  }
505235
505324
  markActiveAgentSwarmsCancelled() {
@@ -505656,6 +505745,7 @@ var SessionEventHandler = class {
505656
505745
  this.mcpServers.set(server.name, server);
505657
505746
  const summary = formatMcpStartupStatusSummary([...this.mcpServers.values()]);
505658
505747
  this.host.setAppState({ mcpServersSummary: summary || null });
505748
+ this.logMcpInventoryIfReady([...this.mcpServers.values()]);
505659
505749
  switch (server.status) {
505660
505750
  case "connected": {
505661
505751
  const tools = uiText(server.toolCount === 1 ? "sessionEvent.mcp.tool.one" : "sessionEvent.mcp.tool.other", { count: server.toolCount });
@@ -505688,6 +505778,12 @@ var SessionEventHandler = class {
505688
505778
  return;
505689
505779
  }
505690
505780
  }
505781
+ logMcpInventoryIfReady(servers) {
505782
+ if (this.mcpInventoryLogged || servers.some((server) => server.status === "pending")) return;
505783
+ const inventory = mcpStartupInventory(servers);
505784
+ log.info("mcp startup inventory", inventory);
505785
+ this.mcpInventoryLogged = true;
505786
+ }
505691
505787
  showMcpServerStatusSpinner(name) {
505692
505788
  const { state } = this.host;
505693
505789
  const label = uiText("sessionEvent.mcp.connecting", { name });
@@ -506203,12 +506299,6 @@ function createReplayRenderContext() {
506203
506299
  suppressNextPlanModeOffNotice: false
506204
506300
  };
506205
506301
  }
506206
- function limitReplayRecordsByTurn(records, maxTurns) {
506207
- if (maxTurns <= 0) return [];
506208
- const turnStarts = records.flatMap((record, index) => isReplayUserTurnRecord(record) ? [index] : []);
506209
- if (turnStarts.length <= maxTurns) return records;
506210
- return records.slice(turnStarts[turnStarts.length - maxTurns]);
506211
- }
506212
506302
  function replayEntry(context, kind, content, renderMode, extras = {}) {
506213
506303
  return {
506214
506304
  id: nextTranscriptId(),
@@ -506536,7 +506626,7 @@ var SessionReplayRenderer = class {
506536
506626
  }
506537
506627
  renderRecords(agent) {
506538
506628
  const context = createReplayRenderContext();
506539
- for (const record of limitReplayRecordsByTurn(agent.replay, 10)) this.renderRecord(context, record);
506629
+ for (const record of agent.replay) this.renderRecord(context, record);
506540
506630
  this.flushAssistant(context);
506541
506631
  this.cleanupRuntime(context);
506542
506632
  }
@@ -507601,7 +507691,7 @@ function formatGoalBadge(goal, colors, wallClockMs) {
507601
507691
  const label = `${uiText(`goal.panel.status.${goal.status}`)} · ${formatBadgeElapsed(wallClockMs ?? goal.wallClockMs)} · ${turns}`;
507602
507692
  return chalk.hex(colors.textMuted)(`[${uiText("footer.goal.label")} `) + chalk.hex(dotColor)("●") + chalk.hex(colors.textMuted)(` ${label}]`);
507603
507693
  }
507604
- function formatLoopBadge(loop, colors, nowMs = Date.now()) {
507694
+ function formatLoopChatIndicator(loop, colors, nowMs = Date.now()) {
507605
507695
  if (loop === null || loop === void 0 || loop.status !== "active" || !Number.isFinite(loop.nextFireAt)) return null;
507606
507696
  const remainingMs = Math.max(0, loop.nextFireAt - nowMs);
507607
507697
  let value;
@@ -507623,7 +507713,7 @@ function formatLoopBadge(loop, colors, nowMs = Date.now()) {
507623
507713
  numeric: "always",
507624
507714
  style: "short"
507625
507715
  }).format(value, unit);
507626
- return chalk.hex(colors.textMuted)(`[/loop · ${relative}]`);
507716
+ return chalk.hex(colors.primary)("\u25cf") + chalk.hex(colors.textMuted)(` Loop ${relative}`);
507627
507717
  }
507628
507718
  function formatBadgeElapsed(ms) {
507629
507719
  const totalSeconds = Math.round(ms / 1e3);
@@ -507794,6 +507884,41 @@ function formatFooterGitBadge(status, colors) {
507794
507884
  if (status.pullRequest === null) return base;
507795
507885
  return `${base} ${chalk.hex(colors.primary)(formatPullRequestBadge(status.pullRequest, { linkPullRequest: true }))}`;
507796
507886
  }
507887
+ var LoopChatIndicatorComponent = class {
507888
+ state;
507889
+ onRefresh;
507890
+ loopTimer = null;
507891
+ constructor(state, onRefresh = () => {}) {
507892
+ this.state = state;
507893
+ this.onRefresh = onRefresh;
507894
+ this.syncTimer(state.loop);
507895
+ }
507896
+ setState(state) {
507897
+ this.state = state;
507898
+ this.syncTimer(state.loop);
507899
+ }
507900
+ render(width) {
507901
+ const indicator = formatLoopChatIndicator(this.state.loop, currentTheme.palette);
507902
+ return indicator === null ? [] : [truncateToWidth(indicator, width, "…")];
507903
+ }
507904
+ syncTimer(loop) {
507905
+ if (loop?.status === "active" && Number.isFinite(loop.nextFireAt)) {
507906
+ if (this.loopTimer !== null) return;
507907
+ this.loopTimer = setInterval(() => {
507908
+ this.onRefresh();
507909
+ }, GOAL_TIMER_INTERVAL_MS);
507910
+ this.loopTimer.unref?.();
507911
+ return;
507912
+ }
507913
+ this.dispose();
507914
+ }
507915
+ dispose() {
507916
+ if (this.loopTimer !== null) {
507917
+ clearInterval(this.loopTimer);
507918
+ this.loopTimer = null;
507919
+ }
507920
+ }
507921
+ };
507797
507922
  var FooterComponent = class {
507798
507923
  state;
507799
507924
  onRefresh;
@@ -507803,7 +507928,6 @@ var FooterComponent = class {
507803
507928
  goalSnapshotKey = null;
507804
507929
  goalObservedAtMs = Date.now();
507805
507930
  goalTimer = null;
507806
- loopTimer = null;
507807
507931
  compactionAttemptStartedAtMs = null;
507808
507932
  compactionEstimatedInputTokens;
507809
507933
  compactionProgress;
@@ -507825,7 +507949,6 @@ var FooterComponent = class {
507825
507949
  this.gitCache = createGitStatusCache(state.workDir, { onChange: this.onRefresh });
507826
507950
  this.syncGoalClock(state.goal);
507827
507951
  this.syncGoalTimer(state.goal);
507828
- this.syncLoopTimer(state.loop);
507829
507952
  if (state.isCompacting) this.startCompaction();
507830
507953
  }
507831
507954
  setState(state) {
@@ -507835,7 +507958,6 @@ var FooterComponent = class {
507835
507958
  }
507836
507959
  this.syncGoalClock(state.goal);
507837
507960
  this.syncGoalTimer(state.goal);
507838
- this.syncLoopTimer(state.loop);
507839
507961
  this.state = state;
507840
507962
  if (state.isCompacting && this.compactionAttemptStartedAtMs === null) this.startCompaction();
507841
507963
  else if (!state.isCompacting) this.finishCompaction();
@@ -507902,13 +508024,13 @@ var FooterComponent = class {
507902
508024
  if (modes.length > 0) left.push(modes.join(" "));
507903
508025
  const goalBadge = formatGoalBadge(state.goal, colors, this.goalWallClockMs(state.goal));
507904
508026
  if (goalBadge !== null) left.push(goalBadge);
507905
- const loopBadge = formatLoopBadge(state.loop, colors);
507906
- if (loopBadge !== null) left.push(loopBadge);
507907
508027
  const model = modelDisplayName(state);
507908
508028
  if (model) {
507909
508029
  const sep = chalk.hex(colors.textMuted)(" · ");
507910
508030
  const persona = personaName();
507911
- const brand = chalk.hex(colors.primary).bold("● BLUN") + sep + (persona !== void 0 ? chalk.hex(colors.text)(persona) + sep : "");
508031
+ const profileLabel = process.env["BLUN_PROFILE"] ?? "default";
508032
+ const profile = chalk.hex(colors.textDim)(profileLabel) + 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 : "");
507912
508034
  const modelLabel = model;
507913
508035
  const modeLabel = uiText(state.permissionMode === "yolo" ? "footer.mode.god" : state.permissionMode === "auto" ? "footer.mode.auto" : "footer.mode.manual");
507914
508036
  const modeSuffix = sep + chalk.hex(colors.textDim)(uiText("footer.mode", { mode: modeLabel }));
@@ -507968,29 +508090,11 @@ var FooterComponent = class {
507968
508090
  this.goalTimer = null;
507969
508091
  }
507970
508092
  }
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
508093
  dispose() {
507986
508094
  if (this.goalTimer !== null) {
507987
508095
  clearInterval(this.goalTimer);
507988
508096
  this.goalTimer = null;
507989
508097
  }
507990
- if (this.loopTimer !== null) {
507991
- clearInterval(this.loopTimer);
507992
- this.loopTimer = null;
507993
- }
507994
508098
  this.finishCompaction();
507995
508099
  }
507996
508100
  goalWallClockMs(goal) {
@@ -511288,12 +511392,18 @@ var CustomEditor = class extends Editor {
511288
511392
  if (isKeyRelease(normalized)) return;
511289
511393
  if (!matchesKey(normalized, Key.escape)) this.onNonEscapeInput?.();
511290
511394
  if (this.consumingPaste) {
511291
- this.consumeBuffer += normalized;
511292
- if (this.consumeBuffer.includes(BRACKET_PASTE_END)) {
511395
+ const cancelPaste = matchesKey(normalized, Key.ctrl("c")) || matchesKey(normalized, Key.escape);
511396
+ if (cancelPaste) {
511293
511397
  this.consumingPaste = false;
511294
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;
511295
511406
  }
511296
- return;
511297
511407
  }
511298
511408
  if (normalized.includes(BRACKET_PASTE_START) && this.expandPasteMarkerAtCursor()) {
511299
511409
  if (!normalized.includes(BRACKET_PASTE_END)) this.consumingPaste = true;
@@ -511348,6 +511458,7 @@ var CustomEditor = class extends Editor {
511348
511458
  if (this.inputMode === "bash" && this.getText().length === 0 && (matchesKey(normalized, Key.escape) || matchesKey(normalized, Key.backspace))) {
511349
511459
  this.inputMode = "prompt";
511350
511460
  this.onInputModeChange?.("prompt");
511461
+ if (matchesKey(normalized, Key.escape)) this.onEscape?.(true);
511351
511462
  return;
511352
511463
  }
511353
511464
  if (matchesKey(normalized, Key.up)) {
@@ -511361,11 +511472,9 @@ var CustomEditor = class extends Editor {
511361
511472
  }
511362
511473
  }
511363
511474
  if (matchesKey(normalized, Key.escape)) {
511364
- if (this.hasAutocompleteActivity()) {
511365
- this.cancelAutocompleteActivity();
511366
- return;
511367
- }
511368
- this.onEscape?.();
511475
+ const autocompleteCancelled = this.hasAutocompleteActivity();
511476
+ if (autocompleteCancelled) this.cancelAutocompleteActivity();
511477
+ this.onEscape?.(autocompleteCancelled);
511369
511478
  return;
511370
511479
  }
511371
511480
  if (matchesKey(normalized, Key.tab) && !this.isShowingAutocomplete()) return;
@@ -511636,6 +511745,7 @@ var GhostSuggestEditor = class extends CustomEditor {
511636
511745
  if (matchesKey(normalized, Key.escape)) {
511637
511746
  this.ghostSuggestion = null;
511638
511747
  this.onGhostDismissed?.();
511748
+ this.onEscape?.(true);
511639
511749
  return;
511640
511750
  }
511641
511751
  if (this.isCursorAtTextEnd() && !this.isShowingAutocomplete()) {
@@ -511736,6 +511846,11 @@ function createTUIState(options) {
511736
511846
  const quotaWarningContainer = new GutterContainer(1, 1);
511737
511847
  const quotaWarning = new ManagedQuotaWarningComponent();
511738
511848
  quotaWarningContainer.addChild(quotaWarning);
511849
+ const loopIndicatorContainer = new GutterContainer(1, 1);
511850
+ const loopIndicator = new LoopChatIndicatorComponent(appState, () => {
511851
+ ui.requestRender();
511852
+ });
511853
+ loopIndicatorContainer.addChild(loopIndicator);
511739
511854
  return {
511740
511855
  ui,
511741
511856
  terminal,
@@ -511747,6 +511862,8 @@ function createTUIState(options) {
511747
511862
  btwPanelContainer,
511748
511863
  quotaWarningContainer,
511749
511864
  quotaWarning,
511865
+ loopIndicatorContainer,
511866
+ loopIndicator,
511750
511867
  editorContainer: new GutterContainer(1, 1),
511751
511868
  editor: new GhostSuggestEditor(ui, {
511752
511869
  disablePasteBurst: initialAppState.disablePasteBurst ?? DEFAULT_TUI_CONFIG.disablePasteBurst,
@@ -512936,13 +513053,13 @@ function readEnvInt(name, fallback) {
512936
513053
  return value;
512937
513054
  }
512938
513055
  /** Keep the most recent N turns. `0` disables trimming. */
512939
- const TRANSCRIPT_MAX_TURNS = readEnvInt("BLUN_TUI_MAX_TURNS", 15);
513056
+ const TRANSCRIPT_MAX_TURNS = readEnvInt("BLUN_TUI_MAX_TURNS", 0);
512940
513057
  /** Only the most recent E turns are allowed to expand (Ctrl+O). `0` disables expanding. */
512941
513058
  const TRANSCRIPT_EXPAND_TURNS = readEnvInt("BLUN_TUI_EXPAND_TURNS", 3);
512942
513059
  /** Only trim once the window exceeds maxTurns by this much (avoids churn). */
512943
513060
  const TRANSCRIPT_HYSTERESIS = readEnvInt("BLUN_TUI_HYSTERESIS", 5);
512944
513061
  /** Keep this many recent steps untouched inside a turn; older steps are merged into a summary. `0` disables merging. */
512945
- 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);
512946
513063
  /**
512947
513064
  * Group consecutive entries into turns by `turnId`. Entries with the same
512948
513065
  * non-undefined `turnId` that are adjacent belong to the same turn.
@@ -512999,6 +513116,60 @@ function turnsToTrim(turns, maxTurns, hysteresis) {
512999
513116
  }
513000
513117
  //#endregion
513001
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
+ }
513002
513173
  function loadingTipKind(mode) {
513003
513174
  if (mode === "waiting" || mode === "tool") return "blun";
513004
513175
  if (mode === "composing") return "composing";
@@ -513083,6 +513254,9 @@ var BlunTUI = class {
513083
513254
  startupLoginRequired = false;
513084
513255
  startupWorkspaceSelectionPending = false;
513085
513256
  startupGoalPromptedSessionId;
513257
+ runningUpdatePreparedVersion;
513258
+ runningUpdateHandoffStarted = false;
513259
+ runningUpdateMessageDispose;
513086
513260
  startupPhaseMs = {};
513087
513261
  lastActivityMode;
513088
513262
  currentLoadingTip = void 0;
@@ -513196,6 +513370,7 @@ var BlunTUI = class {
513196
513370
  this.editorKeyboard.install();
513197
513371
  this.scrollbackController = new ScrollbackController(this.state);
513198
513372
  this.scrollbackController.install();
513373
+ this.runningUpdateMessageDispose = installRunningUpdateListener(this);
513199
513374
  this.buildLayout();
513200
513375
  }
513201
513376
  getSlashCommands() {
@@ -513624,7 +513799,10 @@ var BlunTUI = class {
513624
513799
  this.editorKeyboard.dispose();
513625
513800
  this.scrollbackController.dispose();
513626
513801
  this.managedQuotaWarningController.dispose();
513802
+ this.state.loopIndicator.dispose();
513627
513803
  this.state.footer.dispose();
513804
+ this.runningUpdateMessageDispose?.();
513805
+ this.runningUpdateMessageDispose = void 0;
513628
513806
  for (const dispose of this.reverseRpcDisposers) dispose();
513629
513807
  this.reverseRpcDisposers.length = 0;
513630
513808
  this.disposeTerminalTracking();
@@ -513704,6 +513882,7 @@ var BlunTUI = class {
513704
513882
  ui.addChild(this.state.queueContainer);
513705
513883
  ui.addChild(this.state.quotaWarningContainer);
513706
513884
  ui.addChild(this.state.btwPanelContainer);
513885
+ ui.addChild(this.state.loopIndicatorContainer);
513707
513886
  ui.addChild(this.state.editorContainer);
513708
513887
  if (ui instanceof BottomPinnedTUI) {
513709
513888
  ui.pinFromIndex = ui.children.indexOf(this.state.quotaWarningContainer);
@@ -513847,6 +514026,7 @@ var BlunTUI = class {
513847
514026
  if (this.shellOutputStreams.size === 0) {
513848
514027
  this.setAppState({ streamingPhase: "idle" });
513849
514028
  this.drainOneQueuedMessage();
514029
+ requestRunningUpdateAtSafeBoundary(this);
513850
514030
  }
513851
514031
  }
513852
514032
  queueDrainTimer;
@@ -514686,6 +514866,7 @@ var BlunTUI = class {
514686
514866
  const busyChanged = "streamingPhase" in effectivePatch || "isCompacting" in effectivePatch;
514687
514867
  Object.assign(this.state.appState, effectivePatch);
514688
514868
  if ("planMode" in effectivePatch) this.updateEditorBorderHighlight();
514869
+ this.state.loopIndicator.setState(this.state.appState);
514689
514870
  this.state.footer.setState(this.state.appState);
514690
514871
  if ("sessionId" in effectivePatch || "managedQuotaWindows" in effectivePatch) this.managedQuotaWarningController.update(this.state.appState.sessionId, this.state.appState.managedQuotaWindows);
514691
514872
  this.updateActivityPane();
@@ -515707,10 +515888,11 @@ var BlunTUI = class {
515707
515888
  const startedAtMs = metrics.startedAtMs ?? this.activitySpinnerFallbackStartMs;
515708
515889
  const elapsed = formatLiveElapsed(startedAtMs > 0 ? Math.max(0, Math.floor((Date.now() - startedAtMs) / 1e3)) : 0);
515709
515890
  const tokens = formatLiveTokenCount(metrics.estimatedOutputTokens);
515891
+ const startedAt = formatLiveStartedAt(startedAtMs);
515710
515892
  return {
515711
- full: `${this.activitySpinnerBaseLabel} (${elapsed} · ↓ ~${tokens} ${uiText("blunTui.activity.tokens")})`,
515712
- compact: `${this.activitySpinnerBaseLabel} (${elapsed} · ↓${tokens})`,
515713
- minimal: `(${elapsed} · ↓${tokens})`
515893
+ full: `${this.activitySpinnerBaseLabel} (${elapsed} · ↓ ~${tokens} ${uiText("blunTui.activity.tokens")} · ${startedAt})`,
515894
+ compact: `${this.activitySpinnerBaseLabel} (${elapsed} · ↓${tokens} · ${startedAt})`,
515895
+ minimal: `(${elapsed} · ${startedAt})`
515714
515896
  };
515715
515897
  }
515716
515898
  ensureActivitySpinner(style, label = "", colorFn) {
@@ -516292,22 +516474,27 @@ async function runShell(opts, version, updateStartupNotice) {
516292
516474
  tui.onExit = async (exitCode = 0) => {
516293
516475
  const sessionId = tui.getCurrentSessionId();
516294
516476
  const hasContent = tui.hasSessionContent();
516477
+ const runningUpdateHandoff = exitCode === RUNNING_UPDATE_HANDOFF_EXIT_CODE;
516295
516478
  setCrashPhase("shutdown");
516296
516479
  trackLifecycle("exit", { duration_ms: Date.now() - startedAt });
516297
516480
  await shutdownTelemetry({ timeoutMs: CLI_SHUTDOWN_TIMEOUT_MS });
516298
516481
  const gutter = " ".repeat(1);
516299
- process.stdout.write(`${gutter}Bye!\n`);
516482
+ if (!runningUpdateHandoff) process.stdout.write(`${gutter}Bye!\n`);
516300
516483
  const hints = [];
516301
- if (sessionId !== "" && hasContent) hints.push(`${gutter}To resume this session: blun -r ${sessionId}`);
516302
- 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)}`);
516303
516486
  if (hints.length > 0) process.stderr.write(`\n${hints.join("\n")}\n`);
516304
516487
  removeCrashHandlers();
516305
516488
  restoreStty();
516489
+ if (process.connected) try {
516490
+ process.disconnect();
516491
+ } catch {}
516306
516492
  process.exit(exitCode);
516307
516493
  };
516308
516494
  try {
516309
516495
  const initStartedAt = Date.now();
516310
516496
  await tui.start();
516497
+ notifyRunningRuntimeReady(tui);
516311
516498
  const initMs = Date.now() - initStartedAt;
516312
516499
  const startupSessionId = tui.getCurrentSessionId();
516313
516500
  const mcpMs = await tui.getStartupMcpMs();
@@ -516860,7 +517047,7 @@ async function appendRolloutDecisionLog(entry, filePath = getUpdateRolloutLogFil
516860
517047
  * creates the telemetry device_id before telemetry can emit first_launch.
516861
517048
  */
516862
517049
  function resolveUpdateDeviceId() {
516863
- return readBlunDeviceId(resolveBlunHome$1()) ?? randomUUID();
517050
+ return readBlunDeviceId(resolveSharedBlunHome(resolveBlunHome$1())) ?? randomUUID();
516864
517051
  }
516865
517052
  /**
516866
517053
  * The experimental master switch opts a device out of staged rollouts: the