blun-king-cli 9.1.459 → 9.1.460

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.
@@ -4,7 +4,7 @@ const crypto = require('node:crypto');
4
4
  const fs = require('node:fs');
5
5
  const path = require('node:path');
6
6
 
7
- const RESUME_CHECKPOINT_SCHEMA_VERSION = 1;
7
+ const RESUME_CHECKPOINT_SCHEMA_VERSION = 2;
8
8
  const MAX_RESUME_SNAPSHOT_BYTES = 4 * 1024 * 1024;
9
9
  const MAX_RESUME_CHECKPOINT_BYTES = MAX_RESUME_SNAPSHOT_BYTES + 256 * 1024;
10
10
  const WIRE_SAMPLE_BYTES = 4096;
@@ -0,0 +1,206 @@
1
+ 'use strict';
2
+
3
+ const crypto = require('node:crypto');
4
+ const fs = require('node:fs');
5
+ const path = require('node:path');
6
+
7
+ const ARCHIVE_SCHEMA_VERSION = 1;
8
+ const INDEX_FILE = 'index.json';
9
+
10
+ function sessionScrollbackArchiveDirectory(wirePath) {
11
+ return path.join(path.dirname(wirePath), 'tui-scrollback-v1');
12
+ }
13
+
14
+ function isNonNegativeInteger(value) {
15
+ return Number.isSafeInteger(value) && value >= 0;
16
+ }
17
+
18
+ function writeAtomic(filePath, value) {
19
+ const temporaryPath = `${filePath}.tmp-${process.pid}-${crypto.randomBytes(8).toString('hex')}`;
20
+ try {
21
+ fs.writeFileSync(temporaryPath, value, {
22
+ encoding: 'utf8',
23
+ flag: 'wx',
24
+ mode: 0o600,
25
+ });
26
+ fs.renameSync(temporaryPath, filePath);
27
+ } finally {
28
+ try {
29
+ fs.rmSync(temporaryPath, { force: true });
30
+ } catch {}
31
+ }
32
+ }
33
+
34
+ class SessionScrollbackArchive {
35
+ constructor({ directory, reset = false, reuse = false }) {
36
+ if (typeof directory !== 'string' || directory.length === 0) {
37
+ throw new TypeError('session scrollback directory is required');
38
+ }
39
+ this.directory = path.resolve(directory);
40
+ this.chunks = [];
41
+ this.loadedChunks = new Map();
42
+ this.lineCount = 0;
43
+ this.loadedExisting = false;
44
+
45
+ if (reset) {
46
+ this.reset();
47
+ } else if (reuse) {
48
+ this.loadExisting();
49
+ }
50
+ }
51
+
52
+ get totalLines() {
53
+ return this.lineCount;
54
+ }
55
+
56
+ get loadedChunkCount() {
57
+ return this.loadedChunks.size;
58
+ }
59
+
60
+ append(lines) {
61
+ if (!Array.isArray(lines) || !lines.every((line) => typeof line === 'string')) {
62
+ return false;
63
+ }
64
+ if (lines.length === 0) return true;
65
+
66
+ fs.mkdirSync(this.directory, { recursive: true });
67
+ const file = `chunk-${String(this.chunks.length).padStart(8, '0')}-${crypto.randomBytes(6).toString('hex')}.json`;
68
+ const chunkPath = path.join(this.directory, file);
69
+ const chunk = {
70
+ file,
71
+ startLine: this.lineCount,
72
+ lineCount: lines.length,
73
+ };
74
+
75
+ try {
76
+ writeAtomic(chunkPath, JSON.stringify(lines));
77
+ const nextChunks = [...this.chunks, chunk];
78
+ const nextLineCount = this.lineCount + lines.length;
79
+ this.writeIndex(nextChunks, nextLineCount);
80
+ this.chunks = nextChunks;
81
+ this.lineCount = nextLineCount;
82
+ return true;
83
+ } catch {
84
+ try {
85
+ fs.rmSync(chunkPath, { force: true });
86
+ } catch {}
87
+ return false;
88
+ }
89
+ }
90
+
91
+ readRange(start, end) {
92
+ if (!Number.isFinite(start) || !Number.isFinite(end)) return [];
93
+ if (start >= end || start >= this.lineCount) return [];
94
+ const boundedStart = Math.max(0, Math.floor(start));
95
+ const boundedEnd = Math.min(this.lineCount, Math.floor(end));
96
+ const output = [];
97
+
98
+ for (let index = 0; index < this.chunks.length; index++) {
99
+ const chunk = this.chunks[index];
100
+ const chunkEnd = chunk.startLine + chunk.lineCount;
101
+ if (chunkEnd <= boundedStart) continue;
102
+ if (chunk.startLine >= boundedEnd) break;
103
+ const lines = this.readChunk(index);
104
+ const sliceStart = Math.max(0, boundedStart - chunk.startLine);
105
+ const sliceEnd = Math.min(lines.length, boundedEnd - chunk.startLine);
106
+ output.push(...lines.slice(sliceStart, sliceEnd));
107
+ }
108
+ return output;
109
+ }
110
+
111
+ reset() {
112
+ try {
113
+ fs.rmSync(this.directory, { recursive: true, force: true });
114
+ } catch {}
115
+ this.chunks = [];
116
+ this.loadedChunks.clear();
117
+ this.lineCount = 0;
118
+ this.loadedExisting = false;
119
+ }
120
+
121
+ clear() {
122
+ this.reset();
123
+ }
124
+
125
+ dispose() {
126
+ this.loadedChunks.clear();
127
+ }
128
+
129
+ loadExisting() {
130
+ const indexPath = path.join(this.directory, INDEX_FILE);
131
+ try {
132
+ const parsed = JSON.parse(fs.readFileSync(indexPath, 'utf8'));
133
+ if (
134
+ parsed?.schemaVersion !== ARCHIVE_SCHEMA_VERSION
135
+ || !isNonNegativeInteger(parsed.totalLines)
136
+ || !Array.isArray(parsed.chunks)
137
+ ) {
138
+ return;
139
+ }
140
+
141
+ let nextLine = 0;
142
+ const chunks = [];
143
+ for (const candidate of parsed.chunks) {
144
+ if (
145
+ typeof candidate?.file !== 'string'
146
+ || path.basename(candidate.file) !== candidate.file
147
+ || !candidate.file.startsWith('chunk-')
148
+ || !candidate.file.endsWith('.json')
149
+ || candidate.startLine !== nextLine
150
+ || !Number.isSafeInteger(candidate.lineCount)
151
+ || candidate.lineCount <= 0
152
+ ) {
153
+ return;
154
+ }
155
+ const chunkPath = path.join(this.directory, candidate.file);
156
+ if (!fs.statSync(chunkPath).isFile()) return;
157
+ chunks.push({
158
+ file: candidate.file,
159
+ startLine: candidate.startLine,
160
+ lineCount: candidate.lineCount,
161
+ });
162
+ nextLine += candidate.lineCount;
163
+ }
164
+ if (nextLine !== parsed.totalLines) return;
165
+
166
+ this.chunks = chunks;
167
+ this.lineCount = parsed.totalLines;
168
+ this.loadedExisting = true;
169
+ } catch {}
170
+ }
171
+
172
+ readChunk(index) {
173
+ const loaded = this.loadedChunks.get(index);
174
+ if (loaded !== undefined) return loaded;
175
+ const chunk = this.chunks[index];
176
+ if (chunk === undefined) return [];
177
+ try {
178
+ const value = JSON.parse(fs.readFileSync(path.join(this.directory, chunk.file), 'utf8'));
179
+ if (
180
+ !Array.isArray(value)
181
+ || value.length !== chunk.lineCount
182
+ || !value.every((line) => typeof line === 'string')
183
+ ) {
184
+ return [];
185
+ }
186
+ this.loadedChunks.set(index, value);
187
+ return value;
188
+ } catch {
189
+ return [];
190
+ }
191
+ }
192
+
193
+ writeIndex(chunks, totalLines) {
194
+ writeAtomic(path.join(this.directory, INDEX_FILE), JSON.stringify({
195
+ schemaVersion: ARCHIVE_SCHEMA_VERSION,
196
+ totalLines,
197
+ chunks,
198
+ }));
199
+ }
200
+ }
201
+
202
+ module.exports = {
203
+ ARCHIVE_SCHEMA_VERSION,
204
+ SessionScrollbackArchive,
205
+ sessionScrollbackArchiveDirectory,
206
+ };
package/blun.mjs CHANGED
@@ -12,6 +12,7 @@ import runtimeExitLedger from "./bin/runtime-exit-ledger.cjs";
12
12
  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
