blun-king-cli 9.1.569 → 9.1.571

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
package/CHANGELOG.md ADDED
@@ -0,0 +1,55 @@
1
+ # Changelog
2
+
3
+ ## 9.1.571
4
+
5
+ - Bind Telegram queue ranges to their owning session. A new session no longer automatically consumes work left pending by an older session; resuming a known session restores only its own pending ranges.
6
+ - Pause channel intake across session selection, creation and switching. Preserve messages arriving after the chosen boundary, local editor follow-ups, and late acknowledgements for their original owner.
7
+ - Keep queue ownership and out-of-order acknowledgements durable across reattachment and lease handover. Reject corrupt or replaced queue state instead of silently replaying old input.
8
+ - Preserve the original inbox and session history. Legacy pending ranges without an identifiable session are parked rather than assigned to an unrelated new session. Automatic recovery of those unowned legacy ranges is not included.
9
+
10
+ This change does not diagnose or repair every model timeout, compaction stall or cancelled turn. An explicit paused-work state remains a pause. Older application versions do not understand the new session-queue checkpoint; downgrading is not a queue-migration mechanism.
11
+
12
+
13
+ - Apply the group-chatter correction to both the Telegram plugin and the console's independent automatic reply fallback.
14
+ - Suppress recognized acknowledgements and narrated silence even when they are longer than 200 characters. Preserve a message whenever it also contains an unrecognized or substantive clause.
15
+ - Preserve short useful group warnings. Message length alone no longer suppresses context-only replies in the console fallback.
16
+ - Explain that ordinary assistant text can be forwarded automatically. Group turns without a request or useful contribution should produce neither a reply tool call nor an assistant message announcing silence.
17
+ - Preserve private replies, group access settings, mention routing, media delivery, and the existing managed-plugin installation mechanism.
18
+
19
+ This is a conservative German/English delivery guard plus prompt guidance, not a guarantee that every model understands every group conversation. A new process must load the updated package; running sessions are not restarted by this release.
20
+
21
+ Packages pinned to the old internal update channel still require an explicit npm installation with King closed. Windows EBUSY remains a separate installation-lock problem; do not delete the package or stop unrelated processes on assumption.
22
+
23
+ All other changes from 9.1.570 remain included. This release does not activate the hosted web-search route or change AgentSpine, billing, authentication, or customer configuration.
24
+
25
+ ## 9.1.570
26
+
27
+ ### Fixed
28
+
29
+ - Give each new prompt delivery a distinct hook correlation ID. Hooks for the same delivery retain the same ID; explicitly supplied IDs are preserved.
30
+ - Pass session-specific runtime environment values to hook subprocesses and MCP connections, including reconnects, without rewriting stored plugin configuration.
31
+ - Preserve native tool-result correlation across turns, including failed tools and duplicate calls.
32
+ - Match canonically equivalent Unicode research queries without conflating different characters. Stored sources, dates, versions and project boundaries are preserved.
33
+ - Keep `king --help` and `king --version` from changing Windows crash-dump registry settings.
34
+
35
+ ### Added
36
+
37
+ - Support negotiated partial web-search results with a visible source-failure notice. Empty or incompatible partial responses remain explicit errors. This client change does not activate or guarantee a hosted search service.
38
+ - Add the default-off `BLUN_EXPERIMENTAL_AGENT_SPINE_TIMELINE` transport. When explicitly enabled, it passes the actual persisted session-record path and protocol to the selected AgentSpine plugin, with a separate capability per session and a new capability after resume. This is transport support, not proof of recall or authorization by the receiving plugin.
39
+
40
+ ### Preserved
41
+
42
+ - Includes the public 9.1.569 Telegram group mention routing, startup, permission persistence, research collection and crawler changes.
43
+ - The bundled AgentSpine implementation is unchanged from public 9.1.569. No Mnemo dependency is introduced.
44
+ - User profiles, permissions, conversations and plugin settings are not migrated or reset by this release.
45
+
46
+ ### Updating
47
+
48
+ - Public-channel installations use `king update`.
49
+ - Old packages explicitly pinned to the internal update channel do not discover public releases. Close the relevant King sessions, then migrate once with `npm install -g blun-king-cli@9.1.570`.
50
+ - A Windows `EBUSY` rename failure means the installation did not complete. Identify the process holding the package before retrying; do not delete the package or terminate unrelated Node processes.
51
+
52
+ ## 9.1.569
53
+
54
+ - Cumulative public release including Telegram group mention routing, startup and permission fixes, research collection, and crawler tooling.
55
+ - Published artifacts are immutable. Later changes ship under a new version.
@@ -12,7 +12,8 @@ const CORE_LOADED_MESSAGE = 'blun-core-bootstrap-loaded';
12
12
  async function runCoreBootstrap() {
13
13
  const packageRoot = path.resolve(__dirname, '..');
14
14
  const mainPath = path.join(packageRoot, 'blun.mjs');
15
- installWindowsNodeCrashDump({ homeDir: process.env.BLUN_HOME });
15
+ const informational = process.argv.slice(2).some(arg => ['-h', '--help', '-V', '--version'].includes(arg));
16
+ if (!informational) installWindowsNodeCrashDump({ homeDir: process.env.BLUN_HOME });
16
17
  const leaseResult = await acquireSharedRuntimeLease({ packageRoot });
17
18
  if (!leaseResult.acquired) throw new Error('RUNTIME_PROTECTION_UNAVAILABLE');
18
19
 
@@ -0,0 +1,306 @@
1
+ // ../king-console-i18n-20260813/apps/blun-king/src/tui/controllers/telegram-session-queue-delivery.ts
2
+ import { closeSync as closeSync2, fstatSync, openSync as openSync2, readSync } from "node:fs";
3
+ import { join } from "node:path";
4
+
5
+ // ../king-console-i18n-20260813/apps/blun-king/src/tui/constant/telegram-session-queue.ts
6
+ var SESSION_QUEUE_READ_BYTES = 65536;
7
+ var SESSION_QUEUE_MAX_IN_FLIGHT = 16;
8
+
9
+ // ../king-console-i18n-20260813/apps/blun-king/src/tui/utils/channel-injection.ts
10
+ function parseChannelEnvelope(line) {
11
+ const trimmed = line.trim();
12
+ if (trimmed.length === 0) return void 0;
13
+ let parsed;
14
+ try {
15
+ parsed = JSON.parse(trimmed);
16
+ } catch {
17
+ return void 0;
18
+ }
19
+ if (typeof parsed !== "object" || parsed === null) return void 0;
20
+ const record = parsed;
21
+ if (typeof record["text"] !== "string" || typeof record["tag"] !== "string") return void 0;
22
+ const meta = record["meta"];
23
+ if (typeof meta !== "object" || meta === null) return void 0;
24
+ if (typeof meta["chat_id"] !== "string") return void 0;
25
+ return record;
26
+ }
27
+
28
+ // ../king-console-i18n-20260813/apps/blun-king/src/tui/controllers/telegram-session-queue.ts
29
+ function requireValid(condition) {
30
+ if (!condition) throw new Error("Invalid Telegram session queue state or transition");
31
+ }
32
+ function isRecord(value) {
33
+ return typeof value === "object" && value !== null && !Array.isArray(value);
34
+ }
35
+ function isId(value) {
36
+ return typeof value === "string" && value.trim() === value && value.length > 0 && value.length <= 256 && !/[\u0000-\u001F\u007F]/u.test(value);
37
+ }
38
+ function isOffset(value) {
39
+ return typeof value === "number" && Number.isSafeInteger(value) && value >= 0;
40
+ }
41
+ function readSpan(value) {
42
+ requireValid(isRecord(value));
43
+ const { start, end } = value;
44
+ requireValid(isOffset(start) && isOffset(end) && start < end);
45
+ return { start, end };
46
+ }
47
+ function ownsRange(state, sessionId, span) {
48
+ return state.spans.some((owned) => owned.sessionId === sessionId && owned.start <= span.start && owned.end >= span.end);
49
+ }
50
+ function parseSessionQueue(value) {
51
+ requireValid(isRecord(value));
52
+ const { version, fileId, frontier, activeSessionId, sessions, spans, acknowledged } = value;
53
+ requireValid(version === 1 && isId(fileId) && isOffset(frontier) && isId(activeSessionId));
54
+ requireValid(Array.isArray(sessions) && sessions.every(isId));
55
+ const known = new Set(sessions);
56
+ requireValid(known.size === sessions.length && known.has(activeSessionId));
57
+ requireValid(Array.isArray(spans) && Array.isArray(acknowledged));
58
+ const owned = spans.map((raw) => {
59
+ const span = readSpan(raw);
60
+ requireValid(isRecord(raw));
61
+ const sessionId = raw["sessionId"];
62
+ requireValid(sessionId === null || isId(sessionId) && known.has(sessionId));
63
+ return { ...span, sessionId };
64
+ });
65
+ for (let i = 0; i < owned.length; i += 1) {
66
+ const span = owned[i];
67
+ const previous = owned[i - 1];
68
+ requireValid(span.end <= frontier && (previous === void 0 || previous.end === span.start));
69
+ requireValid(previous === void 0 || previous.sessionId !== span.sessionId);
70
+ }
71
+ requireValid(owned.length === 0 || owned.at(-1).end === frontier);
72
+ const confirmed = acknowledged.map(readSpan);
73
+ const state = {
74
+ version,
75
+ fileId,
76
+ frontier,
77
+ activeSessionId,
78
+ sessions: [...sessions],
79
+ spans: owned,
80
+ acknowledged: confirmed
81
+ };
82
+ for (let i = 0; i < confirmed.length; i += 1) {
83
+ const span = confirmed[i];
84
+ const previous = confirmed[i - 1];
85
+ requireValid(previous === void 0 || previous.end < span.start);
86
+ requireValid(owned.length > 0 && span.start >= owned[0].start && span.end <= frontier);
87
+ requireValid(!owned.some((part) => part.sessionId === null && part.start < span.end && part.end > span.start));
88
+ }
89
+ return state;
90
+ }
91
+ function createSessionQueue(input) {
92
+ requireValid(input.mode === "new" || input.mode === "resume");
93
+ requireValid(isOffset(input.checkpoint) && isOffset(input.frontier) && input.checkpoint <= input.frontier);
94
+ return parseSessionQueue({
95
+ version: 1,
96
+ fileId: input.fileId,
97
+ frontier: input.frontier,
98
+ activeSessionId: input.sessionId,
99
+ sessions: [input.sessionId],
100
+ spans: input.checkpoint === input.frontier ? [] : [{
101
+ start: input.checkpoint,
102
+ end: input.frontier,
103
+ sessionId: input.mode === "resume" ? input.sessionId : null
104
+ }],
105
+ acknowledged: []
106
+ });
107
+ }
108
+ function observeSessionQueue(state, position) {
109
+ const next = parseSessionQueue(state);
110
+ requireValid(position.fileId === next.fileId && isOffset(position.frontier) && position.frontier >= next.frontier);
111
+ if (position.frontier === next.frontier) return next;
112
+ const last = next.spans.at(-1);
113
+ if (last?.sessionId === next.activeSessionId) last.end = position.frontier;
114
+ else next.spans.push({ start: next.frontier, end: position.frontier, sessionId: next.activeSessionId });
115
+ next.frontier = position.frontier;
116
+ return next;
117
+ }
118
+ function bindSessionQueue(state, binding) {
119
+ requireValid(isId(binding.sessionId) && (binding.mode === "new" || binding.mode === "resume"));
120
+ const next = observeSessionQueue(state, binding);
121
+ requireValid(binding.mode !== "new" || !next.sessions.includes(binding.sessionId));
122
+ if (!next.sessions.includes(binding.sessionId)) next.sessions.push(binding.sessionId);
123
+ next.activeSessionId = binding.sessionId;
124
+ return next;
125
+ }
126
+ function pendingSessionQueue(state, sessionId) {
127
+ const validated = parseSessionQueue(state);
128
+ requireValid(isId(sessionId));
129
+ const pending = [];
130
+ for (const span of validated.spans) {
131
+ if (span.sessionId !== sessionId) continue;
132
+ let cursor = span.start;
133
+ for (const confirmed of validated.acknowledged) {
134
+ if (confirmed.end <= cursor) continue;
135
+ if (confirmed.start >= span.end) break;
136
+ if (confirmed.start > cursor) pending.push({ fileId: validated.fileId, start: cursor, end: confirmed.start });
137
+ cursor = Math.min(span.end, Math.max(cursor, confirmed.end));
138
+ }
139
+ if (cursor < span.end) pending.push({ fileId: validated.fileId, start: cursor, end: span.end });
140
+ }
141
+ return pending;
142
+ }
143
+ function acknowledgeSessionQueue(state, sessionId, range) {
144
+ const next = parseSessionQueue(state);
145
+ const span = readSpan(range);
146
+ requireValid(range.fileId === next.fileId && isId(sessionId) && ownsRange(next, sessionId, span));
147
+ const sorted = [...next.acknowledged, span].toSorted((a, b) => a.start - b.start);
148
+ next.acknowledged = [];
149
+ for (const item of sorted) {
150
+ const previous = next.acknowledged.at(-1);
151
+ if (previous !== void 0 && item.start <= previous.end) previous.end = Math.max(previous.end, item.end);
152
+ else next.acknowledged.push({ ...item });
153
+ }
154
+ return next;
155
+ }
156
+
157
+ // ../king-console-i18n-20260813/apps/blun-king/src/tui/controllers/telegram-session-queue-store.ts
158
+ import { randomUUID } from "node:crypto";
159
+ import { closeSync, fsyncSync, openSync, readFileSync, renameSync, rmSync, writeFileSync } from "node:fs";
160
+ function readSessionQueue(path) {
161
+ try {
162
+ return parseSessionQueue(JSON.parse(readFileSync(path, "utf8")));
163
+ } catch (error) {
164
+ if (error.code === "ENOENT") return void 0;
165
+ throw error;
166
+ }
167
+ }
168
+ function writeSessionQueue(path, state) {
169
+ const bytes = `${JSON.stringify(parseSessionQueue(state))}
170
+ `;
171
+ const temporary = `${path}.${randomUUID()}.tmp`;
172
+ const fd = openSync(temporary, "wx", 384);
173
+ try {
174
+ try {
175
+ writeFileSync(fd, bytes, "utf8");
176
+ fsyncSync(fd);
177
+ } finally {
178
+ closeSync(fd);
179
+ }
180
+ renameSync(temporary, path);
181
+ } finally {
182
+ rmSync(temporary, { force: true });
183
+ }
184
+ }
185
+
186
+ // ../king-console-i18n-20260813/apps/blun-king/src/tui/controllers/telegram-session-queue-delivery.ts
187
+ function captureSessionQueueBoundary(queueFile) {
188
+ let fd;
189
+ try {
190
+ fd = openSync2(queueFile, "r");
191
+ } catch (error) {
192
+ if (error.code === "ENOENT") return void 0;
193
+ throw error;
194
+ }
195
+ try {
196
+ const stats = fstatSync(fd);
197
+ const fileId = `${stats.dev}:${stats.ino}:${stats.birthtimeMs}`;
198
+ let end = stats.size;
199
+ const buffer = Buffer.alloc(SESSION_QUEUE_READ_BYTES);
200
+ while (end > 0) {
201
+ const start = Math.max(0, end - buffer.length);
202
+ const length = readSync(fd, buffer, 0, end - start, start);
203
+ const newline = buffer.subarray(0, length).lastIndexOf(10);
204
+ if (newline >= 0) return { fileId, frontier: start + newline + 1 };
205
+ end = start;
206
+ }
207
+ return { fileId, frontier: 0 };
208
+ } finally {
209
+ closeSync2(fd);
210
+ }
211
+ }
212
+ function* readLines(queueFile, range) {
213
+ const fd = openSync2(queueFile, "r");
214
+ try {
215
+ const stats = fstatSync(fd);
216
+ if (`${stats.dev}:${stats.ino}:${stats.birthtimeMs}` !== range.fileId || stats.size < range.end) {
217
+ throw new Error("Telegram queue changed during delivery");
218
+ }
219
+ let cursor = range.start;
220
+ let lineStart = range.start;
221
+ let remainder = Buffer.alloc(0);
222
+ while (cursor < range.end) {
223
+ const buffer = Buffer.alloc(Math.min(SESSION_QUEUE_READ_BYTES, range.end - cursor));
224
+ const read = readSync(fd, buffer, 0, buffer.length, cursor);
225
+ if (read === 0) throw new Error("Telegram queue changed during delivery");
226
+ cursor += read;
227
+ const combined = Buffer.concat([remainder, buffer.subarray(0, read)]);
228
+ let from = 0;
229
+ for (let index = 0; index < combined.length; index += 1) {
230
+ if (combined[index] !== 10) continue;
231
+ const end = lineStart + index - from + 1;
232
+ yield { text: combined.subarray(from, index).toString("utf8"), range: { fileId: range.fileId, start: lineStart, end } };
233
+ lineStart = end;
234
+ from = index + 1;
235
+ }
236
+ remainder = combined.subarray(from);
237
+ }
238
+ if (remainder.length > 0) throw new Error("Telegram queue range is not a complete record");
239
+ } finally {
240
+ closeSync2(fd);
241
+ }
242
+ }
243
+ var SessionQueueDelivery = class {
244
+ constructor(dir, host) {
245
+ this.host = host;
246
+ this.queueFile = join(dir, "inbound-queue.jsonl");
247
+ this.stateFile = join(dir, "session-queue.json");
248
+ }
249
+ host;
250
+ boundSessionId;
251
+ inFlight = /* @__PURE__ */ new Set();
252
+ queueFile;
253
+ stateFile;
254
+ poll(scope, deliver = true) {
255
+ if (scope === void 0 || !this.host.ownsChannel()) return;
256
+ const position = captureSessionQueueBoundary(this.queueFile);
257
+ if (position === void 0) return;
258
+ let state = readSessionQueue(this.stateFile);
259
+ const before = state === void 0 ? void 0 : JSON.stringify(state);
260
+ const rebind = this.boundSessionId !== scope.sessionId || state?.activeSessionId !== scope.sessionId;
261
+ if (rebind && state?.activeSessionId !== scope.sessionId) {
262
+ const boundary = this.boundSessionId === scope.sessionId ? position : scope.boundary === "absent" ? { fileId: position.fileId, frontier: 0 } : scope.boundary ?? position;
263
+ if (boundary.fileId !== position.fileId || boundary.frontier > position.frontier) {
264
+ throw new Error("Telegram session queue boundary is stale");
265
+ }
266
+ state = state === void 0 ? createSessionQueue({ ...boundary, checkpoint: Math.min(this.host.legacyCheckpoint(), boundary.frontier), sessionId: scope.sessionId, mode: scope.mode }) : bindSessionQueue(state, {
267
+ fileId: position.fileId,
268
+ frontier: Math.max(state.frontier, boundary.frontier),
269
+ sessionId: scope.sessionId,
270
+ mode: state.sessions.includes(scope.sessionId) ? "resume" : scope.mode
271
+ });
272
+ }
273
+ if (state === void 0) throw new Error("Telegram session queue checkpoint is missing");
274
+ state = observeSessionQueue(state, position);
275
+ if (JSON.stringify(state) !== before) writeSessionQueue(this.stateFile, state);
276
+ if (rebind) this.inFlight.clear();
277
+ this.boundSessionId = scope.sessionId;
278
+ if (!deliver) return;
279
+ if (this.inFlight.size >= SESSION_QUEUE_MAX_IN_FLIGHT) return;
280
+ let delivered = 0;
281
+ for (const pending of pendingSessionQueue(state, scope.sessionId)) {
282
+ for (const line of readLines(this.queueFile, pending)) {
283
+ if (!this.host.ownsChannel() || this.host.currentSessionId() !== scope.sessionId) return;
284
+ const key = `${line.range.fileId}:${line.range.start}:${line.range.end}`;
285
+ if (this.inFlight.has(key)) continue;
286
+ const acknowledge = () => {
287
+ if (!this.host.ownsChannel()) return;
288
+ const current = readSessionQueue(this.stateFile);
289
+ if (current === void 0) throw new Error("Telegram session queue checkpoint is missing");
290
+ writeSessionQueue(this.stateFile, acknowledgeSessionQueue(current, scope.sessionId, line.range));
291
+ this.inFlight.delete(key);
292
+ };
293
+ this.inFlight.add(key);
294
+ const envelope = parseChannelEnvelope(line.text);
295
+ if (envelope === void 0) acknowledge();
296
+ else this.host.inject(envelope, acknowledge);
297
+ delivered += 1;
298
+ if (delivered >= SESSION_QUEUE_MAX_IN_FLIGHT || this.inFlight.size >= SESSION_QUEUE_MAX_IN_FLIGHT) return;
299
+ }
300
+ }
301
+ }
302
+ };
303
+ export {
304
+ SessionQueueDelivery,
305
+ captureSessionQueueBoundary
306
+ };