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 +2 -0
- package/README.md +2 -0
- package/bin/cognitive-goal-autostart-policy.cjs +39 -10
- package/bin/cognitive-goal-time-trigger-controller.cjs +146 -0
- package/bin/launcher-runtime.js +30 -2
- package/bin/runtime-exit-ledger.cjs +143 -0
- package/blun.mjs +112 -17
- package/package.json +1 -1
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
|
-
|
|
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:
|
|
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 };
|
package/bin/launcher-runtime.js
CHANGED
|
@@ -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)
|
|
222
|
-
|
|
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.
|
|
516702
|
-
this.
|
|
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))
|
|
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 });
|