blun-king-cli 9.1.421 → 9.1.423

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
package/LIESMICH.txt CHANGED
@@ -106,6 +106,8 @@ ausdrücklichen Trigger-Beleg. Andere Trigger-Arten dürfen kein `dueAt`
106
106
  enthalten. Ein Laufzeit-Wecker für eine durchgehend geöffnete Sitzung ist in
107
107
  diesem Release noch nicht enthalten.
108
108
 
109
+ Ab BLUN King 9.1.422 überwacht auch eine durchgehend geöffnete, untätige Sitzung ihren gespeicherten Zeit-Trigger. Sobald `dueAt` erreicht oder überschritten ist, fügt der Auto- oder God-Modus genau eine verborgene Fortsetzung in dieselbe Sitzung ein. Wird der Zeitpunkt während einer laufenden Antwort, Verdichtung, wartenden Nachricht, eines Befehls oder Dialogs fällig, wartet die Fortsetzung bis zum Leerlauf. Unmittelbar vor dem Start liest King Ziel, Checkpoint-Revision, Fälligkeit, Berechtigungsmodus und Sitzung erneut; ein geänderter oder veralteter Trigger kann deshalb nicht auslösen. Stoppen, Notausgang, Entladen und Wechseln der Sitzung entsorgen den Timer. Im manuellen Modus erfolgt kein selbstständiger Start.
110
+
109
111
  Zuverlässiger King-Start
110
112
  -----------------------
111
113
  Bei einer ausdrücklich als wiederholbar gekennzeichneten Serverüberlastung
package/README.md CHANGED
@@ -125,6 +125,8 @@ ausdrücklichen Trigger-Beleg. Andere Trigger-Arten dürfen kein `dueAt`
125
125
  enthalten. Ein Laufzeit-Wecker für eine durchgehend geöffnete Sitzung ist in
126
126
  diesem Release noch nicht enthalten.
127
127
 
128
+ Ab BLUN King 9.1.422 überwacht auch eine durchgehend geöffnete, untätige Sitzung ihren gespeicherten Zeit-Trigger. Sobald `dueAt` erreicht oder überschritten ist, fügt der Auto- oder God-Modus genau eine verborgene Fortsetzung in dieselbe Sitzung ein. Wird der Zeitpunkt während einer laufenden Antwort, Verdichtung, wartenden Nachricht, eines Befehls oder Dialogs fällig, wartet die Fortsetzung bis zum Leerlauf. Unmittelbar vor dem Start liest King Ziel, Checkpoint-Revision, Fälligkeit, Berechtigungsmodus und Sitzung erneut; ein geänderter oder veralteter Trigger kann deshalb nicht auslösen. Stoppen, Notausgang, Entladen und Wechseln der Sitzung entsorgen den Timer. Im manuellen Modus erfolgt kein selbstständiger Start.
129
+
128
130
  ## Zuverlässiger King-Start
129
131
 
130
132
  Bei einer vom Server ausdrücklich als wiederholbar gekennzeichneten
@@ -7,6 +7,40 @@ const START = Object.freeze({ kind: 'start', trigger: 'immediate' });
7
7
  const WAIT = Object.freeze({ kind: 'wait' });
8
8
  const CONTINUE = Object.freeze({ kind: 'continue' });
9
9
  const YIELD = Object.freeze({ kind: 'yield' });
10
+ const IGNORE = Object.freeze({ kind: 'ignore' });
11
+
12
+ function goalTimeTriggerDecision({ goal, permissionMode, now = new Date() } = {}) {
13
+ if (!goal || typeof goal !== 'object' || Array.isArray(goal)) return IGNORE;
14
+ if (goal.status !== 'active' || !AUTONOMOUS_PERMISSION_MODES.has(permissionMode)) return IGNORE;
15
+ if (typeof goal.goalId !== 'string' || goal.goalId.trim().length === 0) return IGNORE;
16
+
17
+ const checkpoint = goal.actionCheckpoint;
18
+ if (!checkpoint || typeof checkpoint !== 'object' || Array.isArray(checkpoint)) return IGNORE;
19
+ if (!Number.isInteger(checkpoint.revision) || checkpoint.revision < 1) return IGNORE;
20
+ if (checkpoint.phase !== 'wait') return IGNORE;
21
+ const trigger = checkpoint.nextTrigger;
22
+ if (!trigger || typeof trigger !== 'object' || Array.isArray(trigger)) return IGNORE;
23
+ if (trigger.kind !== 'time' || typeof trigger.condition !== 'string' || trigger.condition.trim().length === 0) return IGNORE;
24
+
25
+ const dueAt = String(trigger.dueAt ?? '');
26
+ if (!ISO_TIMESTAMP_RE.test(dueAt)) return IGNORE;
27
+ const dueAtMs = new Date(dueAt).getTime();
28
+ const nowMs = new Date(now).getTime();
29
+ if (!Number.isFinite(dueAtMs) || !Number.isFinite(nowMs)) return IGNORE;
30
+ const key = `${goal.goalId}:${checkpoint.revision}:${dueAt}`;
31
+ if (dueAtMs > nowMs) return Object.freeze({
32
+ kind: 'schedule',
33
+ key,
34
+ dueAt,
35
+ delayMs: dueAtMs - nowMs,
36
+ });
37
+ return Object.freeze({
38
+ kind: 'due',
39
+ key,
40
+ dueAt,
41
+ observation: `The checkpointed time trigger became due at ${dueAt}.`,
42
+ });
43
+ }
10
44
 
