blun-king-cli 9.1.569 → 9.1.571

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
@@ -1,5 +1,6 @@
1
1
  #!/usr/bin/env node
2
2
  // BLUN_BUILD_INPUT_SHA256:058cccc03f986fac95f89d198bb56bfa928b0c5117340651158d2f731509129c
3
+ import { SessionQueueDelivery as BlunSessionQueueDelivery, captureSessionQueueBoundary } from "./bin/telegram-session-queue-runtime.mjs";
3
4
  import { fileURLToPath as __cjsShimFileURLToPath } from 'node:url';
4
5
  import { dirname as __cjsShimDirname } from 'node:path';
5
6
  const __filename = __cjsShimFileURLToPath(import.meta.url);
@@ -21351,7 +21352,15 @@ var init_types$14 = __esmMin((() => {}));
21351
21352
  //#region ../../packages/agent-core/src/flags/registry.ts
21352
21353
  var FLAG_DEFINITIONS;
21353
21354
  var init_registry$1 = __esmMin((() => {
21354
- FLAG_DEFINITIONS = [{
21355
+ FLAG_DEFINITIONS = [
21356
+ {
21357
+ id: 'agent_spine_timeline',
21358
+ title: 'AgentSpine session transport',
21359
+ description: 'Pass the current native record source to the enabled AgentSpine plugin.',
21360
+ env: 'BLUN_EXPERIMENTAL_AGENT_SPINE_TIMELINE',
21361
+ default: false,
21362
+ surface: 'core',
21363
+ },{
21355
21364
  id: "vision_reader",
21356
21365
  title: "Vision reader",
21357
21366
  description: "Enrich user images with observations from a configured internal service.",
@@ -231318,10 +231327,9 @@ async function runHook(command, input, options) {
231318
231327
  cwd: options.cwd,
231319
231328
  stdio: "pipe",
231320
231329
  detached: process.platform !== "win32",
231321
- env: options.env ? {
231322
- ...process.env,
231323
- ...options.env
231324
- } : void 0
231330
+ env: options.env || options.runtimeEnv
231331
+ ? overlayRuntimeEnv({ ...process.env, ...options.env }, options.runtimeEnv)
231332
+ : undefined
231325
231333
  });
231326
231334
  } catch (error) {
231327
231335
  return allowResult({ stderr: errorMessage$11(error) });
@@ -231659,28 +231667,44 @@ async prepareToolUse(args = {}) {
231659
231667
  await Promise.allSettled([...this.pendingTriggers]);
231660
231668
  }
231661
231669
  async triggerInner(event, args) {
231662
- const matcherValue = matcherValueText(args.matcherValue);
231663
- const inputData = toHookInputData({
231664
- hookEventName: event,
231665
- sessionId: this.options.sessionId ?? "",
231666
- cwd: this.options.cwd ?? "",
231667
- ...args.inputData
231668
- });
231669
- const matched = this.matchingHooks(event, matcherValue);
231670
- if (matched.length === 0) return [];
231671
- this.emitTriggered(event, matcherValue, matched.length);
231672
- const startedAt = Date.now();
231673
- const results = await createRequire(import.meta.url)("./bin/observer-hooks.cjs").runHookPhases(matched, (hook) => runHook(hook.command, inputData, {
231674
- timeout: hook.timeout ?? DEFAULT_HOOK_TIMEOUT_SECONDS,
231675
- cwd: hook.cwd ?? (this.options.cwd === "" ? void 0 : this.options.cwd),
231676
- env: hook.env,
231677
- signal: args.signal
231678
- }), args.signal);
231679
- for (const message of hookWarningMessages(results)) this.emitWarning(event, message);
231680
- const { action, reason } = aggregateResults(event, results);
231681
- this.emitResolved(event, matcherValue, action, reason, Date.now() - startedAt);
231682
- return results;
231683
- }
231670
+ const matcherValue = matcherValueText(args.matcherValue);
231671
+ const inputData = toHookInputData({
231672
+ hookEventName: event,
231673
+ sessionId: this.options.sessionId ?? '',
231674
+ cwd: this.options.cwd ?? '',
231675
+ ...args.inputData,
231676
+ });
231677
+ const matched = this.matchingHooks(event, matcherValue);
231678
+ if (matched.length === 0)
231679
+ return [];
231680
+ if (event === 'UserPromptSubmit'
231681
+ && !Object.hasOwn(inputData, 'event_id') && !Object.hasOwn(inputData, 'hook_event_id')) {
231682
+ // One logical delivery, shared by every hook; explicit IDs preserve redelivery identity.
231683
+ inputData['event_id'] = releasePromptUUID();
231684
+ }
231685
+ this.emitTriggered(event, matcherValue, matched.length);
231686
+ const startedAt = Date.now();
231687
+ const results = await Promise.all(matched.map(async (hook) => {
231688
+ let runtimeEnv;
231689
+ try {
231690
+ runtimeEnv = this.options.runtimeEnv?.(hook);
231691
+ }
231692
+ catch {
231693
+ // A resolver error may contain private values. Other hooks still run.
231694
+ return { action: 'allow', stderr: 'Hook runtime environment is unavailable.' };
231695
+ }
231696
+ return runHook(hook.command, inputData, {
231697
+ timeout: hook.timeout ?? DEFAULT_HOOK_TIMEOUT_SECONDS,
231698
+ cwd: hook.cwd ?? (this.options.cwd === '' ? undefined : this.options.cwd),
231699
+ env: hook.env,
231700
+ runtimeEnv,
231701
+ signal: args.signal,
231702
+ });
231703
+ }));
231704
+ const { action, reason } = aggregateResults(event, results);
231705
+ this.emitResolved(event, matcherValue, action, reason, Date.now() - startedAt);
231706
+ return results;
231707
+ }
231684
231708
  matchingHooks(event, matcherValue) {
231685
231709
  const seen = /* @__PURE__ */ new Set();
231686
231710
  const matched = [];
@@ -236096,6 +236120,13 @@ var init_records = __esmMin((() => {
236096
236120
  persistence;
236097
236121
  _restoring = null;
236098
236122
  metadataInitialized = false;
236123
+ persistedProtocolVersion;
236124
+ get fileSource() {
236125
+ if (!(this.persistence instanceof FileSystemAgentRecordPersistence)
236126
+ || this.persistedProtocolVersion === undefined)
236127
+ return undefined;
236128
+ return { path: this.persistence.filePath, protocolVersion: this.persistedProtocolVersion };
236129
+ }
236099
236130
  resumeCheckpointRestored = false;
236100
236131
  messageTimes = new WeakMap();
236101
236132
  constructor(agent, persistence) {
@@ -236119,8 +236150,15 @@ var init_records = __esmMin((() => {
236119
236150
  created_at: Date.now()
236120
236151
  });
236121
236152
  this.metadataInitialized = true;
236122
- }
236123
- if (stamped.type === "metadata") this.metadataInitialized = true;
236153
+
236154
+ this.persistedProtocolVersion = "1.4";
236155
+ }
236156
+ if (stamped.type === 'metadata') {
236157
+ if (!this.metadataInitialized) {
236158
+ this.persistedProtocolVersion = stamped.protocol_version;
236159
+ }
236160
+ this.metadataInitialized = true;
236161
+ }
236124
236162
  this.persistence?.append(stamped);
236125
236163
  }
236126
236164
  restore(record) {
@@ -236134,7 +236172,8 @@ var init_records = __esmMin((() => {
236134
236172
  }
236135
236173
  }
236136
236174
  async replay(options = {}) {
236137
- if (!this.persistence) throw new Error("No persistence provided for AgentRecords");
236175
+ this.persistedProtocolVersion = undefined;
236176
+ if (!this.persistence) throw new Error("No persistence provided for AgentRecords");
236138
236177
  this.resumeCheckpointRestored = false;
236139
236178
  const rewriteMigratedRecords = options.rewriteMigratedRecords ?? true;
236140
236179
  if (rewriteMigratedRecords && await this.tryReplayResumeCheckpoint()) return {};
@@ -236152,6 +236191,7 @@ var init_records = __esmMin((() => {
236152
236191
  hasMetadata = true;
236153
236192
  this.metadataInitialized = true;
236154
236193
  const readVersion = record.protocol_version;
236194
+ this.persistedProtocolVersion = readVersion;
236155
236195
  if (isNewerWireVersion(readVersion)) {
236156
236196
  warning = `Session wire protocol version ${readVersion} is newer than the current version 1.4. Records will be replayed without migration.`;
236157
236197
  shouldRewrite = false;
@@ -236174,6 +236214,7 @@ var init_records = __esmMin((() => {
236174
236214
  if (completed && shouldRewrite && replayedRecords !== void 0) {
236175
236215
  this.persistence.rewrite(replayedRecords);
236176
236216
  await this.persistence.flush();
236217
+ this.persistedProtocolVersion = "1.4";
236177
236218
  }
236178
236219
  if (completed && this.persistence instanceof FileSystemAgentRecordPersistence) this.persistence.persistedRecordCount = replayedRecordCount;
236179
236220
  if (completed && this.agent.blobStore !== void 0) for (const msg of this.agent.context.history) await this.agent.blobStore.rehydrateParts(msg.content);
@@ -236184,8 +236225,17 @@ var init_records = __esmMin((() => {
236184
236225
  if (this.agent.replayBuilder.options.range !== void 0) return false;
236185
236226
  const checkpoint = await loadResumeCheckpoint({ wirePath: this.persistence.filePath });
236186
236227
  if (!checkpoint.ok) return false;
236228
+ let sourceProtocol;
236229
+ for await (const metadata of this.persistence.read()) {
236230
+ if (metadata.type !== 'metadata' || metadata.protocol_version !== "1.4") return false;
236231
+ sourceProtocol = metadata.protocol_version;
236232
+ break;
236233
+ }
236234
+ if (sourceProtocol === undefined) return false;
236235
+
236187
236236
  if (!restoreAgentResumeSnapshot(this.agent, checkpoint.snapshot)) return false;
236188
236237
  this.metadataInitialized = true;
236238
+ this.persistedProtocolVersion = sourceProtocol;
236189
236239
  let tailRecordCount = 0;
236190
236240
  for await (const record of this.persistence.readFrom(checkpoint.offset, checkpoint.recordCount)) {
236191
236241
  this.restore(record);
@@ -265678,34 +265728,52 @@ var init_web_search = __esmMin((() => {
265678
265728
  execute: (ctx) => this.execution(args, ctx)
265679
265729
  };
265680
265730
  }
265681
- async execution(args, { toolCallId }) {
265682
- try {
265683
- const opts = { toolCallId };
265684
- const results = await this.provider.search(args.query, opts);
265685
- const builder = new ToolResultBuilder({ maxLineLength: null });
265686
- if (results.length === 0) {
265687
- builder.write("No search results found.");
265688
- return builder.ok();
265689
- }
265690
- let first = true;
265691
- for (const result of results) {
265692
- if (!first) builder.write("---\n\n");
265693
- first = false;
265694
- builder.write(`Title: ${result.title}\n`);
265695
- if (result.siteName) builder.write(`Site: ${result.siteName}\n`);
265696
- if (result.date) builder.write(`Date: ${result.date}\n`);
265697
- builder.write(`URL: ${result.url}\n`);
265698
- builder.write(`Snippet: ${result.snippet}\n\n`);
265699
- }
265700
- builder.write("When you rely on a result in your answer, cite it inline as a markdown link, e.g. [title](url).");
265701
- return builder.ok();
265702
- } catch (error) {
265703
- return {
265704
- isError: true,
265705
- output: classifySearchError(error)
265706
- };
265707
- }
265708
- }
265731
+ async execution(args, { toolCallId, }) {
265732
+ try {
265733
+ let failedSources = 0;
265734
+ const opts = { toolCallId, onNotice: notice => {
265735
+ if (notice.code === 'partial-sources' && Number.isInteger(notice.failedSources)
265736
+ && notice.failedSources > 0 && notice.failedSources <= 128) {
265737
+ failedSources = Math.max(failedSources, notice.failedSources);
265738
+ }
265739
+ } };
265740
+ const results = await this.provider.search(args.query, opts);
265741
+ const builder = new ToolResultBuilder({ maxLineLength: null });
265742
+ if (failedSources > 0) {
265743
+ if (results.length === 0)
265744
+ throw new Error('Search sources failed without usable results.');
265745
+ builder.write(`Some search sources failed (${failedSources}). Results below are partial, not a complete search.\n\n`);
265746
+ }
265747
+ if (results.length === 0) {
265748
+ builder.write('No search results found.');
265749
+ return builder.ok();
265750
+ }
265751
+ let first = true;
265752
+ for (const result of results) {
265753
+ if (!first)
265754
+ builder.write('---\n\n');
265755
+ first = false;
265756
+ builder.write(`Title: ${result.title}\n`);
265757
+ if (result.siteName)
265758
+ builder.write(`Site: ${result.siteName}\n`);
265759
+ if (result.date)
265760
+ builder.write(`Date: ${result.date}\n`);
265761
+ builder.write(`URL: ${result.url}\n`);
265762
+ builder.write(`Snippet: ${result.snippet}\n\n`);
265763
+ }
265764
+ // Keep the citation reminder next to the data (not just in the static tool
265765
+ // description), so it is present on every search. Cite the page actually
265766
+ // relied on — after a FetchURL follow-up, that is the fetched page.
265767
+ builder.write('When you rely on a result in your answer, cite it inline as a markdown link, e.g. [title](url).');
265768
+ return builder.ok();
265769
+ }
265770
+ catch (error) {
265771
+ return {
265772
+ isError: true,
265773
+ output: classifySearchError(error),
265774
+ };
265775
+ }
265776
+ }
265709
265777
  };
265710
265778
  }));
265711
265779
  //#endregion
@@ -298151,13 +298219,11 @@ function resolveStdioCwd(configCwd, defaultCwd) {
298151
298219
  if (defaultCwd !== void 0 && !isAbsolute$2(configCwd)) return resolve$2(defaultCwd, configCwd);
298152
298220
  return configCwd;
298153
298221
  }
298154
- function mergeStdioEnv(configEnv, parentEnv = process.env) {
298155
- const merged = {};
298156
- for (const [key, value] of Object.entries(parentEnv)) if (value !== void 0) merged[key] = value;
298157
- if (configEnv !== void 0) Object.assign(merged, configEnv);
298158
- Object.assign(merged, proxyEnvForChild(merged));
298159
- reconcileChildNoProxy(merged, configEnv);
298160
- return merged;
298222
+ function mergeStdioEnv(configEnv, parentEnv = process.env, runtimeEnv) {
298223
+ const merged = overlayRuntimeEnv({ ...parentEnv, ...configEnv }, runtimeEnv);
298224
+ Object.assign(merged, proxyEnvForChild(merged));
298225
+ reconcileChildNoProxy(merged, overlayRuntimeEnv(configEnv ?? {}, runtimeEnv));
298226
+ return merged;
298161
298227
  }
298162
298228
  var STDERR_BUFFER_CAPACITY, StdioMcpClient, BoundedTail;
298163
298229
  var init_client_stdio = __esmMin((() => {
@@ -298187,7 +298253,7 @@ var init_client_stdio = __esmMin((() => {
298187
298253
  this.transport = new StdioClientTransport({
298188
298254
  command: config.command,
298189
298255
  args: config.args,
298190
- env: mergeStdioEnv(config.env, config.inheritEnv === false ? {} : process.env),
298256
+ env: mergeStdioEnv(config.env, config.inheritEnv === false ? {} : process.env, options.runtimeEnv),
298191
298257
  cwd: resolveStdioCwd(config.cwd, options.defaultCwd),
298192
298258
  stderr: "pipe"
298193
298259
  });
@@ -298689,22 +298755,32 @@ var init_connection_manager = __esmMin((() => {
298689
298755
  return entry.attemptId;
298690
298756
  }
298691
298757
  createClient(config, name) {
298692
- const toolCallTimeoutMs = config.toolTimeoutMs;
298693
- if (config.transport === "stdio") return new StdioMcpClient(config, {
298694
- toolCallTimeoutMs,
298695
- defaultCwd: this.options.stdioCwd
298696
- });
298697
- if (config.transport === "sse") return new SseMcpClient(config, {
298698
- toolCallTimeoutMs,
298699
- envLookup: this.options.envLookup,
298700
- oauthProvider: this.resolveOAuthProvider(config, name)
298701
- });
298702
- return new HttpMcpClient(config, {
298703
- toolCallTimeoutMs,
298704
- envLookup: this.options.envLookup,
298705
- oauthProvider: this.resolveOAuthProvider(config, name)
298706
- });
298707
- }
298758
+ const toolCallTimeoutMs = config.toolTimeoutMs;
298759
+ if (config.transport === 'stdio') {
298760
+ let runtimeEnv;
298761
+ try {
298762
+ runtimeEnv = this.options.stdioRuntimeEnv?.(name, config);
298763
+ }
298764
+ catch {
298765
+ throw new Error('MCP runtime environment is unavailable.');
298766
+ }
298767
+ return new StdioMcpClient(config, {
298768
+ toolCallTimeoutMs, defaultCwd: this.options.stdioCwd, runtimeEnv,
298769
+ });
298770
+ }
298771
+ if (config.transport === 'sse') {
298772
+ return new SseMcpClient(config, {
298773
+ toolCallTimeoutMs,
298774
+ envLookup: this.options.envLookup,
298775
+ oauthProvider: this.resolveOAuthProvider(config, name),
298776
+ });
298777
+ }
298778
+ return new HttpMcpClient(config, {
298779
+ toolCallTimeoutMs,
298780
+ envLookup: this.options.envLookup,
298781
+ oauthProvider: this.resolveOAuthProvider(config, name),
298782
+ });
298783
+ }
298708
298784
  resolveOAuthProvider(config, name) {
298709
298785
  const oauthService = this.oauthService;
298710
298786
  if (oauthService === void 0) return void 0;
@@ -299417,6 +299493,43 @@ var init_lsp = __esmMin((() => {
299417
299493
  init_manager$1();
299418
299494
  }));
299419
299495
  //#endregion
299496
+
299497
+ import { randomBytes as releaseRecordRandomBytes } from "node:crypto";
299498
+ const EMPTY_RUNTIME = Object.freeze({
299499
+ AGENTSPINE_KING_TIMELINE_SOURCE: '',
299500
+ AGENTSPINE_KING_WIRE_PROTOCOL_VERSION: '',
299501
+ AGENTSPINE_TIMELINE_TRANSPORT_SESSION_ID: '',
299502
+ AGENTSPINE_TIMELINE_SESSION_CAPABILITY: '',
299503
+ });
299504
+ class AgentSpineSessionRuntime {
299505
+ #env = EMPTY_RUNTIME;
299506
+ #hooks;
299507
+ #servers;
299508
+ constructor(recipients) {
299509
+ this.#hooks = new Set(recipients?.hooks);
299510
+ this.#servers = new Map(Object.entries(recipients?.mcpServers ?? {}));
299511
+ }
299512
+ get hasRecipients() {
299513
+ return this.#hooks.size > 0 || this.#servers.size > 0;
299514
+ }
299515
+ bind(sessionId, source) {
299516
+ this.#env = EMPTY_RUNTIME;
299517
+ if (!sessionId || !/^[A-Za-z0-9][A-Za-z0-9:_.@/-]{0,127}$/.test(sessionId) || !source)
299518
+ return;
299519
+ this.#env = Object.freeze({
299520
+ AGENTSPINE_KING_TIMELINE_SOURCE: source.path,
299521
+ AGENTSPINE_KING_WIRE_PROTOCOL_VERSION: source.protocolVersion,
299522
+ AGENTSPINE_TIMELINE_TRANSPORT_SESSION_ID: sessionId,
299523
+ AGENTSPINE_TIMELINE_SESSION_CAPABILITY: `astc_${releaseRecordRandomBytes(32).toString('base64url')}`,
299524
+ });
299525
+ }
299526
+ forHook(hook) {
299527
+ return this.#hooks.has(hook) ? this.#env : EMPTY_RUNTIME;
299528
+ }
299529
+ forMcp(name, config) {
299530
+ return config.transport === 'stdio' && this.#servers.get(name) === config ? this.#env : EMPTY_RUNTIME;
299531
+ }
299532
+ }
299420
299533
  //#region ../../packages/agent-core/src/session/index.ts
299421
299534
  async function waitForSettlementOrTimeout(promise, timeoutMs) {
299422
299535
  let timeout;
@@ -299497,6 +299610,16 @@ var init_session$1 = __esmMin((() => {
299497
299610
  writeMetadataPromise = Promise.resolve();
299498
299611
  agentsMdWarning;
299499
299612
  accountMemoryContext = "";
299613
+ agentSpineRuntime;
299614
+ mcpLoadStarted = false;
299615
+ startMcpServers() {
299616
+ if (this.mcpLoadStarted)
299617
+ return;
299618
+ this.mcpLoadStarted = true;
299619
+ void this.loadMcpServers().catch((error) => {
299620
+ this.emitInitialMcpLoadError(error);
299621
+ });
299622
+ }
299500
299623
  constructor(options) {
299501
299624
  this.options = options;
299502
299625
  this.logHandle = options.id === void 0 ? void 0 : getRootLogger().attachSession({
@@ -299506,6 +299629,8 @@ var init_session$1 = __esmMin((() => {
299506
299629
  this.log = this.logHandle?.logger ?? (options.id === void 0 ? log : log.createChild({ sessionId: options.id }));
299507
299630
  this.rpc = options.rpc;
299508
299631
  this.experimentalFlags = options.experimentalFlags ?? new FlagResolver();
299632
+ this.agentSpineRuntime = this.experimentalFlags.enabled('agent_spine_timeline')
299633
+ ? new AgentSpineSessionRuntime(options.agentSpineRuntime) : undefined;
299509
299634
  this.visionReader = createManagedMediaVisionReader(options.toolServices?.media) ?? (this.experimentalFlags.enabled("vision_reader") ? createVisionReader(options.config?.services?.visionReader) : void 0);
299510
299635
  this.hookEngine = new HookEngine(options.hooks, {
299511
299636
  cwd: options.kaos.getcwd(),
@@ -299534,8 +299659,13 @@ var init_session$1 = __esmMin((() => {
299534
299659
  code: "HOST_HOOK_WARNING",
299535
299660
  message
299536
299661
  });
299537
- }
299538
- });
299662
+ },
299663
+ runtimeEnv: (hook) => {
299664
+ const hostEnv = options.hookRuntimeEnv?.(hook);
299665
+ const timelineEnv = this.agentSpineRuntime?.forHook(hook);
299666
+ return timelineEnv === undefined ? hostEnv : { ...hostEnv, ...timelineEnv };
299667
+ }
299668
+ });
299539
299669
  this.telemetry = options.telemetry ?? noopTelemetryClient;
299540
299670
  this.toolKaos = options.kaos;
299541
299671
  this.persistenceKaos = options.persistenceKaos ?? options.kaos;
@@ -299546,8 +299676,13 @@ var init_session$1 = __esmMin((() => {
299546
299676
  this.mcp = new McpConnectionManager({
299547
299677
  oauthService: new McpOAuthService({ blunHomeDir: resolveSharedBlunHome(options.blunHomeDir) }),
299548
299678
  log: this.log,
299549
- stdioCwd: options.kaos.getcwd()
299550
- });
299679
+ stdioCwd: options.kaos.getcwd(),
299680
+ stdioRuntimeEnv: (name, config) => {
299681
+ const hostEnv = options.mcpRuntimeEnv?.(name, config);
299682
+ const timelineEnv = this.agentSpineRuntime?.forMcp(name, config);
299683
+ return timelineEnv === undefined ? hostEnv : { ...hostEnv, ...timelineEnv };
299684
+ }
299685
+ });
299551
299686
  this.lsp = options.lspServers === void 0 || Object.keys(options.lspServers).length === 0 ? void 0 : new LspManager({
299552
299687
  cwd: options.kaos.getcwd(),
299553
299688
  servers: options.lspServers
@@ -299560,9 +299695,7 @@ var init_session$1 = __esmMin((() => {
299560
299695
  }).then(() => {
299561
299696
  this.refreshAgentBuiltinTools();
299562
299697
  });
299563
- this.loadMcpServers().catch((error) => {
299564
- this.emitInitialMcpLoadError(error);
299565
- });
299698
+ if (!this.agentSpineRuntime?.hasRecipients) this.startMcpServers();
299566
299699
  }
299567
299700
  setToolKaos(kaos) {
299568
299701
  this.toolKaos = kaos;
@@ -300208,7 +300341,18 @@ var init_session$1 = __esmMin((() => {
300208
300341
  });
300209
300342
  }
300210
300343
  async triggerSessionStart(source) {
300211
- const results = await this.hookEngine.trigger("SessionStart", {
300344
+ if (this.agentSpineRuntime?.hasRecipients && !this.mcpLoadStarted) {
300345
+ try {
300346
+ const main = this.getReadyAgent('main');
300347
+ await main?.records.flush();
300348
+ this.agentSpineRuntime.bind(this.options.id, main?.records.fileSource);
300349
+ }
300350
+ catch {
300351
+ this.log.warn('Native session record source is unavailable.');
300352
+ }
300353
+ this.startMcpServers();
300354
+ }
300355
+ const results = await this.hookEngine.trigger("SessionStart", {
300212
300356
  matcherValue: source,
300213
300357
  inputData: { source }
300214
300358
  });
@@ -315095,43 +315239,80 @@ var init_blun_web_search = __esmMin((() => {
315095
315239
  this.fetchImpl = options.fetchImpl ?? globalThis.fetch.bind(globalThis);
315096
315240
  }
315097
315241
  async search(query, options) {
315098
- const bodyJson = JSON.stringify({ text_query: query });
315099
- const toolCallId = options?.toolCallId;
315100
- const response = await this.post(bodyJson, toolCallId);
315101
- if (response.status === 401) {
315102
- const detail = await safeReadText(response);
315103
- throw new Error(`Blun search request failed: HTTP 401 (auth/unauthorized). ${detail}`.trim());
315104
- }
315105
- if (response.status !== 200) {
315106
- const detail = await safeReadText(response);
315107
- throw new Error(`Blun search request failed: HTTP ${String(response.status)}. ${detail}`.trim());
315108
- }
315109
- const json = await response.json();
315110
- return (Array.isArray(json.search_results) ? json.search_results : []).map((r) => {
315111
- const out = {
315112
- title: r.title ?? "",
315113
- url: r.url ?? "",
315114
- snippet: r.snippet ?? ""
315115
- };
315116
- if (typeof r.date === "string" && r.date.length > 0) out.date = r.date;
315117
- if (typeof r.site_name === "string" && r.site_name.length > 0) out.siteName = r.site_name;
315118
- return out;
315119
- });
315120
- }
315121
- async post(bodyJson, toolCallId) {
315122
- const accessToken = await this.resolveApiKey();
315123
- return this.fetchImpl(this.baseUrl, {
315124
- method: "POST",
315125
- headers: {
315126
- ...this.defaultHeaders,
315127
- Authorization: `Bearer ${accessToken}`,
315128
- "Content-Type": "application/json",
315129
- ...toolCallId !== void 0 && toolCallId.length > 0 ? { "X-Msh-Tool-Call-Id": toolCallId } : {},
315130
- ...this.customHeaders
315131
- },
315132
- body: bodyJson
315133
- });
315134
- }
315242
+ const body = { text_query: query };
315243
+ const bodyJson = JSON.stringify(body);
315244
+ const toolCallId = options?.toolCallId;
315245
+ const supportsNotices = typeof options?.onNotice === 'function';
315246
+ const response = await this.post(bodyJson, toolCallId, supportsNotices);
315247
+ if (response.status === 401) {
315248
+ const detail = await safeReadText(response);
315249
+ throw new Error(`Blun search request failed: HTTP 401 (auth/unauthorized). ${detail}`.trim());
315250
+ }
315251
+ if (response.status !== 200) {
315252
+ const detail = await safeReadText(response);
315253
+ throw new Error(`Blun search request failed: HTTP ${String(response.status)}. ${detail}`.trim());
315254
+ }
315255
+ const json = (await response.json());
315256
+ const raw = Array.isArray(json.search_results) ? json.search_results : [];
315257
+ const status = json.search_status;
315258
+ if (status !== undefined) {
315259
+ if (!status || typeof status !== 'object' || !Array.isArray(json.search_results)
315260
+ || !Number.isInteger(status.failed_sources) || typeof status.failed_sources !== 'number'
315261
+ || status.failed_sources < 0 || status.failed_sources > 128
315262
+ || (status.state !== 'complete' && status.state !== 'partial')
315263
+ || (status.state === 'complete' && status.failed_sources !== 0)
315264
+ || (status.state === 'partial' && (status.failed_sources === 0 || raw.length === 0))) {
315265
+ throw new Error('Invalid search status response.');
315266
+ }
315267
+ if (!raw.every(row => {
315268
+ if (!row || typeof row.title !== 'string' || !row.title.trim()
315269
+ || typeof row.snippet !== 'string' || typeof row.url !== 'string')
315270
+ return false;
315271
+ try {
315272
+ const url = new URL(row.url);
315273
+ return (url.protocol === 'https:' || url.protocol === 'http:') && !url.username && !url.password;
315274
+ }
315275
+ catch {
315276
+ return false;
315277
+ }
315278
+ }))
315279
+ throw new Error('Invalid search results response.');
315280
+ if (status.state === 'partial') {
315281
+ if (!supportsNotices)
315282
+ throw new Error('Partial search results require a notice consumer.');
315283
+ options?.onNotice?.({ code: 'partial-sources', failedSources: status.failed_sources });
315284
+ }
315285
+ }
315286
+ return raw.map((r) => {
315287
+ const out = {
315288
+ title: r.title ?? '',
315289
+ url: r.url ?? '',
315290
+ snippet: r.snippet ?? '',
315291
+ };
315292
+ if (typeof r.date === 'string' && r.date.length > 0)
315293
+ out.date = r.date;
315294
+ if (typeof r.site_name === 'string' && r.site_name.length > 0)
315295
+ out.siteName = r.site_name;
315296
+ return out;
315297
+ });
315298
+ }
315299
+ async post(bodyJson, toolCallId, supportsNotices) {
315300
+ const accessToken = await this.resolveApiKey();
315301
+ return this.fetchImpl(this.baseUrl, {
315302
+ method: 'POST',
315303
+ headers: {
315304
+ ...this.defaultHeaders,
315305
+ Authorization: `Bearer ${accessToken}`,
315306
+ 'Content-Type': 'application/json',
315307
+ ...(toolCallId !== undefined && toolCallId.length > 0
315308
+ ? { 'X-Msh-Tool-Call-Id': toolCallId }
315309
+ : {}),
315310
+ ...this.customHeaders,
315311
+ ...(supportsNotices ? { 'X-BLUN-Search-Contract': 'partial-v1' } : {}),
315312
+ },
315313
+ body: bodyJson,
315314
+ });
315315
+ }
315135
315316
  async resolveApiKey() {
315136
315317
  if (this.tokenProvider !== void 0) try {
315137
315318
  return await this.tokenProvider.getAccessToken();
@@ -318903,6 +319084,7 @@ var init_core_impl = __esmMin((() => {
318903
319084
  const pluginSessionStarts = this.plugins.enabledSessionStarts();
318904
319085
  const pluginCommands = await this.plugins.enabledCommands();
318905
319086
  const mcpConfig = this.mergePluginMcpConfig(withCallerMcp);
319087
+ const pluginHooks = this.plugins.enabledHooks();
318906
319088
  const runtime = await this.resolveRuntime(config);
318907
319089
  const session = new Session$1({
318908
319090
  kaos: parentKaos.withCwd(workDir),
@@ -318915,7 +319097,8 @@ var init_core_impl = __esmMin((() => {
318915
319097
  rpc: proxyWithExtraPayload(await this.sdk, { sessionId: summary.id }),
318916
319098
  providerManager: this.resolveProviderManager(summary.id),
318917
319099
  background: config.background,
318918
- hooks: [...config.hooks ?? [], ...this.plugins.enabledHooks()],
319100
+ hooks: [...(config.hooks ?? []), ...pluginHooks],
319101
+ agentSpineRuntime: this.agentSpineRuntimeRecipients(pluginHooks, mcpConfig),
318919
319102
  permissionRules: config.permission?.rules,
318920
319103
  skills: this.resolveSessionSkillConfig(config),
318921
319104
  mcpConfig,
@@ -318998,6 +319181,7 @@ var init_core_impl = __esmMin((() => {
318998
319181
  const pluginSessionStarts = this.plugins.enabledSessionStarts();
318999
319182
  const pluginCommands = await this.plugins.enabledCommands();
319000
319183
  const mcpConfig = this.mergePluginMcpConfig(withCallerMcp);
319184
+ const pluginHooks = this.plugins.enabledHooks();
319001
319185
  const runtime = await this.resolveRuntime(config);
319002
319186
  const parentKaos = parentKaosForRead;
319003
319187
  const persistenceKaos = overrides.persistenceKaos ?? parentKaos;
@@ -319012,7 +319196,8 @@ var init_core_impl = __esmMin((() => {
319012
319196
  rpc: proxyWithExtraPayload(await this.sdk, { sessionId: summary.id }),
319013
319197
  providerManager: this.resolveProviderManager(summary.id),
319014
319198
  background: config.background,
319015
- hooks: [...config.hooks ?? [], ...this.plugins.enabledHooks()],
319199
+ hooks: [...(config.hooks ?? []), ...pluginHooks],
319200
+ agentSpineRuntime: this.agentSpineRuntimeRecipients(pluginHooks, mcpConfig),
319016
319201
  permissionRules: config.permission?.rules,
319017
319202
  skills: this.resolveSessionSkillConfig(config),
319018
319203
  mcpConfig,
@@ -319468,6 +319653,21 @@ var init_core_impl = __esmMin((() => {
319468
319653
  }
319469
319654
  };
319470
319655
  }
319656
+ agentSpineRuntimeRecipients(pluginHooks, mcpConfig) {
319657
+ if (!this.experimentalFlags.enabled('agent_spine_timeline'))
319658
+ return undefined;
319659
+ const plugin = this.plugins.get('agent-spine');
319660
+ if (plugin?.state !== 'ok' || !plugin.enabled)
319661
+ return undefined;
319662
+ // These objects came from PluginManager, not caller hooks or session metadata.
319663
+ const hooks = pluginHooks.filter(hook => hook.env?.['BLUN_PLUGIN_ROOT'] === plugin.root);
319664
+ const mcpServers = Object.fromEntries(Object.entries(mcpConfig?.servers ?? {}).filter(([name, config]) => {
319665
+ const origin = mcpConfig?.origins?.[name];
319666
+ return origin?.source === 'plugin' && origin.pluginId === plugin.id && origin.pluginEnabled
319667
+ && config.transport === 'stdio' && config.enabled !== false;
319668
+ }));
319669
+ return hooks.length > 0 || Object.keys(mcpServers).length > 0 ? { hooks, mcpServers } : undefined;
319670
+ }
319471
319671
  async resolveCurrentMcpConfig(session) {
319472
319672
  const snapshot = session.mcpConfigSnapshot();
319473
319673
  const callerServers = Object.fromEntries(Object.entries(snapshot?.servers ?? {}).filter(([name]) => snapshot?.origins?.[name]?.source === "session"));
@@ -422051,6 +422251,21 @@ var TelegramChannelController = class {
422051
422251
  constructor(host, dir = telegramStateDir(), options = {}) {
422052
422252
  this.host = host;
422053
422253
  this.dir = dir;
422254
+ this.sessionScope = options.sessionScope;
422255
+ this.sessionDelivery = new BlunSessionQueueDelivery(dir, {
422256
+ ownsChannel: () => !this.stopped && this.ownsChannel(),
422257
+ currentSessionId: () => this.sessionScope?.()?.sessionId,
422258
+ legacyCheckpoint: () => this.checkpointOffset,
422259
+ inject: (envelope, acknowledge) => {
422260
+ const trace = telegramDeliveryIdentity(envelope.meta);
422261
+ telegramDeliveryLifecycle.record({ stage: "tui_injected", ...trace, route: "tui" });
422262
+ this.host.inject(envelope, () => {
422263
+ if (this.stopped || !this.ownsChannel()) return;
422264
+ acknowledge();
422265
+ telegramDeliveryLifecycle.record({ stage: "acknowledged", ...trace, route: "tui" });
422266
+ });
422267
+ }
422268
+ });
422054
422269
  this.spawnBridge = options.spawnBridge ?? spawn;
422055
422270
  this.bridgeStopTimeoutMs = options.bridgeStopTimeoutMs ?? BRIDGE_STOP_TIMEOUT_MS;
422056
422271
  this.bridgeForceStopTimeoutMs = options.bridgeForceStopTimeoutMs ?? BRIDGE_FORCE_STOP_TIMEOUT_MS;
@@ -422305,6 +422520,7 @@ var TelegramChannelController = class {
422305
422520
  this.loseOwnership();
422306
422521
  return;
422307
422522
  }
422523
+ if (this.sessionScope !== undefined) { this.pollSessionQueue(this.sessionScope()); return; }
422308
422524
  let stats;
422309
422525
  try {
422310
422526
  stats = statSync(this.queueFile);
@@ -422486,6 +422702,16 @@ var TelegramChannelController = class {
422486
422702
  if (!servers.some((server) => (server.name === "telegram" || server.name.endsWith(":telegram")) && server.status !== "disabled" && server.status !== "failed")) this.host.warn(uiText("telegramChannel.replyToolsMissing", { command: "/plugins install <repo>\\plugins\\telegram" }));
422487
422703
  } catch {}
422488
422704
  }
422705
+
422706
+ pollSessionQueue(scope, deliver = true) {
422707
+ try { this.sessionDelivery.poll(scope, deliver); this.sessionQueueError = undefined; }
422708
+ catch (error) { const message = String(error); if (message !== this.sessionQueueError) {
422709
+ this.sessionQueueError = message;
422710
+ this.host.warn(uiText("telegramChannel.leaseWriteFailed", { error: message }));
422711
+ } }
422712
+ }
422713
+
422714
+ bindSessionQueue(scope) { if (this.sessionScope !== undefined) this.pollSessionQueue(scope, false); }
422489
422715
  };
422490
422716
  //#endregion
422491
422717
  //#region src/tui/commands/config-general.copy.ts
@@ -518947,28 +519173,46 @@ async function saveAutoRetrievedMedia(result) {
518947
519173
  function isGroupChat(chatId) {
518948
519174
  return chatId.startsWith("-");
518949
519175
  }
518950
- const STRONG_META = /\b(keine (gruppen ?)?antwort|gar keine antwort|halte (mich|die klappe)|(bin|bleibe) (jetzt )?still|ich schweige|schweige|stille|nichts (zu tun|zu melden|fur mich|fuer mich)|nicht (direkt )?an mich( gerichtet)?|nicht meine lane|keine reaktion|no ?action|kein wort|silence|staying (silent|out|quiet)|keeping (quiet|out)|no (response|reply|answer|comment)|nothing (to (add|do|report)|for me)|not (for me|my lane|addressed to me|directed at me)|holding back|standing by|no need to (respond|reply))\b/;
518951
- const SHORT_CONFIRM = /^(verstanden|kapiert|ok|okay|alles klar|geht klar|angekommen|seh sie|ich seh sie|gemerkt|passt|erledigt notiert|notiert|no ?action|understood|got it|noted|acknowledged|roger|received|silence)\b[\s\S]{0,48}$/;
519176
+ function normalizeGroupNoise(text) {
519177
+ return text
519178
+ .toLowerCase()
519179
+ .replace(/\u00e4/g, 'a')
519180
+ .replace(/\u00f6/g, 'o')
519181
+ .replace(/\u00fc/g, 'u')
519182
+ .replace(/\u00df/g, 'ss')
519183
+ .replace(/[\u2010-\u2015]/g, ' ')
519184
+ .replace(/[^\p{L}\p{N}\s]/gu, ' ')
519185
+ .replace(/\s+/g, ' ')
519186
+ .trim();
519187
+ }
519188
+ const GROUP_REPLY_EMPTY_CLAUSES = [
519189
+ /^(verstanden|kapiert|ok|okay|alles klar|geht klar|angekommen|seh sie|ich seh sie|gemerkt|passt|erledigt notiert|notiert|understood|got it|noted|acknowledged|roger|received)$/,
519190
+ /^(bin da|bin hier|hier bin ich|ich bin da|was brauchst du|was gibt es|was liegt an|wie kann ich helfen|bereit|ich warte|warte auf|melde mich|freut mich|here|standing by|ready|awaiting|what do you need)$/,
519191
+ /^(keine (gruppen ?)?antwort|gar keine antwort|keine reaktion|kein wort|no ?action|no (response|reply|answer|comment))( (notig|erforderlich|needed|required))?$/,
519192
+ /^(ich )?(halte mich raus|(bin|bleibe) (jetzt )?still|schweige|habe nichts zu melden)$/,
519193
+ /^(stille|still|silence|nichts (zu tun|zu melden|fur mich|fuer mich)|nicht meine lane)$/,
519194
+ /^(i (am |will be )?)?(staying (silent|out|quiet)|keeping (quiet|out)|holding back)$/,
519195
+ /^(i have )?nothing (to (add|do|report)|for me)$/,
519196
+ /^no need to (respond|reply)$/,
519197
+ /^(nicht (direkt )?an mich( gerichtet)?|not (for me|my lane|addressed to me|directed at me|to me))$/,
519198
+ /^(die(se)? nachricht ist )?(nicht (direkt )?an mich|an die gruppe)( gerichtet)?$/,
519199
+ /^(this|the) message is (addressed to the group|not (addressed )?to me)$/,
519200
+ /^(es gibt )?kein(en|e)? (auftrag( (an|fur|fuer) mich)?|frage( an mich)?|arbeit( verlangt)?)$/,
519201
+ /^(there is )?no (task|question|work)( for me)?$/,
519202
+ /^(ich beende den )?turn ohne (ausgabe|antwort)( beendet)?$/,
519203
+ /^i (will )?end this turn without (a response|output)$/,
519204
+ /^(nach|gemaess) der leerlauf regel( \d+)? gibt es nichts zu melden$/,
519205
+ ];
519206
+ const GROUP_REPLY_NOTIFICATION_CONTEXT = /^die(se)? nachricht ist eine (konfigurations )?information( [\p{L}\p{N} _-]{1,64} wird ab jetzt nur noch bei erwahnung benachrichtigt)?( an die gruppe gerichtet)?$/u;
519207
+
518952
519208
  function isGroupNoiseReply(text) {
518953
- const n = text.toLowerCase().replace(/ä/g, "a").replace(/ö/g, "o").replace(/ü/g, "u").replace(/ß/g, "ss").replace(/[‐-―]/g, " ").replace(/[^\p{L}\p{N}\s]/gu, " ").replace(/\s+/g, " ").trim();
518954
- if (n.length === 0) return true;
518955
- if (n.length > 200) return false;
518956
- return STRONG_META.test(n) || SHORT_CONFIRM.test(n);
519209
+ const clauses = text.split(/[.!?;,\r\n]+|\s+[-\u2013\u2014]\s+/u).map(normalizeGroupNoise).filter(Boolean);
519210
+ return clauses.every((clause) => GROUP_REPLY_EMPTY_CLAUSES.some((pattern) => pattern.test(clause))
519211
+ || GROUP_REPLY_NOTIFICATION_CONTEXT.test(clause));
518957
519212
  }
518958
- /**
518959
- * Whether a group reply must be suppressed. `contextOnly` = the triggering
518960
- * message was context-only and not addressed to King. All
518961
- * Sprachen": on a read-only message King should stay silent, so ANY short reply
518962
- * back is meta chatter regardless of language ("Silence.", "Tystnad.", "沈黙。").
518963
- * Long substantive contributions still pass; the word-list catches longer DE/EN
518964
- * meta lines. Addressed messages only hit the word-list, never the length rule.
518965
- */
518966
- function isGroupSuppressed(text, contextOnly) {
518967
- if (isGroupNoiseReply(text)) return true;
518968
- if (contextOnly) {
518969
- if (text.replace(/\s+/g, " ").trim().length <= 40) return true;
518970
- }
518971
- return false;
519213
+ /** Unknown content is preserved, including short useful context-only warnings. */
519214
+ function isGroupSuppressed(text, _contextOnly) {
519215
+ return isGroupNoiseReply(text);
518972
519216
  }
518973
519217
  /**
518974
519218
  * Send `text` to `chatId` via the Bot API and log it to outbox.jsonl as
@@ -520803,6 +521047,7 @@ var BlunTUI = class {
520803
521047
  const { workDir } = this.state.appState;
520804
521048
  let session;
520805
521049
  let shouldReplayHistory = false;
521050
+ const telegramBoundary = this.captureTelegramQueueBoundary();
520806
521051
  const isResumeStartup = startup.sessionFlag !== void 0 || startup.continueLast;
520807
521052
  const createSessionOptions = {
520808
521053
  workDir,
@@ -520876,7 +521121,7 @@ var BlunTUI = class {
520876
521121
  return false;
520877
521122
  }
520878
521123
  if (session === void 0) throw new Error(uiText("blunTui.startup.sessionNotInitialized"));
520879
- await this.measureStartupPhase("session_bind_ms", () => this.setSession(session, { refreshPersonalMemory: false }));
521124
+ await this.measureStartupPhase("session_bind_ms", () => this.setSession(session, { refreshPersonalMemory: false, telegramSessionScope: { sessionId: session.id, mode: shouldReplayHistory ? "resume" : "new", boundary: telegramBoundary } }));
520880
521125
  await this.measureStartupPhase("runtime_state_ms", () => this.syncRuntimeState(session));
520881
521126
  this.applyStartupPermissionAndPlanToAppState();
520882
521127
  this.state.startupState = "ready";
@@ -522115,6 +522360,7 @@ var BlunTUI = class {
522115
522360
  text: focusedModelInput
522116
522361
  }, imagePart] : focusedModelInput;
522117
522362
  session.promptAccepted(promptInput, channelPromptOrigin(channelReportSources)).then((result) => {
522363
+ if (this.session !== session) return;
522118
522364
  if (result.accepted) {
522119
522365
  if (channelFocusId !== void 0) this.clearAddressedChannelFocusIds([channelFocusId]);
522120
522366
  this.traceTelegramDelivery?.({ stage: "accepted", ...channelDeliveryTrace, route: "prompt", turnId: result.turnId });
@@ -522174,6 +522420,7 @@ var BlunTUI = class {
522174
522420
  this.updateQueueDisplay();
522175
522421
  this.state.ui.requestRender();
522176
522422
  }).catch((error) => {
522423
+ if (this.session !== session) return;
522177
522424
  this.failSessionRequest(uiText("blunTui.session.sendFailed", { error: formatErrorMessage$2(error) }));
522178
522425
  });
522179
522426
  this.updateQueueDisplay();
@@ -522662,8 +522909,8 @@ var BlunTUI = class {
522662
522909
  onOwnershipLost: () => {
522663
522910
  if (this.telegramChannel === controller) this.telegramChannel = void 0;
522664
522911
  }
522665
- });
522666
- controller.start();
522912
+ }, telegramStateDir(), { sessionScope: () => this.deferUserMessages || this.session?.id !== this.telegramSessionScope?.sessionId ? undefined : this.telegramSessionScope });
522913
+ controller.start();
522667
522914
  this.telegramChannel = controller;
522668
522915
  this.showStatus(uiText("blunTui.telegram.attached", { pid: process.pid }));
522669
522916
  }
@@ -523269,27 +523516,10 @@ var BlunTUI = class {
523269
523516
  return this.harness.createSession(options);
523270
523517
  }
523271
523518
  async setSession(session, options = {}) {
523272
- const resumedStatus = await session.getStatus();
523273
- if (!isManagedBlunModelAlias(resumedStatus.model ?? BLUN_KING_MODEL_ALIAS, this.state.appState.availableModels)) {
523274
- await session.setModel(BLUN_KING_MODEL_ALIAS, { allowFallback: false });
523275
- this.showStatus(uiText("config.model.unknownAlias", { alias: resumedStatus.model ?? "" }), "warning");
523276
- }
523277
- await this.personalMemoryController.clear(this.session);
523278
- const previous = this.unloadCurrentSession(approvalCancellationFeedback("switching_session"));
523279
- await this.managedQuotaWarningPersistence;
523280
- await this.flushGoalChannelReplyPersistence();
523281
- await previous?.close();
523282
- resetChannelPreambleState(this.channelPreamble);
523283
- this.session = session;
523284
- this.managedQuotaWarningController.restore(session.id, managedQuotaWarningThresholdFromMetadata(session.getResumeState()?.sessionMetadata?.custom));
523285
- this.setAppState({ modelFallbackAllowed: false });
523286
- this.harness.setTelemetryContext({ sessionId: session.id });
523287
- this.registerSessionHandlers(session);
523288
- this.syncAdditionalDirs(session);
523289
- await this.refreshManagedImageReaderAvailability(session);
523290
- await this.authFlow.applyManagedAccountContextToSession();
523291
- if (options.refreshPersonalMemory !== false) await this.personalMemoryController.refresh();
523292
- }
523519
+ await this.withTelegramQueuePaused(async boundary => {
523520
+ await this.setSessionRuntime(session, options, options.telegramSessionScope ?? { sessionId: session.id, mode: "resume", boundary });
523521
+ });
523522
+ }
523293
523523
  async syncRuntimeState(session = this.requireSession()) {
523294
523524
  const [status, goalResult, loopResult] = await Promise.all([session.getStatus(), session.getGoal(), session.getLoop()]);
523295
523525
  this.setAppState({
@@ -523498,28 +523728,10 @@ var BlunTUI = class {
523498
523728
  return true;
523499
523729
  }
523500
523730
  async switchToSession(session, statusMessage) {
523501
- this.resetSessionRuntime();
523502
- await this.setSession(session);
523503
- await this.syncRuntimeState(session);
523504
- this.updateTerminalTitle();
523505
- try {
523506
- await this.refreshSkillCommands(this.session);
523507
- await this.refreshPluginCommands(this.session);
523508
- } catch {}
523509
- this.clearTranscriptAndRedraw({ preserveScrollbackArchive: true });
523510
- try {
523511
- await this.sessionReplay.hydrateFromReplay(session);
523512
- } catch (error) {
523513
- const msg = formatErrorMessage$2(error);
523514
- this.showError(uiText("blunTui.session.replayFailed", { error: msg }));
523515
- } finally {
523516
- this.sessionEventHandler.startSubscription();
523517
- }
523518
- const resumeState = session.getResumeState();
523519
- if (resumeState?.warning !== void 0) this.showStatus(uiText("blunTui.warning", { warning: resumeState.warning }), "warning");
523520
- this.showStatus(statusMessage);
523521
- this.showSessionWarnings(session);
523522
- }
523731
+ await this.withTelegramQueuePaused(async (boundary) => {
523732
+ await this.switchToSessionRuntime(session, statusMessage, boundary);
523733
+ });
523734
+ }
523523
523735
  async reloadCurrentSessionView(session, statusMessage) {
523524
523736
  await this.personalMemoryController.clear(session);
523525
523737
  if (this.goalChannelReplyScope?.session === session && this.goalChannelReplyScope.reloading) this.goalChannelReplyScope.restored = false;
@@ -523553,56 +523765,10 @@ var BlunTUI = class {
523553
523765
  this.showSessionWarnings(session);
523554
523766
  }
523555
523767
  async createNewSession() {
523556
- if (this.state.appState.isReplaying) {
523557
- this.showError(uiText("blunTui.session.createReplayBlocked"));
523558
- return;
523559
- }
523560
- this.clearQueuedMessages();
523561
- const activeSession = this.session;
523562
- const activeTurn = activeSession !== void 0 && (this.streamingUI.hasActiveTurn() || this.state.appState.streamingPhase !== "idle" || this.state.appState.isCompacting);
523563
- if (activeTurn) {
523564
- this.markCurrentRunCancelledByUser();
523565
- try {
523566
- await activeSession.cancel();
523567
- } catch (error) {
523568
- if (!(error instanceof BlunError && error.code === ErrorCodes.SESSION_NOT_FOUND)) {
523569
- this.showError(formatErrorMessage$2(error));
523570
- return;
523571
- }
523572
- }
523573
- this.clearTurnCancellation();
523574
- }
523575
- let session;
523576
- try {
523577
- session = await this.createSessionFromCurrentState();
523578
- } catch (error) {
523579
- const msg = formatErrorMessage$2(error);
523580
- this.showError(uiText("blunTui.session.createFailed", { error: msg }));
523581
- return;
523582
- }
523583
- this.resetSessionRuntime();
523584
- await this.setSession(session);
523585
- this.setAppState({ sessionId: session.id });
523586
- try {
523587
- await this.activateRuntime();
523588
- await this.syncRuntimeState(session);
523589
- } catch (error) {
523590
- this.sessionEventHandler.startSubscription();
523591
- const msg = formatErrorMessage$2(error);
523592
- this.showError(uiText("blunTui.session.postCreateFailed", { error: msg }));
523593
- return;
523594
- }
523595
- try {
523596
- await this.refreshSkillCommands(this.session);
523597
- await this.refreshPluginCommands(this.session);
523598
- } catch {}
523599
- this.sessionEventHandler.startSubscription();
523600
- this.clearTranscriptAndRedraw({ preserveScrollbackArchive: true });
523601
- this.scrollbackController.prepareNewSession(session);
523602
- this.showStatus(uiText("blunTui.session.started", { sessionId: session.id }));
523603
- this.showSessionWarnings(session);
523604
- this.showConfigWarningsIfAny();
523605
- }
523768
+ await this.withTelegramQueuePaused(async (boundary) => {
523769
+ await this.createNewSessionRuntime(boundary);
523770
+ });
523771
+ }
523606
523772
  /** Surface config.toml load warnings (degraded or kept-previous config) in the status bar. */
523607
523773
  async showConfigWarningsIfAny() {
523608
523774
  try {
@@ -524574,6 +524740,133 @@ var BlunTUI = class {
524574
524740
  this.patchLivePane({ pendingQuestion: null });
524575
524741
  this.restoreEditor();
524576
524742
  }
524743
+
524744
+ async switchToSessionRuntime(session, statusMessage, boundary) {
524745
+ this.resetSessionRuntime();
524746
+ await this.setSession(session, { telegramSessionScope: { sessionId: session.id, mode: "resume", boundary } });
524747
+ await this.syncRuntimeState(session);
524748
+ this.updateTerminalTitle();
524749
+ try {
524750
+ await this.refreshSkillCommands(this.session);
524751
+ await this.refreshPluginCommands(this.session);
524752
+ } catch {}
524753
+ this.clearTranscriptAndRedraw({ preserveScrollbackArchive: true });
524754
+ try {
524755
+ await this.sessionReplay.hydrateFromReplay(session);
524756
+ } catch (error) {
524757
+ const msg = formatErrorMessage$2(error);
524758
+ this.showError(uiText("blunTui.session.replayFailed", { error: msg }));
524759
+ } finally {
524760
+ this.sessionEventHandler.startSubscription();
524761
+ }
524762
+ const resumeState = session.getResumeState();
524763
+ if (resumeState?.warning !== void 0) this.showStatus(uiText("blunTui.warning", { warning: resumeState.warning }), "warning");
524764
+ this.showStatus(statusMessage);
524765
+ this.showSessionWarnings(session);
524766
+ }
524767
+
524768
+ async createNewSessionRuntime(boundary) {
524769
+ if (this.state.appState.isReplaying) {
524770
+ this.showError(uiText("blunTui.session.createReplayBlocked"));
524771
+ return;
524772
+ }
524773
+ const activeSession = this.session;
524774
+ const activeTurn = activeSession !== void 0 && (this.streamingUI.hasActiveTurn() || this.state.appState.streamingPhase !== "idle" || this.state.appState.isCompacting);
524775
+ if (activeTurn) {
524776
+ this.markCurrentRunCancelledByUser();
524777
+ try {
524778
+ await activeSession.cancel();
524779
+ } catch (error) {
524780
+ if (!(error instanceof BlunError && error.code === ErrorCodes.SESSION_NOT_FOUND)) {
524781
+ this.showError(formatErrorMessage$2(error));
524782
+ return;
524783
+ }
524784
+ }
524785
+ this.clearTurnCancellation();
524786
+ }
524787
+ let session;
524788
+ try {
524789
+ session = await this.createSessionFromCurrentState();
524790
+ } catch (error) {
524791
+ const msg = formatErrorMessage$2(error);
524792
+ this.showError(uiText("blunTui.session.createFailed", { error: msg }));
524793
+ return;
524794
+ }
524795
+ this.resetSessionRuntime();
524796
+ await this.setSession(session, { telegramSessionScope: { sessionId: session.id, mode: "new", boundary } });
524797
+ this.setAppState({ sessionId: session.id });
524798
+ try {
524799
+ await this.activateRuntime();
524800
+ await this.syncRuntimeState(session);
524801
+ } catch (error) {
524802
+ this.sessionEventHandler.startSubscription();
524803
+ const msg = formatErrorMessage$2(error);
524804
+ this.showError(uiText("blunTui.session.postCreateFailed", { error: msg }));
524805
+ return;
524806
+ }
524807
+ try {
524808
+ await this.refreshSkillCommands(this.session);
524809
+ await this.refreshPluginCommands(this.session);
524810
+ } catch {}
524811
+ this.sessionEventHandler.startSubscription();
524812
+ this.clearTranscriptAndRedraw({ preserveScrollbackArchive: true });
524813
+ this.scrollbackController.prepareNewSession(session);
524814
+ this.showStatus(uiText("blunTui.session.started", { sessionId: session.id }));
524815
+ this.showSessionWarnings(session);
524816
+ this.showConfigWarningsIfAny();
524817
+ }
524818
+
524819
+ async setSessionRuntime(session, options, queueScope) {
524820
+ const resumedStatus = await session.getStatus();
524821
+ if (!isManagedBlunModelAlias(resumedStatus.model ?? BLUN_KING_MODEL_ALIAS, this.state.appState.availableModels)) {
524822
+ await session.setModel(BLUN_KING_MODEL_ALIAS, { allowFallback: false });
524823
+ this.showStatus(uiText("config.model.unknownAlias", { alias: resumedStatus.model ?? "" }), "warning");
524824
+ }
524825
+ await this.personalMemoryController.clear(this.session);
524826
+ const previous = this.unloadCurrentSession(approvalCancellationFeedback("switching_session"));
524827
+ await this.managedQuotaWarningPersistence;
524828
+ await this.flushGoalChannelReplyPersistence();
524829
+ await previous?.close();
524830
+ if (previous?.id !== session.id) {
524831
+ this.state.queuedMessages = this.state.queuedMessages.filter(item => item.mode !== "channel" && item.mode !== "channel-command");
524832
+ this.queueFlushBatchRemaining = 0;
524833
+ this.queueSteerInFlight = undefined;
524834
+ this.channelPromptInFlight = undefined;
524835
+ this.deferredChannelAcknowledgements.clear();
524836
+ this.pendingChannelReplyGuard = undefined;
524837
+ this.syncChannelQueueDeadline();
524838
+ }
524839
+ resetChannelPreambleState(this.channelPreamble);
524840
+ this.session = session;
524841
+ this.managedQuotaWarningController.restore(session.id, managedQuotaWarningThresholdFromMetadata(session.getResumeState()?.sessionMetadata?.custom));
524842
+ this.setAppState({ modelFallbackAllowed: false });
524843
+ this.harness.setTelemetryContext({ sessionId: session.id });
524844
+ this.registerSessionHandlers(session);
524845
+ this.syncAdditionalDirs(session);
524846
+ await this.refreshManagedImageReaderAvailability(session);
524847
+ await this.authFlow.applyManagedAccountContextToSession();
524848
+ if (options.refreshPersonalMemory !== false) await this.personalMemoryController.refresh();
524849
+
524850
+ this.telegramSessionScope = queueScope;
524851
+ this.telegramChannel?.bindSessionQueue(queueScope);
524852
+ }
524853
+
524854
+ async withTelegramQueuePaused(work) {
524855
+ const boundary = this.captureTelegramQueueBoundary();
524856
+ const wasDeferred = this.deferUserMessages;
524857
+ this.deferUserMessages = true;
524858
+ try {
524859
+ return await work(boundary);
524860
+ }
524861
+ finally {
524862
+ this.deferUserMessages = wasDeferred;
524863
+ this.scheduleQueueDrain();
524864
+ }
524865
+ }
524866
+
524867
+ captureTelegramQueueBoundary() {
524868
+ return captureSessionQueueBoundary(join(telegramStateDir(), 'inbound-queue.jsonl')) ?? 'absent';
524869
+ }
524577
524870
  };
524578
524871
  //#endregion
524579
524872
  //#region src/cli/run-shell.ts
@@ -525292,3 +525585,23 @@ async function logStartupFailure(operation, error) {
525292
525585
  }
525293
525586
  //#endregion
525294
525587
  export { configureTelegramPluginLaunchMode, createBlunHarness, createBlunHostIdentity, handleMainCommand, isDirectCliEntry, main };
525588
+
525589
+ import { randomUUID as releasePromptUUID } from "node:crypto";
525590
+ function overlayRuntimeEnv(base, runtime) {
525591
+ const result = {};
525592
+ for (const [key, value] of Object.entries(base)) {
525593
+ if (value !== undefined)
525594
+ result[key] = value;
525595
+ }
525596
+ for (const [key, value] of Object.entries(runtime ?? {})) {
525597
+ // Node selects one casing on Windows; leaving both can preserve a stale value.
525598
+ if (process.platform === 'win32') {
525599
+ for (const existing of Object.keys(result)) {
525600
+ if (existing.toUpperCase() === key.toUpperCase())
525601
+ delete result[existing];
525602
+ }
525603
+ }
525604
+ result[key] = value;
525605
+ }
525606
+ return result;
525607
+ }