+ import sessionScrollbackArchive from "./bin/session-scrollback-archive.cjs";
15
16
  import crypto$1, { createHash, randomBytes, randomInt, randomUUID, timingSafeEqual } from "node:crypto";
16
17
  import * as fs$16 from "node:fs";
17
18
  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";
@@ -48,6 +49,7 @@ const { recordRuntimeExit } = runtimeExitLedger;
48
49
  const { resolveCompactionModelAlias } = compactionModelPolicy;
49
50
  const { createAgentResumeSnapshot, restoreAgentResumeSnapshot } = agentResumeSnapshot;
50
51
  const { loadResumeCheckpoint, writeResumeCheckpoint } = sessionResumeCheckpoint;
52
+ const { SessionScrollbackArchive, sessionScrollbackArchiveDirectory } = sessionScrollbackArchive;
51
53
  import { EventEmitter as EventEmitter$1 } from "node:events";
52
54
  import { StringDecoder } from "node:string_decoder";
53
55
  import co from "node:assert";
@@ -235236,6 +235238,7 @@ var init_records = __esmMin((() => {
235236
235238
  persistence;
235237
235239
  _restoring = null;
235238
235240
  metadataInitialized = false;
235241
+ resumeCheckpointRestored = false;
235239
235242
  messageTimes = new WeakMap();
235240
235243
  constructor(agent, persistence) {
235241
235244
  this.agent = agent;
@@ -235274,6 +235277,7 @@ var init_records = __esmMin((() => {
235274
235277
  }
235275
235278
  async replay(options = {}) {
235276
235279
  if (!this.persistence) throw new Error("No persistence provided for AgentRecords");
235280
+ this.resumeCheckpointRestored = false;
235277
235281
  const rewriteMigratedRecords = options.rewriteMigratedRecords ?? true;
235278
235282
  if (rewriteMigratedRecords && await this.tryReplayResumeCheckpoint()) return {};
235279
235283
  let migrations = [];
@@ -235331,6 +235335,7 @@ var init_records = __esmMin((() => {
235331
235335
  }
235332
235336
  this.persistence.persistedRecordCount = checkpoint.recordCount + tailRecordCount;
235333
235337
  if (this.agent.blobStore !== void 0) for (const msg of this.agent.context.history) await this.agent.blobStore.rehydrateParts(msg.content);
235338
+ this.resumeCheckpointRestored = true;
235334
235339
  return true;
235335
235340
  }
235336
235341
  async writeResumeCheckpoint() {
@@ -316822,6 +316827,8 @@ async function resumeSessionResult(summary, session, warning) {
316822
316827
  const usage = await api.getUsage({ agentId });
316823
316828
  agents[agentId] = {
316824
316829
  type: agent.type,
316830
+ resumeCheckpointRestored: agent.records.resumeCheckpointRestored === true,
316831
+ sessionWirePath: agent.records.persistence instanceof FileSystemAgentRecordPersistence ? agent.records.persistence.filePath : void 0,
316825
316832
  config,
316826
316833
  context,
316827
316834
  replay: agent.replayBuilder.buildResult(),
@@ -506093,6 +506100,27 @@ var ScrollbackBuffer = class {
506093
506100
  archiveLines(lines) {
506094
506101
  return this.archive.append(lines);
506095
506102
  }
506103
+ usePersistentArchive(directory, options) {
506104
+ this.releaseArchive();
506105
+ const archive = new SessionScrollbackArchive({
506106
+ directory,
506107
+ reset: options.reset,
506108
+ reuse: options.reuse
506109
+ });
506110
+ this.archive = archive;
506111
+ return archive.loadedExisting;
506112
+ }
506113
+ releaseArchive() {
506114
+ if (this.archive instanceof DiskBackedLineArchive) this.archive.clear();
506115
+ else this.archive.dispose();
506116
+ this.archive = new DiskBackedLineArchive();
506117
+ this.resetViewState();
506118
+ }
506119
+ dispose() {
506120
+ if (this.archive instanceof DiskBackedLineArchive) this.archive.clear();
506121
+ else this.archive.dispose();
506122
+ this.resetViewState();
506123
+ }
506096
506124
  activate() {
506097
506125
  this.active = true;
506098
506126
  this.scrollOffset = 0;
@@ -506103,6 +506131,9 @@ var ScrollbackBuffer = class {
506103
506131
  }
506104
506132
  clear() {
506105
506133
  this.archive.clear();
506134
+ this.resetViewState();
506135
+ }
506136
+ resetViewState() {
506106
506137
  this.snapshotLines = [];
506107
506138
  this.publishedLineCount = 0;
506108
506139
  this.deactivate();
@@ -506414,6 +506445,7 @@ var ScrollbackController = class {
506414
506445
  state;
506415
506446
  buffer;
506416
506447
  disposeListener;
506448
+ suppressReplayArchiveWrites = false;
506417
506449
  constructor(state) {
506418
506450
  this.state = state;
506419
506451
  }
@@ -506428,16 +506460,52 @@ var ScrollbackController = class {
506428
506460
  dispose() {
506429
506461
  this.disposeListener?.();
506430
506462
  this.disposeListener = void 0;
506431
- this.buffer?.clear();
506463
+ this.buffer?.dispose();
506432
506464
  if (this.state.ui instanceof BottomPinnedTUI) this.state.ui.scrollbackBuffer = void 0;
506433
506465
  this.buffer = void 0;
506434
506466
  }
506435
506467
  reset() {
506436
506468
  this.buffer?.clear();
506469
+ this.suppressReplayArchiveWrites = false;
506470
+ }
506471
+ releaseSessionArchive() {
506472
+ this.buffer?.releaseArchive();
506473
+ this.suppressReplayArchiveWrites = false;
506474
+ }
506475
+ prepareNewSession(session) {
506476
+ const sessionDirectory = session?.summary?.sessionDir;
506477
+ if (typeof sessionDirectory !== "string" || sessionDirectory.length === 0) return false;
506478
+ const wirePath = path.join(sessionDirectory, "agents", "main", "wire.jsonl");
506479
+ this.buffer?.usePersistentArchive(sessionScrollbackArchiveDirectory(wirePath), {
506480
+ reset: true,
506481
+ reuse: false
506482
+ });
506483
+ this.suppressReplayArchiveWrites = false;
506484
+ return true;
506485
+ }
506486
+ prepareSessionReplay(agent) {
506487
+ const buffer = this.buffer;
506488
+ if (buffer === void 0) return false;
506489
+ const persistence = agent?.records?.persistence;
506490
+ let directory;
506491
+ if (typeof persistence?.filePath === "string") directory = sessionScrollbackArchiveDirectory(persistence.filePath);
506492
+ else if (typeof agent?.sessionWirePath === "string") directory = sessionScrollbackArchiveDirectory(agent.sessionWirePath);
506493
+ else return false;
506494
+ const resumeCheckpointRestored = agent?.records?.resumeCheckpointRestored === true || agent?.resumeCheckpointRestored === true;
506495
+ const loadedExisting = buffer.usePersistentArchive(directory, {
506496
+ reset: !resumeCheckpointRestored,
506497
+ reuse: resumeCheckpointRestored
506498
+ });
506499
+ this.suppressReplayArchiveWrites = resumeCheckpointRestored && loadedExisting;
506500
+ return true;
506501
+ }
506502
+ finishSessionReplay() {
506503
+ this.suppressReplayArchiveWrites = false;
506437
506504
  }
506438
506505
  archiveComponents(components) {
506439
506506
  const buffer = this.buffer;
506440
506507
  if (buffer === void 0 || components.length === 0) return components.length === 0;
506508
+ if (this.suppressReplayArchiveWrites && this.state.appState.isReplaying) return true;
506441
506509
  try {
506442
506510
  const width = Math.max(1, this.state.terminal.columns);
506443
506511
  const lines = components.flatMap((component) => component.render(width));
@@ -509723,6 +509791,7 @@ var SessionReplayRenderer = class {
509723
509791
  this.host.showError(uiText("replay.error.unavailable"));
509724
509792
  return false;
509725
509793
  }
509794
+ this.host.scrollbackController.prepareSessionReplay(main);
509726
509795
  this.hydrateSnapshot(main);
509727
509796
  await this.renderRecords(main);
509728
509797
  this.applyTerminalBackgroundAgentStatuses(main);
@@ -509734,6 +509803,7 @@ var SessionReplayRenderer = class {
509734
509803
  this.host.showError(uiText("replay.error.failed", { error: message }));
509735
509804
  return false;
509736
509805
  } finally {
509806
+ this.host.scrollbackController.finishSessionReplay();
509737
509807
  this.host.setAppState({ isReplaying: false });
509738
509808
  }
509739
509809
  }
@@ -516940,7 +517010,7 @@ var BlunTUI = class {
516940
517010
  if (shouldReplayHistory) {
516941
517011
  await this.sessionReplay.hydrateFromReplay(this.requireSession());
516942
517012
  this.applyStartupPermissionAndPlanToAppState();
516943
- }
517013
+ } else if (this.session !== void 0) this.scrollbackController.prepareNewSession(this.session);
516944
517014
  const resumeState = this.session?.getResumeState();
516945
517015
  if (resumeState?.warning !== void 0) this.showStatus(uiText("blunTui.warning", { warning: resumeState.warning }), "warning");
516946
517016
  if (this.session !== void 0) {
@@ -518902,7 +518972,7 @@ var BlunTUI = class {
518902
518972
  await this.refreshSkillCommands(this.session);
518903
518973
  await this.refreshPluginCommands(this.session);
518904
518974
  } catch {}
518905
- this.clearTranscriptAndRedraw();
518975
+ this.clearTranscriptAndRedraw({ preserveScrollbackArchive: true });
518906
518976
  try {
518907
518977
  await this.sessionReplay.hydrateFromReplay(session);
518908
518978
  } catch (error) {
@@ -518974,7 +519044,8 @@ var BlunTUI = class {
518974
519044
  await this.refreshPluginCommands(this.session);
518975
519045
  } catch {}
518976
519046
  this.sessionEventHandler.startSubscription();
518977
- this.clearTranscriptAndRedraw();
519047
+ this.clearTranscriptAndRedraw({ preserveScrollbackArchive: true });
519048
+ this.scrollbackController.prepareNewSession(session);
518978
519049
  this.showStatus(uiText("blunTui.session.started", { sessionId: session.id }));
518979
519050
  this.showSessionWarnings(session);
518980
519051
  this.showConfigWarningsIfAny();
@@ -519090,9 +519161,10 @@ var BlunTUI = class {
519090
519161
  disposeTranscriptChildren() {
519091
519162
  for (const child of this.state.transcriptContainer.children) if (hasDispose(child)) child.dispose();
519092
519163
  }
519093
- clearTranscriptAndRedraw() {
519164
+ clearTranscriptAndRedraw(options = {}) {
519094
519165
  this.streamingUI.discardPending();
519095
- this.scrollbackController.reset();
519166
+ if (options.preserveScrollbackArchive === true) this.scrollbackController.releaseSessionArchive();
519167
+ else this.scrollbackController.reset();
519096
519168
  this.state.transcriptEntries = [];
519097
519169
  this.streamingUI.disposeActiveCompactionBlock();
519098
519170
  this.streamingUI.resetLiveText();
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "blun-king-cli",
3
- "version": "9.1.459",
3
+ "version": "9.1.460",
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": {