11
45
  function goalAutostartDecision({ goal, permissionMode, now = new Date() } = {}) {
12
46
  if (!goal || typeof goal !== 'object' || Array.isArray(goal)) return WAIT;
@@ -18,18 +52,13 @@ function goalAutostartDecision({ goal, permissionMode, now = new Date() } = {})
18
52
  if (!trigger || typeof trigger !== 'object' || Array.isArray(trigger)) return WAIT;
19
53
  if (typeof trigger.condition !== 'string' || trigger.condition.trim().length === 0) return WAIT;
20
54
  if (ACTIVE_PHASES.has(checkpoint.phase) && trigger.kind === 'immediate') return START;
21
- if (checkpoint.phase !== 'wait' || trigger.kind !== 'time') return WAIT;
22
-
23
- const dueAt = String(trigger.dueAt ?? '');
24
- if (!ISO_TIMESTAMP_RE.test(dueAt)) return WAIT;
25
- const dueAtMs = new Date(dueAt).getTime();
26
- const nowMs = new Date(now).getTime();
27
- if (!Number.isFinite(dueAtMs) || !Number.isFinite(nowMs) || dueAtMs > nowMs) return WAIT;
55
+ const timed = goalTimeTriggerDecision({ goal, permissionMode, now });
56
+ if (timed.kind !== 'due') return WAIT;
28
57
  return Object.freeze({
29
58
  kind: 'start',
30
59
  trigger: 'time',
31
- dueAt,
32
- observation: `The checkpointed time trigger became due at ${dueAt}.`,
60
+ dueAt: timed.dueAt,
61
+ observation: timed.observation,
33
62
  });
34
63
  }
35
64
 
@@ -40,4 +69,4 @@ function goalContinuationDecision({ goal } = {}) {
40
69
  return checkpoint.phase === 'wait' ? YIELD : CONTINUE;
41
70
  }
42
71
 
