blun-king-cli 9.1.462 → 9.1.464

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.
@@ -0,0 +1,252 @@
1
+ import { createHash } from 'node:crypto';
2
+ import { createRequire } from 'node:module';
3
+
4
+ const require = createRequire(import.meta.url);
5
+ const { createAgentApiToolBroker } = require('./agent-api-tool-broker.cjs');
6
+
7
+ const CLIENT_TOOL_NAME_PATTERN = /^[A-Za-z0-9_-]{1,64}$/u;
8
+ const INTERNAL_TOOL_PREFIX = 'client__';
9
+ const NO_CLIENT_TOOLS_PREFIX = '__blun_agent_api_no_client_tools__';
10
+
11
+ class AgentApiRuntimeError extends Error {
12
+ constructor(code, message) {
13
+ super(message);
14
+ this.name = 'AgentApiRuntimeError';
15
+ this.code = code;
16
+ }
17
+ }
18
+
19
+ function runtimeError(code, message) {
20
+ return new AgentApiRuntimeError(code, message);
21
+ }
22
+
23
+ function requireId(value, label) {
24
+ if (typeof value !== 'string' || value.trim().length === 0) {
25
+ throw runtimeError('AGENT_API_INVALID_SCOPE', `${label} must be a non-empty string`);
26
+ }
27
+ return value.trim();
28
+ }
29
+
30
+ function cloneStructured(value, label) {
31
+ try {
32
+ return structuredClone(value);
33
+ } catch {
34
+ throw runtimeError(
35
+ 'AGENT_API_INVALID_TOOL_DEFINITION',
36
+ `${label} must be structured-cloneable`,
37
+ );
38
+ }
39
+ }
40
+
41
+ function normalizeParameters(value, name) {
42
+ if (value === undefined) {
43
+ return { type: 'object', properties: {} };
44
+ }
45
+ if (value === null || typeof value !== 'object' || Array.isArray(value)) {
46
+ throw runtimeError(
47
+ 'AGENT_API_INVALID_TOOL_DEFINITION',
48
+ `tool "${name}" parameters must be an object`,
49
+ );
50
+ }
51
+ return cloneStructured(value, `tool "${name}" parameters`);
52
+ }
53
+
54
+ function normalizeClientToolDefinitions(tools) {
55
+ if (!Array.isArray(tools)) {
56
+ throw runtimeError('AGENT_API_INVALID_TOOLS', 'tools must be an array');
57
+ }
58
+
59
+ const names = new Set();
60
+ return tools.map((tool, index) => {
61
+ if (tool === null || typeof tool !== 'object' || Array.isArray(tool)
62
+ || tool.type !== 'function'
63
+ || tool.function === null
64
+ || typeof tool.function !== 'object'
65
+ || Array.isArray(tool.function)) {
66
+ throw runtimeError(
67
+ 'AGENT_API_INVALID_TOOL_DEFINITION',
68
+ `tools[${index}] must be an OpenAI function tool`,
69
+ );
70
+ }
71
+
72
+ const name = tool.function.name;
73
+ if (typeof name !== 'string' || !CLIENT_TOOL_NAME_PATTERN.test(name)) {
74
+ throw runtimeError(
75
+ 'AGENT_API_INVALID_TOOL_DEFINITION',
76
+ `tools[${index}].function.name must match ${CLIENT_TOOL_NAME_PATTERN}`,
77
+ );
78
+ }
79
+ if (names.has(name)) {
80
+ throw runtimeError(
81
+ 'AGENT_API_DUPLICATE_TOOL_NAME',
82
+ `tool name "${name}" is declared more than once`,
83
+ );
84
+ }
85
+ names.add(name);
86
+
87
+ const description = tool.function.description;
88
+ if (description !== undefined && typeof description !== 'string') {
89
+ throw runtimeError(
90
+ 'AGENT_API_INVALID_TOOL_DEFINITION',
91
+ `tool "${name}" description must be a string`,
92
+ );
93
+ }
94
+
95
+ return Object.freeze({
96
+ name,
97
+ description: description ?? '',
98
+ parameters: normalizeParameters(tool.function.parameters, name),
99
+ });
100
+ });
101
+ }
102
+
103
+ function shortHash(value, length = 10) {
104
+ return createHash('sha256').update(value).digest('hex').slice(0, length);
105
+ }
106
+
107
+ function internalToolName(publicName, index) {
108
+ const ordinal = index.toString(36).padStart(2, '0');
109
+ const prefix = `${INTERNAL_TOOL_PREFIX}${ordinal}__`;
110
+ const suffix = `__${shortHash(publicName, 8)}`;
111
+ const available = 64 - prefix.length - suffix.length;
112
+ return `${prefix}${publicName.slice(0, available)}${suffix}`;
113
+ }
114
+
115
+ function noClientToolsSentinel(sessionId, requestId) {
116
+ return `${NO_CLIENT_TOOLS_PREFIX}${shortHash(`${sessionId}\0${requestId}`, 16)}`;
117
+ }
118
+
119
+ function validateSession(session) {
120
+ if (session === null || typeof session !== 'object') {
121
+ throw runtimeError('AGENT_API_INVALID_SESSION', 'session must be an SDK session');
122
+ }
123
+ const requiredMethods = [
124
+ 'registerToolAndWait',
125
+ 'unregisterToolAndWait',
126
+ 'setAllowedTools',
127
+ ];
128
+ for (const method of requiredMethods) {
129
+ if (typeof session[method] !== 'function') {
130
+ throw runtimeError(
131
+ 'AGENT_API_INVALID_SESSION',
132
+ `session.${method} must be a function`,
133
+ );
134
+ }
135
+ }
136
+ return requireId(session.id, 'session.id');
137
+ }
138
+
139
+ async function unregisterTools(session, names) {
140
+ let firstError;
141
+ for (const name of [...names].reverse()) {
142
+ try {
143
+ await session.unregisterToolAndWait(name);
144
+ } catch (error) {
145
+ firstError ??= error;
146
+ }
147
+ }
148
+ if (firstError !== undefined) throw firstError;
149
+ }
150
+
151
+ function createAgentApiRuntime(options = {}) {
152
+ const broker = options.broker ?? createAgentApiToolBroker(options.brokerOptions);
153
+
154
+ async function bindClientTools(input) {
155
+ if (input === null || typeof input !== 'object') {
156
+ throw runtimeError('AGENT_API_INVALID_REQUEST', 'binding input must be an object');
157
+ }
158
+
159
+ const sessionId = validateSession(input.session);
160
+ const requestId = requireId(input.requestId, 'requestId');
161
+ const tools = normalizeClientToolDefinitions(input.tools ?? []);
162
+ const mappings = tools.map((tool, index) => Object.freeze({
163
+ ...tool,
164
+ internalName: internalToolName(tool.name, index),
165
+ }));
166
+ const registeredNames = [];
167
+
168
+ broker.beginRequest({
169
+ sessionId,
170
+ requestId,
171
+ toolNames: mappings.map((tool) => tool.name),
172
+ });
173
+
174
+ try {
175
+ for (const tool of mappings) {
176
+ await input.session.registerToolAndWait(
177
+ tool.internalName,
178
+ tool.description,
179
+ tool.parameters,
180
+ (request) => broker.handleToolCall({
181
+ sessionId,
182
+ turnId: request.turnId,
183
+ toolCallId: request.toolCallId,
184
+ toolName: tool.name,
185
+ args: request.args,
186
+ }),
187
+ );
188
+ registeredNames.push(tool.internalName);
189
+ }
190
+
191
+ await input.session.setAllowedTools(
192
+ registeredNames.length > 0
193
+ ? registeredNames
194
+ : [noClientToolsSentinel(sessionId, requestId)],
195
+ );
196
+ } catch (error) {
197
+ broker.abortRequest({
198
+ sessionId,
199
+ requestId,
200
+ reason: 'Agent API SDK binding failed',
201
+ });
202
+ try {
203
+ await unregisterTools(input.session, registeredNames);
204
+ } catch {
205
+ // Preserve the binding error; cleanup was best-effort and all handlers are fail-closed.
206
+ }
207
+ throw error;
208
+ }
209
+
210
+ let closed = false;
211
+ function ensureOpen() {
212
+ if (closed) {
213
+ throw runtimeError('AGENT_API_BINDING_CLOSED', 'Agent API SDK binding is closed');
214
+ }
215
+ }
216
+
217
+ return Object.freeze({
218
+ sessionId,
219
+ requestId,
220
+ nextToolCall() {
221
+ ensureOpen();
222
+ return broker.nextToolCall({ sessionId, requestId });
223
+ },
224
+ submitToolResult(result) {
225
+ ensureOpen();
226
+ return broker.submitToolResult({ sessionId, requestId, ...result });
227
+ },
228
+ async finish() {
229
+ ensureOpen();
230
+ broker.finishRequest({ sessionId, requestId });
231
+ closed = true;
232
+ await unregisterTools(input.session, registeredNames);
233
+ },
234
+ async abort(reason = 'Agent API request aborted') {
235
+ ensureOpen();
236
+ broker.abortRequest({ sessionId, requestId, reason });
237
+ closed = true;
238
+ await unregisterTools(input.session, registeredNames);
239
+ },
240
+ });
241
+ }
242
+
243
+ return Object.freeze({ bindClientTools });
244
+ }
245
+
246
+ export {
247
+ AgentApiRuntimeError,
248
+ CLIENT_TOOL_NAME_PATTERN,
249
+ NO_CLIENT_TOOLS_PREFIX,
250
+ createAgentApiRuntime,
251
+ normalizeClientToolDefinitions,
252
+ };
@@ -0,0 +1,248 @@
1
+ 'use strict';
2
+
3
+ const DEFAULT_MAX_PENDING_TOOL_CALLS = 32;
4
+
5
+ class AgentApiToolBrokerError extends Error {
6
+ constructor(code, message) {
7
+ super(message);
8
+ this.name = 'AgentApiToolBrokerError';
9
+ this.code = code;
10
+ }
11
+ }
12
+
13
+ function brokerError(code, message) {
14
+ return new AgentApiToolBrokerError(code, message);
15
+ }
16
+
17
+ function requireId(value, label) {
18
+ if (typeof value !== 'string' || value.trim().length === 0) {
19
+ throw brokerError('AGENT_API_INVALID_SCOPE', `${label} must be a non-empty string`);
20
+ }
21
+ return value.trim();
22
+ }
23
+
24
+ function normalizeToolNames(value) {
25
+ if (!Array.isArray(value)) {
26
+ throw brokerError('AGENT_API_INVALID_TOOLS', 'toolNames must be an array');
27
+ }
28
+ const names = value.map((name) => requireId(name, 'tool name'));
29
+ if (new Set(names).size !== names.length) {
30
+ throw brokerError('AGENT_API_INVALID_TOOLS', 'toolNames must not contain duplicates');
31
+ }
32
+ return new Set(names);
33
+ }
34
+
35
+ function normalizeMaxPendingToolCalls(value) {
36
+ const normalized = value ?? DEFAULT_MAX_PENDING_TOOL_CALLS;
37
+ if (!Number.isSafeInteger(normalized) || normalized < 1) {
38
+ throw brokerError(
39
+ 'AGENT_API_INVALID_LIMIT',
40
+ 'maxPendingToolCalls must be a positive safe integer',
41
+ );
42
+ }
43
+ return normalized;
44
+ }
45
+
46
+ function normalizeToolResult(input) {
47
+ if (typeof input.output !== 'string') {
48
+ throw brokerError('AGENT_API_INVALID_TOOL_RESULT', 'tool result output must be a string');
49
+ }
50
+ if (input.isError !== undefined && typeof input.isError !== 'boolean') {
51
+ throw brokerError('AGENT_API_INVALID_TOOL_RESULT', 'tool result isError must be a boolean');
52
+ }
53
+ return {
54
+ output: input.output,
55
+ isError: input.isError === true,
56
+ };
57
+ }
58
+
59
+ function cloneArgs(value) {
60
+ try {
61
+ return structuredClone(value);
62
+ } catch {
63
+ throw brokerError('AGENT_API_INVALID_TOOL_ARGS', 'tool arguments must be structured-cloneable');
64
+ }
65
+ }
66
+
67
+ function createDeferred() {
68
+ let resolve;
69
+ let reject;
70
+ const promise = new Promise((resolvePromise, rejectPromise) => {
71
+ resolve = resolvePromise;
72
+ reject = rejectPromise;
73
+ });
74
+ return { promise, reject, resolve };
75
+ }
76
+
77
+ function createAgentApiToolBroker(options = {}) {
78
+ const maxPendingToolCalls = normalizeMaxPendingToolCalls(options.maxPendingToolCalls);
79
+ const activeRequests = new Map();
80
+
81
+ function requestForScope(input) {
82
+ const sessionId = requireId(input.sessionId, 'sessionId');
83
+ const requestId = requireId(input.requestId, 'requestId');
84
+ const request = activeRequests.get(sessionId);
85
+ if (request === undefined) {
86
+ throw brokerError('AGENT_API_REQUEST_NOT_ACTIVE', 'no active Agent API request for session');
87
+ }
88
+ if (request.requestId !== requestId) {
89
+ throw brokerError(
90
+ 'AGENT_API_TOOL_RESULT_SCOPE_MISMATCH',
91
+ 'Agent API request does not match the active session request',
92
+ );
93
+ }
94
+ return request;
95
+ }
96
+
97
+ function pendingToolCallCount() {
98
+ let count = 0;
99
+ for (const request of activeRequests.values()) count += request.pending.size;
100
+ return count;
101
+ }
102
+
103
+ function deliverEvent(request, event) {
104
+ const waiter = request.eventWaiters.shift();
105
+ if (waiter !== undefined) {
106
+ waiter.resolve(event);
107
+ return;
108
+ }
109
+ request.events.push(event);
110
+ }
111
+
112
+ function beginRequest(input) {
113
+ const sessionId = requireId(input.sessionId, 'sessionId');
114
+ const requestId = requireId(input.requestId, 'requestId');
115
+ if (activeRequests.has(sessionId)) {
116
+ throw brokerError(
117
+ 'AGENT_API_SESSION_BUSY',
118
+ 'session already has an active Agent API request',
119
+ );
120
+ }
121
+ const request = {
122
+ sessionId,
123
+ requestId,
124
+ toolNames: normalizeToolNames(input.toolNames),
125
+ seenToolCallIds: new Set(),
126
+ pending: new Map(),
127
+ events: [],
128
+ eventWaiters: [],
129
+ };
130
+ activeRequests.set(sessionId, request);
131
+ return { sessionId, requestId };
132
+ }
133
+
134
+ function handleToolCall(input) {
135
+ const sessionId = requireId(input.sessionId, 'sessionId');
136
+ const toolCallId = requireId(input.toolCallId, 'toolCallId');
137
+ const toolName = requireId(input.toolName, 'toolName');
138
+ const request = activeRequests.get(sessionId);
139
+ if (request === undefined) {
140
+ return Promise.reject(brokerError(
141
+ 'AGENT_API_REQUEST_NOT_ACTIVE',
142
+ 'no active Agent API request for SDK tool call',
143
+ ));
144
+ }
145
+ if (!request.toolNames.has(toolName)) {
146
+ return Promise.reject(brokerError(
147
+ 'AGENT_API_TOOL_NOT_DECLARED',
148
+ `tool "${toolName}" was not declared by the API client`,
149
+ ));
150
+ }
151
+ if (request.seenToolCallIds.has(toolCallId)) {
152
+ return Promise.reject(brokerError(
153
+ 'AGENT_API_TOOL_CALL_DUPLICATE',
154
+ `tool call "${toolCallId}" has already been accepted`,
155
+ ));
156
+ }
157
+ if (pendingToolCallCount() >= maxPendingToolCalls) {
158
+ return Promise.reject(brokerError(
159
+ 'AGENT_API_TOOL_CALL_LIMIT',
160
+ 'too many client tool calls are waiting for results',
161
+ ));
162
+ }
163
+
164
+ const deferred = createDeferred();
165
+ request.seenToolCallIds.add(toolCallId);
166
+ request.pending.set(toolCallId, deferred);
167
+ deliverEvent(request, Object.freeze({
168
+ type: 'client_tool_call',
169
+ sessionId,
170
+ requestId: request.requestId,
171
+ turnId: input.turnId,
172
+ toolCallId,
173
+ toolName,
174
+ args: cloneArgs(input.args),
175
+ }));
176
+ return deferred.promise;
177
+ }
178
+
179
+ function nextToolCall(input) {
180
+ const request = requestForScope(input);
181
+ const event = request.events.shift();
182
+ if (event !== undefined) return Promise.resolve(event);
183
+ const deferred = createDeferred();
184
+ request.eventWaiters.push(deferred);
185
+ return deferred.promise;
186
+ }
187
+
188
+ function submitToolResult(input) {
189
+ const request = requestForScope(input);
190
+ const toolCallId = requireId(input.toolCallId, 'toolCallId');
191
+ const pending = request.pending.get(toolCallId);
192
+ if (pending === undefined) {
193
+ throw brokerError(
194
+ 'AGENT_API_TOOL_CALL_NOT_PENDING',
195
+ `tool call "${toolCallId}" is not waiting for a result`,
196
+ );
197
+ }
198
+ const result = normalizeToolResult(input);
199
+ request.pending.delete(toolCallId);
200
+ pending.resolve(result);
201
+ return true;
202
+ }
203
+
204
+ function abortRequest(input) {
205
+ const request = requestForScope(input);
206
+ const reason = typeof input.reason === 'string' && input.reason.trim().length > 0
207
+ ? input.reason.trim()
208
+ : 'request aborted';
209
+ const error = brokerError('AGENT_API_REQUEST_ABORTED', reason);
210
+ activeRequests.delete(request.sessionId);
211
+ request.events.length = 0;
212
+ for (const waiter of request.eventWaiters.splice(0)) waiter.reject(error);
213
+ for (const pending of request.pending.values()) pending.reject(error);
214
+ request.pending.clear();
215
+ return true;
216
+ }
217
+
218
+ function finishRequest(input) {
219
+ const request = requestForScope(input);
220
+ if (request.pending.size > 0) {
221
+ throw brokerError(
222
+ 'AGENT_API_REQUEST_HAS_PENDING_TOOLS',
223
+ 'cannot finish a request while client tool results are pending',
224
+ );
225
+ }
226
+ const error = brokerError('AGENT_API_REQUEST_FINISHED', 'Agent API request finished');
227
+ activeRequests.delete(request.sessionId);
228
+ request.events.length = 0;
229
+ for (const waiter of request.eventWaiters.splice(0)) waiter.reject(error);
230
+ return true;
231
+ }
232
+
233
+ return Object.freeze({
234
+ abortRequest,
235
+ beginRequest,
236
+ finishRequest,
237
+ handleToolCall,
238
+ nextToolCall,
239
+ pendingToolCallCount,
240
+ submitToolResult,
241
+ });
242
+ }
243
+
244
+ module.exports = {
245
+ AgentApiToolBrokerError,
246
+ DEFAULT_MAX_PENDING_TOOL_CALLS,
247
+ createAgentApiToolBroker,
248
+ };
@@ -28,6 +28,8 @@ const LEGACY_PROFILE_ENTRIES = Object.freeze([
28
28
  const MIGRATION_MARKER = '.legacy-profile-migrated-v1';
29
29
  const SESSION_PATH_MIGRATION_MARKER = '.legacy-profile-session-paths-v2';
30
30
  const SESSION_PATH_BACKUP_DIR = '.legacy-profile-session-paths-v2-backup';
31
+ const PERSONA_LANGUAGE_MIGRATION_MARKER = '.legacy-persona-language-v1';
32
+ const PERSONA_LANGUAGE_BACKUP = '.legacy-persona-language-v1-backup.json';
31
33
 
32
34
  function normalizeProfileName(value) {
33
35
  const normalized = String(value || '').trim().toLowerCase();
@@ -134,6 +136,56 @@ function backupFile(source, target, fsImpl) {
134
136
  fsImpl.copyFileSync(source, target);
135
137
  }
136
138
 
139
+ function readJsonObject(target, fsImpl) {
140
+ if (!fsImpl.existsSync(target)) return null;
141
+ try {
142
+ const text = fsImpl.readFileSync(target, 'utf8');
143
+ const parsed = JSON.parse(text.charCodeAt(0) === 0xFEFF ? text.slice(1) : text);
144
+ return parsed && typeof parsed === 'object' && !Array.isArray(parsed) ? parsed : null;
145
+ } catch {
146
+ return null;
147
+ }
148
+ }
149
+
150
+ function writeMarkerOnce(target, text, fsImpl) {
151
+ try {
152
+ fsImpl.writeFileSync(target, text, { encoding: 'utf8', flag: 'wx' });
153
+ } catch (error) {
154
+ if (error?.code !== 'EEXIST') throw error;
155
+ }
156
+ }
157
+
158
+ function repairDefaultProfilePersonaLanguage(sharedHome, fsImpl = fs) {
159
+ const paths = resolveProfilePaths(sharedHome, DEFAULT_PROFILE);
160
+ const marker = path.join(paths.home, PERSONA_LANGUAGE_MIGRATION_MARKER);
161
+ const profilePersonaPath = path.join(paths.home, 'persona.json');
162
+ const legacyPersonaPath = path.join(sharedHome, 'persona.json');
163
+ if (fsImpl.existsSync(marker) || !fsImpl.existsSync(profilePersonaPath)) return paths;
164
+
165
+ const profilePersona = readJsonObject(profilePersonaPath, fsImpl);
166
+ const legacyPersona = readJsonObject(legacyPersonaPath, fsImpl);
167
+ if (profilePersona === null || legacyPersona === null) return paths;
168
+
169
+ if (!Object.hasOwn(profilePersona, 'language')) {
170
+ const legacyLanguage = typeof legacyPersona.language === 'string'
171
+ ? legacyPersona.language.trim()
172
+ : '';
173
+ if (legacyLanguage) {
174
+ const original = fsImpl.readFileSync(profilePersonaPath, 'utf8');
175
+ backupFile(profilePersonaPath, path.join(paths.home, PERSONA_LANGUAGE_BACKUP), fsImpl);
176
+ replaceTextFileIfUnchanged(
177
+ profilePersonaPath,
178
+ original,
179
+ `${JSON.stringify({ ...profilePersona, language: legacyLanguage }, null, 2)}\n`,
180
+ fsImpl,
181
+ );
182
+ }
183
+ }
184
+
185
+ writeMarkerOnce(marker, 'legacy persona language repaired once\n', fsImpl);
186
+ return paths;
187
+ }
188
+
137
189
  function rewriteSessionState(statePath, legacySessionDir, profileSessionDir, backupRoot, relativeSessionDir, fsImpl) {
138
190
  if (!fsImpl.existsSync(statePath)) return false;
139
191
  const original = fsImpl.readFileSync(statePath, 'utf8');
@@ -251,6 +303,7 @@ function seedDefaultProfileFromLegacy(sharedHome, fsImpl = fs) {
251
303
  if (error?.code !== 'EEXIST') throw error;
252
304
  }
253
305
  }
306
+ repairDefaultProfilePersonaLanguage(sharedHome, fsImpl);
254
307
  return repairDefaultProfileSessionPaths(sharedHome, fsImpl);
255
308
  }
256
309
 
@@ -258,6 +311,7 @@ module.exports = {
258
311
  DEFAULT_PROFILE,
259
312
  normalizeProfileName,
260
313
  parseProfileLaunchArgs,
314
+ repairDefaultProfilePersonaLanguage,
261
315
  repairDefaultProfileSessionPaths,
262
316
  resolveProfilePaths,
263
317
  seedDefaultProfileFromLegacy,
@@ -0,0 +1,22 @@
1
+ 'use strict';
2
+
3
+ function replayWindowStartIndex(records, activeTurnCount, startsTurn) {
4
+ if (
5
+ !Array.isArray(records)
6
+ || !Number.isSafeInteger(activeTurnCount)
7
+ || activeTurnCount <= 0
8
+ || typeof startsTurn !== 'function'
9
+ ) {
10
+ return 0;
11
+ }
12
+
13
+ let remainingTurns = activeTurnCount;
14
+ for (let index = records.length - 1; index >= 0; index -= 1) {
15
+ if (!startsTurn(records[index])) continue;
16
+ remainingTurns -= 1;
17
+ if (remainingTurns === 0) return index;
18
+ }
19
+ return 0;
20
+ }
21
+
22
+ module.exports = { replayWindowStartIndex };
@@ -41,6 +41,7 @@ class SessionScrollbackArchive {
41
41
  this.loadedChunks = new Map();
42
42
  this.lineCount = 0;
43
43
  this.loadedExisting = false;
44
+ this.activeTurnCount = undefined;
44
45
 
45
46
  if (reset) {
46
47
  this.reset();
@@ -108,6 +109,21 @@ class SessionScrollbackArchive {
108
109
  return output;
109
110
  }
110
111
 
112
+ setActiveTurnCount(count) {
113
+ if (!isNonNegativeInteger(count)) return false;
114
+ if (this.activeTurnCount === count) return true;
115
+ const previous = this.activeTurnCount;
116
+ this.activeTurnCount = count;
117
+ try {
118
+ fs.mkdirSync(this.directory, { recursive: true });
119
+ this.writeIndex(this.chunks, this.lineCount);
120
+ return true;
121
+ } catch {
122
+ this.activeTurnCount = previous;
123
+ return false;
124
+ }
125
+ }
126
+
111
127
  reset() {
112
128
  try {
113
129
  fs.rmSync(this.directory, { recursive: true, force: true });
@@ -116,6 +132,7 @@ class SessionScrollbackArchive {
116
132
  this.loadedChunks.clear();
117
133
  this.lineCount = 0;
118
134
  this.loadedExisting = false;
135
+ this.activeTurnCount = undefined;
119
136
  }
120
137
 
121
138
  clear() {
@@ -165,6 +182,9 @@ class SessionScrollbackArchive {
165
182
 
166
183
  this.chunks = chunks;
167
184
  this.lineCount = parsed.totalLines;
185
+ this.activeTurnCount = isNonNegativeInteger(parsed.activeTurnCount)
186
+ ? parsed.activeTurnCount
187
+ : undefined;
168
188
  this.loadedExisting = true;
169
189
  } catch {}
170
190
  }
@@ -195,6 +215,9 @@ class SessionScrollbackArchive {
195
215
  schemaVersion: ARCHIVE_SCHEMA_VERSION,
196
216
  totalLines,
197
217
  chunks,
218
+ ...(isNonNegativeInteger(this.activeTurnCount)
219
+ ? { activeTurnCount: this.activeTurnCount }
220
+ : {}),
198
221
  }));
199
222
  }
200
223
  }
@@ -0,0 +1,19 @@
1
+ 'use strict';
2
+
3
+ function resolveSubagentRunMode({ allowBackground, requested }) {
4
+ if (allowBackground !== true) {
5
+ return {
6
+ runInBackground: false,
7
+ forcedForParentResponsiveness: false,
8
+ };
9
+ }
10
+
11
+ return {
12
+ runInBackground: true,
13
+ forcedForParentResponsiveness: requested !== true,
14
+ };
15
+ }
16
+
17
+ module.exports = {
18
+ resolveSubagentRunMode,
19
+ };
@@ -0,0 +1,45 @@
1
+ 'use strict';
2
+
3
+ function createAddressedChannelFocusStore(options = {}) {
4
+ const maxItems = Math.max(1, Number(options.maxItems) || 3);
5
+ const onChange = typeof options.onChange === 'function' ? options.onChange : () => {};
6
+ let sequence = 0;
7
+ let items = [];
8
+
9
+ function snapshot() {
10
+ return items.map((item) => ({ ...item }));
11
+ }
12
+
13
+ function publish() {
14
+ onChange(snapshot());
15
+ }
16
+
17
+ return {
18
+ pin(input) {
19
+ const item = {
20
+ id: `channel-focus-${++sequence}`,
21
+ chatId: String(input.chatId),
22
+ origin: String(input.origin ?? ''),
23
+ text: String(input.text ?? '').trim(),
24
+ };
25
+ items = [...items, item].slice(-maxItems);
26
+ publish();
27
+ return item.id;
28
+ },
29
+
30
+ clearChat(chatId) {
31
+ const expected = String(chatId);
32
+ const next = items.filter((item) => item.chatId !== expected);
33
+ if (next.length === items.length) return false;
34
+ items = next;
35
+ publish();
36
+ return true;
37
+ },
38
+
39
+ snapshot,
40
+ };
41
+ }
42
+
43
+ module.exports = {
44
+ createAddressedChannelFocusStore,
45
+ };
package/blun.mjs CHANGED
@@ -13,6 +13,7 @@ import compactionModelPolicy from "./bin/compaction-model-policy.cjs";
13
13
  import agentResumeSnapshot from "./bin/agent-resume-snapshot.cjs";
14
14
  import sessionResumeCheckpoint from "./bin/session-resume-checkpoint.cjs";
15
15
  import sessionScrollbackArchive from "./bin/session-scrollback-archive.cjs";
16
+ import sessionReplayWindowPolicy from "./bin/session-replay-window-policy.cjs";
16
17
  import crypto$1, { createHash, randomBytes, randomInt, randomUUID, timingSafeEqual } from "node:crypto";
17
18
  import * as fs$16 from "node:fs";
18
19
  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";
@@ -50,6 +51,7 @@ const { resolveCompactionModelAlias } = compactionModelPolicy;
50
51
  const { createAgentResumeSnapshot, restoreAgentResumeSnapshot } = agentResumeSnapshot;
51
52
  const { loadResumeCheckpoint, writeResumeCheckpoint } = sessionResumeCheckpoint;
52
53
  const { SessionScrollbackArchive, sessionScrollbackArchiveDirectory } = sessionScrollbackArchive;
54
+ const { replayWindowStartIndex } = sessionReplayWindowPolicy;
53
55
  import { EventEmitter as EventEmitter$1 } from "node:events";
54
56
  import { StringDecoder } from "node:string_decoder";
55
57
  import co from "node:assert";
@@ -28242,6 +28244,16 @@ function readPersona$1(env = process.env) {
28242
28244
  const resolved = readProfilePersona(env);
28243
28245
  return resolved.persona;
28244
28246
  }
28247
+ function defaultAnswerLanguageSystemBlock() {
28248
+ return [
28249
+ "## Default answer language — HARD RULE",
28250
+ "",
28251
+ "English is the initial language for new or unconfigured accounts, profiles, and sessions.",
28252
+ "An explicit language saved in the account, profile, or current session overrides this default.",
28253
+ "Detect the language of the user's latest conversational message and answer in that language, including languages such as Bengali.",
28254
+ "Tool output does not change the conversational language. This rule overrides the generic # Language section when no explicit language is active."
28255
+ ].join("\n");
28256
+ }
28245
28257
  /**
28246
28258
  * Build the system-prompt block that teaches the agent its persona name, so it
28247
28259
  * warmly adopts the name the user gave it. Empty string when no name is set.
@@ -28408,6 +28420,7 @@ function buildTemplateVars(context, promptVars, tools) {
28408
28420
  const now = context.now instanceof Date ? context.now.toISOString() : context.now ?? (/* @__PURE__ */ new Date()).toISOString();
28409
28421
  const roleBase = context.roleAdditional ?? promptVars["ROLE_ADDITIONAL"] ?? promptVars["roleAdditional"] ?? "";
28410
28422
  const roleAdditional = [
28423
+ defaultAnswerLanguageSystemBlock(),
28411
28424
  personaSystemBlock(),
28412
28425
  soulSystemBlock(),
28413
28426
  identitySystemBlock(),
@@ -252785,7 +252798,7 @@ var init_agent_background_disabled = __esmMin((() => {
252785
252798
  //#region ../../packages/agent-core/src/tools/builtin/collaboration/agent-background-enabled.md?raw
252786
252799
  var agent_background_enabled_default;
252787
252800
  var init_agent_background_enabled = __esmMin((() => {
252788
- agent_background_enabled_default = "When `run_in_background=true`, the subagent runs detached from this turn. The completion arrives in a later turn as a synthetic user-role message containing its result — you do not need to poll, sleep, or check on its progress. Continue with other work or respond to the user. Never fabricate or predict what the result will say.\n\nDefault to a foreground subagent (omit `run_in_background`) when your next step needs its result — foreground hands the result straight back. Reach for `run_in_background=true` only when you have other work to do while it runs and do not need its result to proceed. Never launch in the background and then immediately wait on it (with `TaskOutput block=true`, sleeping, or otherwise): that just blocks the turn for no benefit — run it in the foreground instead.\n";
252801
+ agent_background_enabled_default = "Subagents run detached whenever task management is available, so the parent remains responsive while delegated work continues. Completion arrives automatically in a later turn. Do not poll, sleep, or block on TaskOutput. Continue with other work or respond to the user. Use TaskUpdate to steer the running subagent and TaskStop only when it must be cancelled. Never fabricate or predict its result.";
252789
252802
  }));
252790
252803
  //#endregion
252791
252804
  //#region ../../packages/agent-core/src/tools/builtin/collaboration/agent.md?raw
@@ -252805,7 +252818,7 @@ function formatBackgroundAgentResult(taskId, handle, description, allowBackgroun
252805
252818
  "",
252806
252819
  `description: ${description}`,
252807
252820
  "",
252808
- allowBackground ? `next_step: The completion arrives automatically in a later turn — do NOT wait, poll, or call TaskOutput on it; continue with other work or hand back to the user. (If you have nothing to do until it finishes, run such tasks in the foreground next time.)` : "next_step: The completion arrives automatically in a later turn.",
252821
+ allowBackground ? "next_step: The subagent continues independently. Do not wait or poll; remain responsive to the user. Use TaskUpdate to steer it and TaskStop only if it must be cancelled. Its completion arrives automatically in a later turn." : "next_step: The completion arrives automatically in a later turn.",
252809
252822
  `resume_hint: To continue or recover this same subagent later, call Agent(resume="${handle.agentId}", prompt="..."). The parameter is agent_id ("${handle.agentId}"), NOT task_id ("${taskId}") or source_id from a later <notification>. Recovery cases: a later <notification type="task.lost" | "task.failed" | "task.killed"> for this subagent — its conversation history is preserved across session restarts and resume will pick it up.`
252810
252823
  ].join("\n");
252811
252824
  }
@@ -252844,7 +252857,7 @@ function buildSubagentDescriptions(subagents) {
252844
252857
  return `${header}\n Tools: ${subagent.tools.join(", ")}`;
252845
252858
  }).join("\n");
252846
252859
  }
252847
- var AgentToolInputSchema, BACKGROUND_AGENT_UNAVAILABLE, AgentTool, USER_INTERRUPTED_SUBAGENT_MESSAGE;
252860
+ var AgentToolInputSchema, BACKGROUND_AGENT_UNAVAILABLE, AgentTool, USER_INTERRUPTED_SUBAGENT_MESSAGE, resolveSubagentRunMode;
252848
252861
  var init_agent$1 = __esmMin((() => {
252849
252862
  init_zod$1();
252850
252863
  init_tool_access();
@@ -252857,6 +252870,7 @@ var init_agent$1 = __esmMin((() => {
252857
252870
  init_agent_background_disabled();
252858
252871
  init_agent_background_enabled();
252859
252872
  init_agent$2();
252873
+ ({ resolveSubagentRunMode } = createRequire(import.meta.url)("./bin/subagent-parent-responsiveness.cjs"));
252860
252874
  AgentToolInputSchema = preprocess((input) => {
252861
252875
  if (typeof input !== "object" || input === null || Array.isArray(input)) return input;
252862
252876
  const normalized = { ...input };
@@ -252870,7 +252884,7 @@ var init_agent$1 = __esmMin((() => {
252870
252884
  description: string().describe("Short task description (3-5 words) for UI display"),
252871
252885
  subagent_type: string().optional().describe("One of the available agent types (see \"Available agent types\" in this tool description). Defaults to \"coder\" when omitted."),
252872
252886
  resume: string().optional().describe("Optional agent ID to resume instead of creating a new instance. When set, do not also pass subagent_type — the resumed agent keeps its own type, and supplying both is rejected."),
252873
- run_in_background: boolean$1().optional().describe("If true, return immediately without waiting for completion. Prefer false unless the task can run independently and there is a clear benefit to not waiting."),
252887
+ run_in_background: boolean$1().optional().describe("Compatibility option. When task management is available, subagents always run detached so the parent remains responsive."),
252874
252888
  timeout_minutes: number$1().int().min(0).max(10080).optional().describe("Wall-clock interval before the same agent is continued automatically. Defaults to 240 minutes. Set 0 to disable the interval for this run.")
252875
252889
  }));
252876
252890
  object({
@@ -252906,14 +252920,18 @@ var init_agent$1 = __esmMin((() => {
252906
252920
  let profileName = args.subagent_type?.length ? args.subagent_type : "coder";
252907
252921
  const resumeAgentId = args.resume?.trim();
252908
252922
  if (resumeAgentId !== void 0 && resumeAgentId.length > 0) profileName = await this.subagentHost.getProfileName?.(resumeAgentId) ?? "subagent";
252923
+ const { runInBackground } = resolveSubagentRunMode({
252924
+ allowBackground: this.allowBackground,
252925
+ requested: args.run_in_background
252926
+ });
252909
252927
  return {
252910
- description: `${args.run_in_background === true ? "Launching background" : "Launching"} ${profileName} agent: ${args.description}`,
252928
+ description: `${runInBackground ? "Launching background" : "Launching"} ${profileName} agent: ${args.description}`,
252911
252929
  accesses: ToolAccesses.none(),
252912
252930
  display: {
252913
252931
  kind: "agent_call",
252914
252932
  agent_name: profileName,
252915
252933
  prompt: args.prompt,
252916
- background: args.run_in_background
252934
+ background: runInBackground
252917
252935
  },
252918
252936
  approvalRule: this.name,
252919
252937
  matchesRule: (ruleArgs) => matchesGlobRuleSubject(ruleArgs, profileName),
@@ -252923,14 +252941,17 @@ var init_agent$1 = __esmMin((() => {
252923
252941
  async execution(args, { toolCallId, signal }) {
252924
252942
  try {
252925
252943
  signal.throwIfAborted();
252926
- const runInBackground = args.run_in_background === true;
252944
+ const { runInBackground } = resolveSubagentRunMode({
252945
+ allowBackground: this.allowBackground,
252946
+ requested: args.run_in_background
252947
+ });
252927
252948
  const requestedProfileName = args.subagent_type?.length ? args.subagent_type : void 0;
252928
252949
  const resumeAgentId = args.resume?.trim();
252929
252950
  if (resumeAgentId !== void 0 && resumeAgentId.length > 0 && requestedProfileName !== void 0) return {
252930
252951
  output: "Cannot set subagent_type when resuming an existing agent. Resume by agent id only.",
252931
252952
  isError: true
252932
252953
  };
252933
- if (runInBackground && !this.allowBackground) return {
252954
+ if (args.run_in_background === true && !this.allowBackground) return {
252934
252955
  output: BACKGROUND_AGENT_UNAVAILABLE,
252935
252956
  isError: true
252936
252957
  };
@@ -265750,10 +265771,10 @@ var init_agent = __esmMin((() => {
265750
265771
  return this.resolveEffectiveSystemPrompt(this.config.systemPrompt, this.runtimeSystemPromptAppend);
265751
265772
  }
265752
265773
  get fastConversationSystemPrompt() {
265753
- return [personaSystemBlock(), soulSystemBlock(), identitySystemBlock(), naturalPresenceSystemBlock(), conductSystemBlock(), `## Fast conversation mode
265774
+ return [defaultAnswerLanguageSystemBlock(), personaSystemBlock(), soulSystemBlock(), identitySystemBlock(), naturalPresenceSystemBlock(), conductSystemBlock(), `## Fast conversation mode
265754
265775
 
265755
265776
  - Answer the user's current conversational message directly and concisely.
265756
- - Use the language of the user's latest message unless the persona specifies a language override.
265777
+ - Use the language of the user's latest conversational message unless the persona or current session specifies a language override.
265757
265778
  - Do not claim to have inspected files, run commands, used tools, or checked current external state.
265758
265779
  - If the message actually requires work or live information, say that plainly instead of inventing a result.`].filter((block) => block.length > 0).join("\n\n");
265759
265780
  }
@@ -342504,7 +342525,7 @@ const DEFAULT_INLINE_SUGGEST_LANGUAGES = {
342504
342525
  };
342505
342526
  const DEFAULT_TUI_CONFIG = TuiConfigSchema.parse({
342506
342527
  theme: "auto",
342507
- uiLocale: "auto",
342528
+ uiLocale: "en",
342508
342529
  disablePasteBurst: false,
342509
342530
  editorCommand: null,
342510
342531
  notifications: DEFAULT_NOTIFICATIONS_CONFIG,
@@ -419349,6 +419370,7 @@ var { projectUnaddressedTelegramContext } = createRequire(import.meta.url)("./bi
419349
419370
  var { enqueueTelegramUrgent, rewriteTelegramUrgentEnvelope, telegramUrgentMessage } = createRequire(import.meta.url)("./bin/telegram-urgent-policy.cjs");
419350
419371
  var { createDirectFocusController, createDirectReplyTurnStop, enqueueTelegramDirect, readDirectFocusCheckpoint, rewriteTelegramDirectEnvelope, telegramDirectMessage, writeDirectFocusCheckpoint } = createRequire(import.meta.url)("./bin/telegram-direct-focus-policy.cjs");
419351
419372
  var { enqueueTelegramAddressed } = createRequire(import.meta.url)("./bin/telegram-addressed-priority.cjs");
419373
+ var { createAddressedChannelFocusStore } = createRequire(import.meta.url)("./bin/telegram-addressed-focus.cjs");
419352
419374
  var { removeQueuedReloadCommands, removeQueuedSlashCommands } = createRequire(import.meta.url)("./bin/reload-queue-policy.cjs");
419353
419375
  const MAX_BUFFERED_CONTEXT_MESSAGES = 20;
419354
419376
  const MAX_BUFFERED_CONTEXT_CHARS = 24e3;
@@ -501854,6 +501876,23 @@ var QueuePaneComponent = class extends Container {
501854
501876
  return lines;
501855
501877
  }
501856
501878
  };
501879
+ var AddressedChannelFocusPaneComponent = class extends Container {
501880
+ items;
501881
+ constructor(items) {
501882
+ super();
501883
+ this.items = items;
501884
+ }
501885
+ render(width) {
501886
+ const bar = chalk.hex(ACCENT)(LEFT_BAR$1);
501887
+ return this.items.slice(-3).map((item) => {
501888
+ const origin = item.origin.trim().length > 0 ? `${item.origin.trim()} ` : "Telegram ";
501889
+ const body = item.text.replaceAll(/\s+/g, " ").trim();
501890
+ const prefix = `${bar} ${currentTheme.fg("primary", "!")} ${currentTheme.fg("textStrong", origin)}`;
501891
+ const available = Math.max(1, width - visibleWidth(prefix));
501892
+ return truncateToWidth(`${prefix}${truncateToWidth(body, available, ELLIPSIS$3)}`, width, ELLIPSIS$3);
501893
+ });
501894
+ }
501895
+ };
501857
501896
  //#endregion
501858
501897
  //#region src/tui/utils/refresh-providers.ts
501859
501898
  init_src$3();
@@ -506116,9 +506155,15 @@ var ScrollbackBuffer = class {
506116
506155
  get totalLines() {
506117
506156
  return this.archive.totalLines + this.snapshotLines.length;
506118
506157
  }
506158
+ get activeTurnCount() {
506159
+ return this.archive instanceof SessionScrollbackArchive ? this.archive.activeTurnCount : void 0;
506160
+ }
506119
506161
  archiveLines(lines) {
506120
506162
  return this.archive.append(lines);
506121
506163
  }
506164
+ setActiveTurnCount(count) {
506165
+ return this.archive instanceof SessionScrollbackArchive ? this.archive.setActiveTurnCount(count) : false;
506166
+ }
506122
506167
  usePersistentArchive(directory, options) {
506123
506168
  this.releaseArchive();
506124
506169
  const archive = new SessionScrollbackArchive({
@@ -506515,12 +506560,17 @@ var ScrollbackController = class {
506515
506560
  reset: !resumeCheckpointRestored,
506516
506561
  reuse: resumeCheckpointRestored
506517
506562
  });
506518
- this.suppressReplayArchiveWrites = resumeCheckpointRestored && loadedExisting;
506519
- return true;
506563
+ const activeTurnCount = resumeCheckpointRestored && loadedExisting && buffer.totalLines > 0 ? buffer.activeTurnCount : void 0;
506564
+ this.suppressReplayArchiveWrites = resumeCheckpointRestored && loadedExisting && activeTurnCount === void 0;
506565
+ return activeTurnCount;
506520
506566
  }
506521
506567
  finishSessionReplay() {
506568
+ this.persistActiveTurnCount(groupTurns(this.state.transcriptEntries).length);
506522
506569
  this.suppressReplayArchiveWrites = false;
506523
506570
  }
506571
+ persistActiveTurnCount(count) {
506572
+ this.buffer?.setActiveTurnCount(count);
506573
+ }
506524
506574
  archiveComponents(components) {
506525
506575
  const buffer = this.buffer;
506526
506576
  if (buffer === void 0 || components.length === 0) return components.length === 0;
@@ -509544,6 +509594,21 @@ function createReplayRenderContext() {
509544
509594
  suppressNextPlanModeOffNotice: false
509545
509595
  };
509546
509596
  }
509597
+ function replayRecordStartsVisibleTurn(record) {
509598
+ if (record?.type !== "message" || record.message?.role !== "user") return false;
509599
+ const message = record.message;
509600
+ if (backgroundOrigin(message) !== void 0) return false;
509601
+ if (message.origin?.kind === "hook_result" || message.origin?.kind === "injection") return false;
509602
+ if (message.origin?.kind === "shell_command") return message.origin.phase === "input";
509603
+ if (message.origin?.kind === "cron_job" || message.origin?.kind === "cron_missed") return false;
509604
+ if (isGoalForkClearedSystemReminder(message)) return false;
509605
+ if (goalOutcomeReminderFromSystemMessage(message) !== null) return false;
509606
+ const skill = skillActivationFromOrigin(message.origin);
509607
+ if (skill !== void 0) return message.origin?.kind === "skill_activation" && message.origin.trigger === "user-slash";
509608
+ const pluginCommand = pluginCommandFromOrigin(message.origin);
509609
+ if (pluginCommand !== void 0) return message.origin?.kind === "plugin_command" && message.origin.trigger === "user-slash";
509610
+ return true;
509611
+ }
509547
509612
  function replayEntry(context, kind, content, renderMode, extras = {}) {
509548
509613
  return {
509549
509614
  id: nextTranscriptId(),
@@ -509810,9 +509875,9 @@ var SessionReplayRenderer = class {
509810
509875
  this.host.showError(uiText("replay.error.unavailable"));
509811
509876
  return false;
509812
509877
  }
509813
- this.host.scrollbackController.prepareSessionReplay(main);
509878
+ const activeTurnCount = this.host.scrollbackController.prepareSessionReplay(main);
509814
509879
  this.hydrateSnapshot(main);
509815
- await this.renderRecords(main);
509880
+ await this.renderRecords(main, activeTurnCount);
509816
509881
  this.applyTerminalBackgroundAgentStatuses(main);
509817
509882
  this.host.mergeAllTurnSteps();
509818
509883
  this.host.requestTranscriptRender();
@@ -509880,9 +509945,11 @@ var SessionReplayRenderer = class {
509880
509945
  state.footer.setBackgroundCounts(countActiveBackgroundTasks(sessionEventHandler.backgroundTasks));
509881
509946
  state.ui.requestRender();
509882
509947
  }
509883
- async renderRecords(agent) {
509948
+ async renderRecords(agent, activeTurnCount) {
509884
509949
  const context = createReplayRenderContext();
509885
- for (const [recordIndex, record] of agent.replay.entries()) {
509950
+ const startIndex = replayWindowStartIndex(agent.replay, activeTurnCount, replayRecordStartsVisibleTurn);
509951
+ for (let recordIndex = startIndex; recordIndex < agent.replay.length; recordIndex++) {
509952
+ const record = agent.replay[recordIndex];
509886
509953
  this.renderRecord(context, record);
509887
509954
  if (shouldYieldSessionReplay(recordIndex + 1, agent.replay.length)) await yieldSessionReplayControl();
509888
509955
  }
@@ -515175,6 +515242,7 @@ function createTUIState(options) {
515175
515242
  ui.setSurfaceEnabled(initialAppState.appearance?.surface !== void 0);
515176
515243
  const transcriptContainer = new GutterContainer(1, 1);
515177
515244
  const activityContainer = new GutterContainer(1, 1);
515245
+ const channelFocusContainer = new GutterContainer(1, 1);
515178
515246
  const todoPanelContainer = new GutterContainer(1, 1);
515179
515247
  const todoPanel = new TodoPanelComponent();
515180
515248
  const queueContainer = new GutterContainer(1, 1);
@@ -515192,6 +515260,7 @@ function createTUIState(options) {
515192
515260
  terminal,
515193
515261
  transcriptContainer,
515194
515262
  activityContainer,
515263
+ channelFocusContainer,
515195
515264
  todoPanelContainer,
515196
515265
  todoPanel,
515197
515266
  queueContainer,
@@ -516705,6 +516774,9 @@ var BlunTUI = class {
516705
516774
  this.startupTranscriptNotice = startupInput.startupTranscriptNotice;
516706
516775
  this.onStartupNoticeShown = startupInput.onStartupNoticeShown;
516707
516776
  this.state = createTUIState(tuiOptions);
516777
+ this.addressedChannelFocusStore = createAddressedChannelFocusStore({
516778
+ onChange: () => this.updateAddressedChannelFocus()
516779
+ });
516708
516780
  this.state.loopIndicator.setRefreshHandler(() => {
516709
516781
  void this.refreshLoopState();
516710
516782
  });
@@ -517375,6 +517447,7 @@ var BlunTUI = class {
517375
517447
  ui.clear();
517376
517448
  ui.addChild(this.state.transcriptContainer);
517377
517449
  ui.addChild(this.state.activityContainer);
517450
+ ui.addChild(this.state.channelFocusContainer);
517378
517451
  ui.addChild(this.state.todoPanelContainer);
517379
517452
  ui.addChild(this.state.queueContainer);
517380
517453
  ui.addChild(this.state.quotaWarningContainer);
@@ -517383,7 +517456,7 @@ var BlunTUI = class {
517383
517456
  ui.addChild(this.state.editorContainer);
517384
517457
  if (ui instanceof BottomPinnedTUI) {
517385
517458
  ui.pinFromIndex = ui.children.indexOf(this.state.quotaWarningContainer);
517386
- ui.chromeFromIndex = ui.children.indexOf(this.state.todoPanelContainer);
517459
+ ui.chromeFromIndex = ui.children.indexOf(this.state.channelFocusContainer);
517387
517460
  ui.overlayRowsProvider = () => this.state.editor.getAutocompleteRows();
517388
517461
  for (const delay of [
517389
517462
  0,
@@ -517595,7 +517668,8 @@ var BlunTUI = class {
517595
517668
  outboxMarker: outboxMarker(),
517596
517669
  transcriptStart: this.state.transcriptEntries.length,
517597
517670
  contextOnly: false,
517598
- directFocus: queued.channelDirect === true
517671
+ directFocus: queued.channelDirect === true,
517672
+ channelFocusId: queued.channelFocusId
517599
517673
  }]);
517600
517674
  this.pendingChannelReplyGuards.push(...installedGuards);
517601
517675
  const inFlight = {
@@ -517649,12 +517723,13 @@ var BlunTUI = class {
517649
517723
  }
517650
517724
  return true;
517651
517725
  }
517652
- installChannelReplyGuard(chatId, contextOnly) {
517726
+ installChannelReplyGuard(chatId, contextOnly, channelFocusId) {
517653
517727
  const guard = {
517654
517728
  chatId,
517655
517729
  outboxMarker: outboxMarker(),
517656
517730
  transcriptStart: this.state.transcriptEntries.length,
517657
- contextOnly
517731
+ contextOnly,
517732
+ channelFocusId
517658
517733
  };
517659
517734
  this.pendingChannelReplyGuards.push(guard);
517660
517735
  return guard;
@@ -517663,6 +517738,18 @@ var BlunTUI = class {
517663
517738
  const index = this.pendingChannelReplyGuards.indexOf(guard);
517664
517739
  if (index >= 0) this.pendingChannelReplyGuards.splice(index, 1);
517665
517740
  }
517741
+ pinAddressedChannelFocus(chatId, origin, text) {
517742
+ return this.addressedChannelFocusStore.pin({ chatId, origin, text });
517743
+ }
517744
+ clearAddressedChannelFocus(chatId) {
517745
+ return this.addressedChannelFocusStore.clearChat(chatId);
517746
+ }
517747
+ updateAddressedChannelFocus() {
517748
+ this.state.channelFocusContainer.clear();
517749
+ const items = this.addressedChannelFocusStore.snapshot();
517750
+ if (items.length > 0) this.state.channelFocusContainer.addChild(new AddressedChannelFocusPaneComponent(items));
517751
+ this.state.ui.requestRender();
517752
+ }
517666
517753
  commitQueuedSteerAtStepStart(turnId) {
517667
517754
  const inFlight = this.queueSteerInFlight;
517668
517755
  if (inFlight === void 0 || inFlight.turnId !== String(turnId)) return;
@@ -517945,11 +518032,12 @@ var BlunTUI = class {
517945
518032
  this.injectTelegramRemoteCommand(envelope, remoteCommand, acknowledge);
517946
518033
  return;
517947
518034
  }
518035
+ const channelFocusId = addressed && urgentEnvelope === void 0 && directEnvelope === void 0 ? this.pinAddressedChannelFocus(routedEnvelope.meta.chat_id, channelOrigin(routedEnvelope), envelope.text) : void 0;
517948
518036
  injectChannelEnvelope({
517949
518037
  canDeliver: () => this.session !== void 0 && this.state.appState.model.trim().length > 0,
517950
518038
  isBusy: () => this.state.queuedMessages.length > 0 || this.queueSteerInFlight !== void 0 || this.deferUserMessages || this.streamingUI.hasActiveTurn() || this.state.appState.streamingPhase !== "idle" || this.state.appState.isCompacting,
517951
518039
  deliverNow: (modelInput, displayText, origin, contextOnly, channelReportSources) => {
517952
- this.sendChannelMessageInternal(this.requireSession(), modelInput, displayText, origin, routedEnvelope.meta.chat_id, routedEnvelope.meta["image_path"], contextOnly, false, acknowledge, false, void 0, directEnvelope !== void 0, directFocus.resumeGranted, channelReportSources);
518040
+ this.sendChannelMessageInternal(this.requireSession(), modelInput, displayText, origin, routedEnvelope.meta.chat_id, routedEnvelope.meta["image_path"], contextOnly, false, acknowledge, false, void 0, directEnvelope !== void 0, directFocus.resumeGranted, channelReportSources, channelFocusId);
517953
518041
  },
517954
518042
  enqueue: (modelInput, displayText, origin, contextOnly, channelReportSources) => {
517955
518043
  const item = {
@@ -517962,6 +518050,7 @@ var BlunTUI = class {
517962
518050
  channelReportSources: channelReportSources,
517963
518051
  channelContextOnly: contextOnly,
517964
518052
  channelAcknowledge: acknowledge,
518053
+ ...channelFocusId === void 0 ? {} : { channelFocusId },
517965
518054
  ...urgentEnvelope !== void 0 ? { channelUrgent: true } : {},
517966
518055
  ...directEnvelope !== void 0 ? { channelDirect: true, channelDirectResume: directFocus.resumeGranted } : {},
517967
518056
  ...addressed && urgentEnvelope === void 0 && directEnvelope === void 0 ? { channelAddressed: true } : {},
@@ -518119,7 +518208,7 @@ var BlunTUI = class {
518119
518208
  this.failSessionRequest(uiText("blunTui.session.sendFailed", { error: formatErrorMessage$2(error) }));
518120
518209
  });
518121
518210
  }
518122
- sendChannelMessageInternal(session, modelInput, displayText, origin, channelChatId, channelImagePath, contextOnly = false, transcriptRendered = false, acknowledge, channelAttention = false, queueKey, channelDirect = false, channelDirectResume = false, channelReportSources = []) {
518211
+ sendChannelMessageInternal(session, modelInput, displayText, origin, channelChatId, channelImagePath, contextOnly = false, transcriptRendered = false, acknowledge, channelAttention = false, queueKey, channelDirect = false, channelDirectResume = false, channelReportSources = [], channelFocusId) {
518123
518212
  armPersonalMemoryRememberIntent(session.id, displayText, {
518124
518213
  permissionMode: this.state.appState.permissionMode,
518125
518214
  channel: true
@@ -518139,7 +518228,8 @@ var BlunTUI = class {
518139
518228
  outboxMarker: outboxMarker(),
518140
518229
  transcriptStart: this.state.transcriptEntries.length,
518141
518230
  contextOnly,
518142
- directFocus: channelDirect
518231
+ directFocus: channelDirect,
518232
+ channelFocusId
518143
518233
  };
518144
518234
  if (installedGuard !== void 0) this.pendingChannelReplyGuard = installedGuard;
518145
518235
  const imagePart = channelImagePath !== void 0 ? buildChannelImagePart(channelImagePath) : void 0;
@@ -518170,6 +518260,7 @@ var BlunTUI = class {
518170
518260
  channelContextOnly: contextOnly,
518171
518261
  channelTranscriptRendered: true,
518172
518262
  channelAcknowledge: acknowledge,
518263
+ ...channelFocusId === void 0 ? {} : { channelFocusId, channelAddressed: true },
518173
518264
  ...channelAttention === true ? { channelAttention: true } : {},
518174
518265
  ...channelDirect === true ? { channelDirect: true, channelDirectResume } : {},
518175
518266
  ...queueKey === void 0 ? {} : { queueKey },
@@ -518262,6 +518353,7 @@ var BlunTUI = class {
518262
518353
  const args = entry.toolCallData?.args;
518263
518354
  return String(args?.["chat_id"] ?? args?.["chatId"] ?? "") === guard.chatId;
518264
518355
  }) || outboxGrewForChat(guard.outboxMarker, guard.chatId)) {
518356
+ this.clearAddressedChannelFocus?.(guard.chatId);
518265
518357
  if (guard.directFocus === true) this.directFocusController.noteReplyDelivered(guard.chatId);
518266
518358
  continue;
518267
518359
  }
@@ -518287,6 +518379,7 @@ var BlunTUI = class {
518287
518379
  if (finalText.length === 0) continue;
518288
518380
  sendReplyFallback(guard.chatId, finalText, guard.contextOnly).then((sent) => {
518289
518381
  if (!sent) return;
518382
+ this.clearAddressedChannelFocus?.(guard.chatId);
518290
518383
  if (guard.directFocus === true) this.directFocusController.noteReplyDelivered(guard.chatId);
518291
518384
  this.showStatus(uiText("blunTui.telegram.fallbackDelivered"));
518292
518385
  });
@@ -518530,7 +518623,7 @@ var BlunTUI = class {
518530
518623
  const activeSession = this.session ?? session;
518531
518624
  if (item.mode === "channel") {
518532
518625
  this.harness.withInteractiveAgent(item.agentId ?? "main", () => {
518533
- this.sendChannelMessageInternal(activeSession, item.text, item.displayText ?? "", item.origin, item.channelChatId, item.channelImagePath, item.channelContextOnly, item.channelTranscriptRendered, item.channelAcknowledge, item.channelAttention, item.queueKey, item.channelDirect, item.channelDirectResume, item.channelReportSources);
518626
+ this.sendChannelMessageInternal(activeSession, item.text, item.displayText ?? "", item.origin, item.channelChatId, item.channelImagePath, item.channelContextOnly, item.channelTranscriptRendered, item.channelAcknowledge, item.channelAttention, item.queueKey, item.channelDirect, item.channelDirectResume, item.channelReportSources, item.channelFocusId);
518534
518627
  });
518535
518628
  return;
518536
518629
  }
@@ -519134,9 +519227,11 @@ var BlunTUI = class {
519134
519227
  markTranscriptComponent(component, entry);
519135
519228
  this.state.transcriptContainer.addChild(component);
519136
519229
  }
519137
- const trimmed = component !== null && this.isTurnBoundaryComponent(component) ? this.trimTranscriptWindow() : false;
519230
+ const turnBoundary = component !== null && this.isTurnBoundaryComponent(component);
519231
+ const trimmed = turnBoundary ? this.trimTranscriptWindow() : false;
519138
519232
  const deferReplayWork = this.state.appState.isReplaying;
519139
519233
  const merged = deferReplayWork ? false : this.mergeCurrentTurnSteps();
519234
+ if (turnBoundary && !deferReplayWork) this.scrollbackController.persistActiveTurnCount(groupTurns(this.state.transcriptEntries).length);
519140
519235
  if ((component || trimmed || merged) && !deferReplayWork) this.state.ui.requestRender();
519141
519236
  }
519142
519237
  appendApprovalTranscriptEntry(request, response) {
@@ -520714,7 +520809,10 @@ function main() {
520714
520809
  });
520715
520810
  }).parse(process.argv);
520716
520811
  }
520717
- main();
520812
+ function isDirectCliEntry(argvEntry = process.argv[1], moduleFilename = __filename) {
520813
+ return typeof argvEntry === "string" && argvEntry.length > 0 && path.resolve(argvEntry) === path.resolve(moduleFilename);
520814
+ }
520815
+ if (isDirectCliEntry()) main();
520718
520816
  async function logStartupFailure(operation, error) {
520719
520817
  log.error("startup failed", {
520720
520818
  operation,
@@ -520725,4 +520823,4 @@ async function logStartupFailure(operation, error) {
520725
520823
  } catch {}
520726
520824
  }
520727
520825
  //#endregion
520728
- export { configureTelegramPluginLaunchMode, handleMainCommand, main };
520826
+ export { configureTelegramPluginLaunchMode, createBlunHarness, createBlunHostIdentity, handleMainCommand, isDirectCliEntry, main };
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "blun-king-cli",
3
- "version": "9.1.462",
3
+ "version": "9.1.464",
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": {