43
- module.exports = { goalAutostartDecision, goalContinuationDecision };
72
+ module.exports = { goalAutostartDecision, goalContinuationDecision, goalTimeTriggerDecision };
@@ -0,0 +1,146 @@
1
+ 'use strict';
2
+
3
+ const { goalTimeTriggerDecision } = require('./cognitive-goal-autostart-policy.cjs');
4
+
5
+ const MAX_TIMER_DELAY_MS = 2_147_483_647;
6
+
7
+ class GoalTimeTriggerController {
8
+ #readState;
9
+ #start;
10
+ #now;
11
+ #setTimer;
12
+ #clearTimer;
13
+ #onError;
14
+ #retryDelayMs;
15
+ #timer;
16
+ #timerKey;
17
+ #inFlightKey;
18
+ #startedKey;
19
+ #generation = 0;
20
+ #disposed = false;
21
+
22
+ constructor({
23
+ readState,
24
+ start,
25
+ now = () => new Date(),
26
+ setTimer = setTimeout,
27
+ clearTimer = clearTimeout,
28
+ onError = () => {},
29
+ retryDelayMs = 1_000,
30
+ } = {}) {
31
+ if (typeof readState !== 'function') throw new TypeError('readState must be a function');
32
+ if (typeof start !== 'function') throw new TypeError('start must be a function');
33
+ this.#readState = readState;
34
+ this.#start = start;
35
+ this.#now = now;
36
+ this.#setTimer = setTimer;
37
+ this.#clearTimer = clearTimer;
38
+ this.#onError = onError;
39
+ this.#retryDelayMs = retryDelayMs;
40
+ }
41
+
42
+ sync() {
43
+ if (this.#disposed) return undefined;
44
+ const state = this.#readState() ?? {};
45
+ const decision = goalTimeTriggerDecision({
46
+ goal: state.goal,
47
+ permissionMode: state.permissionMode,
48
+ now: this.#now(),
49
+ });
50
+ if (decision.kind === 'ignore' || typeof state.sessionId !== 'string' || state.sessionId.length === 0) {
51
+ this.#clearScheduledTimer();
52
+ return undefined;
53
+ }
54
+
55
+ const key = `${state.sessionId}:${decision.key}`;
56
+ if (this.#startedKey === key || this.#inFlightKey === key) {
57
+ this.#clearScheduledTimer();
58
+ return undefined;
59
+ }
60
+ if (decision.kind === 'schedule') {
61
+ this.#schedule(key, Math.min(MAX_TIMER_DELAY_MS, Math.max(0, decision.delayMs)));
62
+ return undefined;
63
+ }
64
+
65
+ this.#clearScheduledTimer();
66
+ if (state.idle !== true) return undefined;
67
+ return this.#startDueTrigger(key, decision);
68
+ }
69
+
70
+ reset() {
71
+ this.#generation += 1;
72
+ this.#clearScheduledTimer();
73
+ this.#inFlightKey = undefined;
74
+ this.#startedKey = undefined;
75
+ }
76
+
77
+ dispose() {
78
+ this.reset();
79
+ this.#disposed = true;
80
+ }
81
+
82
+ #schedule(key, delayMs) {
83
+ if (this.#timer !== undefined && this.#timerKey === key) return;
84
+ this.#clearScheduledTimer();
85
+ this.#timerKey = key;
86
+ this.#timer = this.#setTimer(() => {
87
+ this.#timer = undefined;
88
+ this.#timerKey = undefined;
89
+ void this.sync();
90
+ }, delayMs);
91
+ this.#timer?.unref?.();
92
+ }
93
+
94
+ #startDueTrigger(key, decision) {
95
+ const generation = this.#generation;
96
+ this.#inFlightKey = key;
97
+ const trigger = {
98
+ key,
99
+ trigger: 'time',
100
+ dueAt: decision.dueAt,
101
+ observation: decision.observation,
102
+ };
103
+ let result;
104
+ try {
105
+ result = this.#start(trigger);
106
+ } catch (error) {
107
+ result = Promise.reject(error);
108
+ }
109
+ return Promise.resolve(result).then((accepted) => {
110
+ if (this.#disposed || generation !== this.#generation || this.#inFlightKey !== key) return false;
111
+ this.#inFlightKey = undefined;
112
+ if (accepted === true) {
113
+ this.#startedKey = key;
114
+ return true;
115
+ }
116
+ this.#scheduleRetry(key);
117
+ return false;
118
+ }, (error) => {
119
+ if (!this.#disposed && generation === this.#generation && this.#inFlightKey === key) {
120
+ this.#inFlightKey = undefined;
121
+ this.#onError(error);
122
+ this.#scheduleRetry(key);
123
+ }
124
+ return false;
125
+ });
126
+ }
127
+
128
+ #scheduleRetry(key) {
129
+ const state = this.#readState() ?? {};
130
+ const decision = goalTimeTriggerDecision({
131
+ goal: state.goal,
132
+ permissionMode: state.permissionMode,
133
+ now: this.#now(),
134
+ });
135
+ if (decision.kind !== 'due' || `${state.sessionId}:${decision.key}` !== key) return;
136
+ this.#schedule(key, this.#retryDelayMs);
137
+ }
138
+
139
+ #clearScheduledTimer() {
140
+ if (this.#timer !== undefined) this.#clearTimer(this.#timer);
141
+ this.#timer = undefined;
142
+ this.#timerKey = undefined;
143
+ }
144
+ }
145
+
146
+ module.exports = { GoalTimeTriggerController, MAX_TIMER_DELAY_MS };
@@ -29,6 +29,7 @@ const { CORE_LOADED_MESSAGE } = require('./core-bootstrap');
29
29
  const { prepareManagedNodeRuntime } = require('./node-runtime');
30
30
  const { repairConfiguredNativeModules } = require('./native-module-repair');
31
31
  const { startMnemoConnectHeartbeat } = require('./mnemo-connect-heartbeat.cjs');
32
+ const { recordRuntimeExit } = require('./runtime-exit-ledger.cjs');
32
33
  const { acquireSharedRuntimeLease } = require('./update-lease');
33
34
  const { runExplicitUpdate, runUpdateNotice } = require('./update-notice');
34
35
  const {
@@ -218,8 +219,35 @@ async function superviseProtectedCore(args, env, cwd, releaseLauncherLeases, opt
218
219
  const loaded = await core.loaded;
219
220
  if (loaded) await releaseLauncherLeases();
220
221
  const result = await core.completed;
221
- if (result.error) throw result.error;
222
- return exitCodeForChild(result, loaded);
222
+ if (result.error) {
223
+ recordRuntimeExit({
224
+ homeDir: env.BLUN_HOME,
225
+ source: 'launcher',
226
+ kind: 'child-error',
227
+ exitCode: 1,
228
+ phase: loaded ? 'runtime' : 'startup',
229
+ cliVersion: env.BLUN_PUBLIC_PACKAGE_VERSION,
230
+ profile: env.BLUN_PROFILE,
231
+ loaded,
232
+ childPid: core.child?.pid,
233
+ error: result.error,
234
+ });
235
+ throw result.error;
236
+ }
237
+ const exitCode = exitCodeForChild(result, loaded);
238
+ recordRuntimeExit({
239
+ homeDir: env.BLUN_HOME,
240
+ source: 'launcher',
241
+ kind: 'child-exit',
242
+ exitCode,
243
+ signal: result.signal,
244
+ phase: loaded ? 'runtime' : 'startup',
245
+ cliVersion: env.BLUN_PUBLIC_PACKAGE_VERSION,
246
+ profile: env.BLUN_PROFILE,
247
+ loaded,
248
+ childPid: core.child?.pid,
249
+ });
250
+ return exitCode;
223
251
  }
224
252
 
225
253
  function installTelegramForProfile(packageRoot, blunDir) {
@@ -0,0 +1,143 @@
1
+ 'use strict';
2
+
3
+ const fs = require('node:fs');
4
+ const os = require('node:os');
5
+ const path = require('node:path');
6
+
7
+ const DEFAULT_MAX_BYTES = 1024 * 1024;
8
+ const DEFAULT_RETAIN_BYTES = 512 * 1024;
9
+ const MAX_MESSAGE_CHARS = 2048;
10
+ const MAX_STACK_CHARS = 8192;
11
+ const MAX_DETAIL_CHARS = 2048;
12
+
13
+ function runtimeExitLedgerPath(homeDir) {
14
+ return path.join(homeDir, 'diagnostics', 'runtime-exits.jsonl');
15
+ }
16
+
17
+ function replaceAllLiteral(value, needle, replacement) {
18
+ if (!needle) return value;
19
+ const lowerValue = process.platform === 'win32' ? value.toLowerCase() : value;
20
+ const lowerNeedle = process.platform === 'win32' ? needle.toLowerCase() : needle;
21
+ let result = '';
22
+ let cursor = 0;
23
+ for (;;) {
24
+ const index = lowerValue.indexOf(lowerNeedle, cursor);
25
+ if (index === -1) return `${result}${value.slice(cursor)}`;
26
+ result += `${value.slice(cursor, index)}${replacement}`;
27
+ cursor = index + needle.length;
28
+ }
29
+ }
30
+
31
+ function sanitizeExitText(input, maxChars = MAX_DETAIL_CHARS) {
32
+ let value = String(input ?? '');
33
+ const homes = new Set([
34
+ os.homedir(),
35
+ process.env.USERPROFILE,
36
+ process.env.HOME,
37
+ ].filter((entry) => typeof entry === 'string' && entry.length > 2));
38
+ for (const home of homes) {
39
+ value = replaceAllLiteral(value, path.normalize(home), '$HOME');
40
+ value = replaceAllLiteral(value, home.replaceAll('\\', '/'), '$HOME');
41
+ }
42
+ value = value
43
+ .replace(/[A-Za-z]:\\Users\\[^\\\s"']+/giu, '$HOME')
44
+ .replace(/\/home\/[^/\s"']+/gu, '$HOME')
45
+ .replace(/(Authorization\s*:\s*Bearer\s+)[^\s,;]+/giu, '$1<redacted>')
46
+ .replace(/\bsk-[A-Za-z0-9_-]{10,}\b/gu, '<redacted>')
47
+ .replace(/\b\d{6,}:[A-Za-z0-9_-]{20,}\b/gu, '<redacted>')
48
+ .replace(/((?:api[_-]?key|access[_-]?token|refresh[_-]?token|token|secret|password)\s*[=:]\s*)[^\s,;}]+/giu, '$1<redacted>')
49
+ .replace(/(https?:\/\/[^\s/:@]+:)[^\s/@]+@/giu, '$1<redacted>@');
50
+ return value.length <= maxChars ? value : `${value.slice(0, maxChars)}...[truncated]`;
51
+ }
52
+
53
+ function normalizedError(error) {
54
+ if (error === undefined || error === null) return undefined;
55
+ if (error instanceof Error) {
56
+ return {
57
+ name: sanitizeExitText(error.name || error.constructor?.name || 'Error', 160),
58
+ message: sanitizeExitText(error.message || String(error), MAX_MESSAGE_CHARS),
59
+ stack: sanitizeExitText(error.stack || '', MAX_STACK_CHARS),
60
+ };
61
+ }
62
+ return {
63
+ name: typeof error === 'object' && typeof error.name === 'string'
64
+ ? sanitizeExitText(error.name, 160)
65
+ : 'NonErrorRejection',
66
+ message: sanitizeExitText(error, MAX_MESSAGE_CHARS),
67
+ };
68
+ }
69
+
70
+ function finiteInteger(value) {
71
+ return Number.isInteger(value) ? value : undefined;
72
+ }
73
+
74
+ function buildRuntimeExitRecord(input) {
75
+ const now = typeof input.now === 'function' ? input.now() : new Date();
76
+ const record = {
77
+ schemaVersion: 1,
78
+ ts: (now instanceof Date ? now : new Date(now)).toISOString(),
79
+ source: sanitizeExitText(input.source || 'unknown', 80),
80
+ kind: sanitizeExitText(input.kind || 'unknown', 120),
81
+ exitCode: finiteInteger(input.exitCode),
82
+ signal: input.signal ? sanitizeExitText(input.signal, 40) : undefined,
83
+ phase: input.phase ? sanitizeExitText(input.phase, 80) : undefined,
84
+ cliVersion: input.cliVersion ? sanitizeExitText(input.cliVersion, 80) : undefined,
85
+ profile: input.profile ? sanitizeExitText(input.profile, 120) : undefined,
86
+ sessionId: input.sessionId ? sanitizeExitText(input.sessionId, 240) : undefined,
87
+ pid: finiteInteger(input.pid ?? process.pid),
88
+ ppid: finiteInteger(input.ppid ?? process.ppid),
89
+ childPid: finiteInteger(input.childPid),
90
+ loaded: typeof input.loaded === 'boolean' ? input.loaded : undefined,
91
+ detail: input.detail ? sanitizeExitText(input.detail, MAX_DETAIL_CHARS) : undefined,
92
+ error: normalizedError(input.error),
93
+ };
94
+ return Object.fromEntries(Object.entries(record).filter(([, value]) => value !== undefined && value !== ''));
95
+ }
96
+
97
+ function trimLedger(filePath, maxBytes, retainBytes, incomingBytes) {
98
+ let size;
99
+ try {
100
+ size = fs.statSync(filePath).size;
101
+ } catch {
102
+ return;
103
+ }
104
+ if (size + incomingBytes <= maxBytes) return;
105
+ const contents = fs.readFileSync(filePath);
106
+ const keepFrom = Math.max(0, contents.length - Math.min(retainBytes, maxBytes - incomingBytes));
107
+ let lineStart = keepFrom;
108
+ if (lineStart > 0) {
109
+ const nextNewline = contents.indexOf(0x0a, lineStart);
110
+ lineStart = nextNewline === -1 ? contents.length : nextNewline + 1;
111
+ }
112
+ fs.writeFileSync(filePath, contents.subarray(lineStart), { mode: 0o600 });
113
+ }
114
+
115
+ function recordRuntimeExit(input = {}) {
116
+ try {
117
+ const homeDir = typeof input.homeDir === 'string' ? input.homeDir.trim() : '';
118
+ if (!homeDir) return false;
119
+ const filePath = runtimeExitLedgerPath(homeDir);
120
+ fs.mkdirSync(path.dirname(filePath), { recursive: true, mode: 0o700 });
121
+ const line = `${JSON.stringify(buildRuntimeExitRecord(input))}\n`;
122
+ const incomingBytes = Buffer.byteLength(line);
123
+ const maxBytes = Math.max(1024, finiteInteger(input.maxBytes) ?? DEFAULT_MAX_BYTES);
124
+ const retainBytes = Math.max(0, finiteInteger(input.retainBytes) ?? DEFAULT_RETAIN_BYTES);
125
+ trimLedger(filePath, maxBytes, retainBytes, incomingBytes);
126
+ fs.appendFileSync(filePath, line, { encoding: 'utf8', mode: 0o600 });
127
+ try {
128
+ fs.chmodSync(filePath, 0o600);
129
+ } catch {}
130
+ return true;
131
+ } catch {
132
+ return false;
133
+ }
134
+ }
135
+
136
+ module.exports = {
137
+ DEFAULT_MAX_BYTES,
138
+ DEFAULT_RETAIN_BYTES,
139
+ buildRuntimeExitRecord,
140
+ recordRuntimeExit,
141
+ runtimeExitLedgerPath,
142
+ sanitizeExitText,
143
+ };
package/blun.mjs CHANGED
@@ -7,6 +7,8 @@ const __dirname = __cjsShimDirname(__filename);
7
7
  import { createRequire } from "node:module";
8
8
  import telegramConsoleStatusPolicy from "./bin/telegram-console-status-policy.cjs";
9
9
  import cognitiveGoalAutostartPolicy from "./bin/cognitive-goal-autostart-policy.cjs";
10
+ import cognitiveGoalTimeTriggerController from "./bin/cognitive-goal-time-trigger-controller.cjs";
11
+ import runtimeExitLedger from "./bin/runtime-exit-ledger.cjs";
10
12
  import crypto$1, { createHash, randomBytes, randomInt, randomUUID, timingSafeEqual } from "node:crypto";
11
13
  import * as fs$16 from "node:fs";
12
14
  import Kt, { accessSync, appendFileSync, chmodSync, closeSync, constants, copyFileSync, createReadStream, createWriteStream, existsSync, fsyncSync, linkSync, lstatSync, mkdirSync, openSync, promises, readFileSync, readSync, readdirSync, realpathSync, renameSync, rmSync, statSync, unlinkSync, writeFileSync, writeSync } from "node:fs";
@@ -38,6 +40,8 @@ import { createServer } from "node:http";
38
40
  import { pipeline as pipeline$1 } from "node:stream/promises";
39
41
  const { writeTelegramConsoleStatus } = telegramConsoleStatusPolicy;
40
42
  const { goalAutostartDecision, goalContinuationDecision } = cognitiveGoalAutostartPolicy;
43
+ const { GoalTimeTriggerController } = cognitiveGoalTimeTriggerController;
44
+ const { recordRuntimeExit } = runtimeExitLedger;
41
45
  import { EventEmitter as EventEmitter$1 } from "node:events";
42
46
  import { StringDecoder } from "node:string_decoder";
43
47
  import co from "node:assert";
@@ -507786,6 +507790,7 @@ var SessionEventHandler = class {
507786
507790
  this.handleEvent(event, sendQueued);
507787
507791
  });
507788
507792
  this.syncMcpServerStatusSnapshot(session);
507793
+ this.host.syncGoalTimeTrigger();
507789
507794
  }
507790
507795
  async syncMcpServerStatusSnapshot(session) {
507791
507796
  const { host } = this;
@@ -508289,6 +508294,7 @@ var SessionEventHandler = class {
508289
508294
  }
508290
508295
  handleGoalUpdated(event) {
508291
508296
  this.host.setAppState({ goal: event.snapshot });
508297
+ this.host.syncGoalTimeTrigger();
508292
508298
  if (event.snapshot === null && this.goalCompletionAwaitingClear) {
508293
508299
  this.goalCompletionAwaitingClear = false;
508294
508300
  this.queuedGoalPromotionPending = true;
@@ -516160,6 +516166,7 @@ var BlunTUI = class {
516160
516166
  personalMemoryController;
516161
516167
  customerMistakeController;
516162
516168
  managedQuotaWarningController;
516169
+ goalTimeTriggerController;
516163
516170
  managedQuotaWarningPersistence = Promise.resolve();
516164
516171
  footerMounted = false;
516165
516172
  /** Timer that auto-clears the one-shot "moved to background" footer hint. */
@@ -516263,6 +516270,13 @@ var BlunTUI = class {
516263
516270
  }
516264
516271
  }));
516265
516272
  this.streamingUI = new StreamingUIController(this);
516273
+ this.goalTimeTriggerController = new GoalTimeTriggerController({
516274
+ readState: () => this.readGoalTimeTriggerState(),
516275
+ start: (trigger) => this.startAutonomousGoalContinuation(trigger),
516276
+ onError: (error) => {
516277
+ this.track("goal_time_trigger_failed", { error_type: error?.code ?? error?.name ?? "Error" });
516278
+ }
516279
+ });
516266
516280
  this.authFlow = new AuthFlowController(this);
516267
516281
  this.personalMemoryController = new PersonalMemoryController(this);
516268
516282
  this.customerMistakeController = new CustomerMistakeHostController();
@@ -516685,6 +516699,41 @@ var BlunTUI = class {
516685
516699
  });
516686
516700
  return result.kind === "selected" ? result.path : void 0;
516687
516701
  }
516702
+ readGoalTimeTriggerState() {
516703
+ return {
516704
+ sessionId: this.session?.id ?? "",
516705
+ goal: this.state.appState.goal,
516706
+ permissionMode: this.state.appState.permissionMode,
516707
+ idle: this.session !== void 0 && !this.isShuttingDown && !this.aborted && !this.queueCommandRunning && this.queueSteerInFlight === void 0 && !this.editorReplacementActive && !this.deferUserMessages && !this.streamingUI.hasActiveTurn() && this.state.appState.streamingPhase === "idle" && !this.state.appState.isCompacting && this.state.queuedMessages.length === 0
516708
+ };
516709
+ }
516710
+ syncGoalTimeTrigger() {
516711
+ if (this.isShuttingDown) return;
516712
+ void this.goalTimeTriggerController?.sync();
516713
+ }
516714
+ async startAutonomousGoalContinuation(autostart) {
516715
+ const session = this.session;
516716
+ if (session === void 0) return false;
516717
+ const sessionId = session.id;
516718
+ const autostartPrompt = autostart.observation === void 0 ? GOAL_CONTINUATION_PROMPT : `${GOAL_CONTINUATION_PROMPT}\n\n${autostart.observation}`;
516719
+ this.beginSessionRequest();
516720
+ this.setAppState({
516721
+ model: BLUN_KING_MODEL_ALIAS,
516722
+ modelFallbackAllowed: false
516723
+ });
516724
+ try {
516725
+ const result = await session.promptAccepted(autostartPrompt);
516726
+ if (!result.accepted && this.session?.id === sessionId) {
516727
+ this.setAppState({ streamingPhase: "idle" });
516728
+ this.resetLivePane();
516729
+ this.track("goal_autostart", { outcome: "not_accepted" });
516730
+ }
516731
+ return result.accepted === true;
516732
+ } catch (error) {
516733
+ if (this.session?.id === sessionId) this.failSessionRequest(uiText("blunTui.session.sendFailed", { error: formatErrorMessage$2(error) }));
516734
+ return false;
516735
+ }
516736
+ }
516688
516737
  async promptStartupResumeGoalIfNeeded() {
516689
516738
  const session = this.session;
516690
516739
  const goal = this.state.appState.goal;
@@ -516696,25 +516745,12 @@ var BlunTUI = class {
516696
516745
  now: new Date()
516697
516746
  });
516698
516747
  if (autostart.kind === "start") {
516699
- const autostartPrompt = autostart.observation === void 0 ? GOAL_CONTINUATION_PROMPT : `${GOAL_CONTINUATION_PROMPT}\n\n${autostart.observation}`;
516700
516748
  this.startupGoalPromptedSessionId = sessionId;
516701
- this.beginSessionRequest();
516702
- this.setAppState({
516703
- model: BLUN_KING_MODEL_ALIAS,
516704
- modelFallbackAllowed: false
516705
- });
516706
- try {
516707
- const result = await session.promptAccepted(autostartPrompt);
516708
- if (!result.accepted && this.session?.id === sessionId) {
516709
- this.setAppState({ streamingPhase: "idle" });
516710
- this.resetLivePane();
516711
- this.track("goal_autostart", { outcome: "not_accepted" });
516712
- }
516713
- } catch (error) {
516714
- if (this.session?.id === sessionId) this.failSessionRequest(uiText("blunTui.session.sendFailed", { error: formatErrorMessage$2(error) }));
516715
- }
516749
+ if (autostart.trigger === "time") await this.goalTimeTriggerController.sync();
516750
+ else await this.startAutonomousGoalContinuation(autostart);
516716
516751
  return;
516717
516752
  }
516753
+ this.syncGoalTimeTrigger();
516718
516754
  if (goal.status !== "paused" && goal.status !== "blocked") return;
516719
516755
  this.startupGoalPromptedSessionId = sessionId;
516720
516756
  const choice = await promptStartupResumeGoal(this, goal.objective, startupResumeGoalPromptCopy());
@@ -516728,6 +516764,7 @@ var BlunTUI = class {
516728
516764
  async stop(exitCode) {
516729
516765
  if (this.isShuttingDown) return;
516730
516766
  this.isShuttingDown = true;
516767
+ this.goalTimeTriggerController.dispose();
516731
516768
  await this.flushInputDraft();
516732
516769
  this.unregisterSignalHandlers();
516733
516770
  this.aborted = true;
@@ -516773,6 +516810,17 @@ var BlunTUI = class {
516773
516810
  if (process.platform !== "win32") signals.push("SIGHUP");
516774
516811
  for (const signal of signals) {
516775
516812
  const handler = () => {
516813
+ recordRuntimeExit({
516814
+ homeDir: process.env["BLUN_HOME"],
516815
+ source: "core",
516816
+ kind: "signal",
516817
+ exitCode: signal === "SIGTERM" ? 143 : 129,
516818
+ signal,
516819
+ phase: "runtime",
516820
+ cliVersion: process.env["BLUN_PUBLIC_PACKAGE_VERSION"],
516821
+ profile: process.env["BLUN_PROFILE"],
516822
+ sessionId: this.getCurrentSessionId()
516823
+ });
516776
516824
  if (signal === "SIGHUP") {
516777
516825
  this.emergencyTerminalExit();
516778
516826
  return;
@@ -516789,7 +516837,20 @@ var BlunTUI = class {
516789
516837
  });
516790
516838
  }
516791
516839
  const terminalErrorHandler = (error) => {
516792
- if (isDeadTerminalError(error)) this.emergencyTerminalExit();
516840
+ if (isDeadTerminalError(error)) {
516841
+ recordRuntimeExit({
516842
+ homeDir: process.env["BLUN_HOME"],
516843
+ source: "core",
516844
+ kind: "dead-terminal",
516845
+ exitCode: 129,
516846
+ phase: "runtime",
516847
+ cliVersion: process.env["BLUN_PUBLIC_PACKAGE_VERSION"],
516848
+ profile: process.env["BLUN_PROFILE"],
516849
+ sessionId: this.getCurrentSessionId(),
516850
+ error
516851
+ });
516852
+ this.emergencyTerminalExit();
516853
+ }
516793
516854
  };
516794
516855
  process.stdout.on("error", terminalErrorHandler);
516795
516856
  process.stderr.on("error", terminalErrorHandler);
@@ -516807,6 +516868,7 @@ var BlunTUI = class {
516807
516868
  }
516808
516869
  emergencyTerminalExit(exitCode = 129) {
516809
516870
  this.isShuttingDown = true;
516871
+ this.goalTimeTriggerController.dispose();
516810
516872
  this.unregisterSignalHandlers();
516811
516873
  this.channelQueueDeadline?.dispose();
516812
516874
  this.directFocusController?.dispose();
@@ -518154,6 +518216,7 @@ var BlunTUI = class {
518154
518216
  if (!hasPatchChanges(this.state.appState, effectivePatch)) return;
518155
518217
  const additionalDirsChanged = "additionalDirs" in effectivePatch && !sameStringArrays(this.state.appState.additionalDirs, effectivePatch.additionalDirs ?? []);
518156
518218
  const busyChanged = "streamingPhase" in effectivePatch || "isCompacting" in effectivePatch;
518219
+ const goalTimeTriggerChanged = busyChanged || "permissionMode" in effectivePatch;
518157
518220
  Object.assign(this.state.appState, effectivePatch);
518158
518221
  if ("planMode" in effectivePatch) this.updateEditorBorderHighlight();
518159
518222
  this.state.loopIndicator.setState(this.state.appState);
@@ -518165,6 +518228,7 @@ var BlunTUI = class {
518165
518228
  this.updateQueueDisplay();
518166
518229
  this.sessionEventHandler.retryQueuedGoalPromotion();
518167
518230
  }
518231
+ if (goalTimeTriggerChanged) queueMicrotask(() => this.syncGoalTimeTrigger());
518168
518232
  if (additionalDirsChanged) this.setupAutocomplete();
518169
518233
  this.state.ui.requestRender();
518170
518234
  }
@@ -518303,6 +518367,7 @@ var BlunTUI = class {
518303
518367
  }
518304
518368
  unloadCurrentSession(reason) {
518305
518369
  const previous = this.session;
518370
+ this.goalTimeTriggerController.reset();
518306
518371
  this.managedQuotaWarningController.dismiss();
518307
518372
  this.sessionEventUnsubscribe?.();
518308
518373
  this.sessionEventUnsubscribe = void 0;
@@ -518355,6 +518420,7 @@ var BlunTUI = class {
518355
518420
  }
518356
518421
  resetSessionRuntime() {
518357
518422
  this.aborted = false;
518423
+ this.goalTimeTriggerController.reset();
518358
518424
  this.managedQuotaWarningController.dismiss();
518359
518425
  this.streamingUI.discardPending();
518360
518426
  this.streamingUI.disposeActiveCompactionBlock();
@@ -519548,6 +519614,15 @@ async function runShell(opts, version) {
519548
519614
  const trackLifecycle = (event, properties) => {
519549
519615
  trackLifecycleForSession(tui.getCurrentSessionId(), event, properties);
519550
519616
  };
519617
+ const recordShellExit = (entry) => recordRuntimeExit({
519618
+ homeDir: process.env["BLUN_HOME"] ?? telemetryBootstrap.homeDir,
519619
+ source: "core",
519620
+ phase: "runtime",
519621
+ cliVersion: version,
519622
+ profile: process.env["BLUN_PROFILE"],
519623
+ sessionId: tui.getCurrentSessionId(),
519624
+ ...entry
519625
+ });
519551
519626
  let savedStty;
519552
519627
  try {
519553
519628
  const saved = execSync("stty -g", {
@@ -519581,12 +519656,22 @@ async function runShell(opts, version) {
519581
519656
  process.exit(exitCode);
519582
519657
  };
519583
519658
  const onUncaughtException = (error) => {
519659
+ recordShellExit({
519660
+ kind: "uncaught-exception",
519661
+ exitCode: 1,
519662
+ error
519663
+ });
519584
519664
  try {
519585
519665
  log.error("uncaughtException, restoring terminal and exiting", { error: String(error) });
519586
519666
  } catch {}
519587
519667
  emergencyExit(1);
519588
519668
  };
519589
519669
  const onUnhandledRejection = (reason) => {
519670
+ recordShellExit({
519671
+ kind: "unhandled-rejection",
519672
+ exitCode: 1,
519673
+ error: reason
519674
+ });
519590
519675
  try {
519591
519676
  log.error("unhandledRejection, restoring terminal and exiting", { reason: String(reason) });
519592
519677
  } catch {}
@@ -519601,6 +519686,10 @@ async function runShell(opts, version) {
519601
519686
  tui.onExit = async (exitCode = 0) => {
519602
519687
  const sessionId = tui.getCurrentSessionId();
519603
519688
  const hasContent = tui.hasSessionContent();
519689
+ recordShellExit({
519690
+ kind: "normal-exit",
519691
+ exitCode
519692
+ });
519604
519693
  setCrashPhase("shutdown");
519605
519694
  trackLifecycle("exit", { duration_ms: Date.now() - startedAt });
519606
519695
  await shutdownTelemetry({ timeoutMs: CLI_SHUTDOWN_TIMEOUT_MS });
@@ -519631,6 +519720,12 @@ async function runShell(opts, version) {
519631
519720
  ...tui.getStartupPhaseMs()
519632
519721
  });
519633
519722
  } catch (error) {
519723
+ recordShellExit({
519724
+ kind: "startup-failure",
519725
+ exitCode: 1,
519726
+ phase: "startup",
519727
+ error
519728
+ });
519634
519729
  removeCrashHandlers();
519635
519730
  setCrashPhase("shutdown");
519636
519731
  trackLifecycle("exit", { duration_ms: Date.now() - startedAt });
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "blun-king-cli",
3
- "version": "9.1.421",
3
+ "version": "9.1.423",
4
4
  "description": "BLUN CLI - your own AI agent with a Telegram channel. Get it done. With BLUN.",
5
5
  "license": "MIT",
6
6
  "bin": {