blun-king-cli 9.1.458 → 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.
@@ -0,0 +1,210 @@
1
+ 'use strict';
2
+
3
+ const AGENT_RESUME_SNAPSHOT_SCHEMA_VERSION = 1;
4
+
5
+ function copyJsonObject(value, fallback) {
6
+ if (value === undefined) return fallback;
7
+ return JSON.parse(JSON.stringify(value));
8
+ }
9
+
10
+ function captureMessageTimes(agent) {
11
+ const replayTimes = new Map();
12
+ for (const record of agent.replayBuilder.records) {
13
+ if (record?.type === 'message' && Number.isFinite(record.time)) {
14
+ replayTimes.set(record.message, record.time);
15
+ }
16
+ }
17
+ return agent.context.history.map((message) => {
18
+ const time = agent.records.messageTimes?.get(message) ?? replayTimes.get(message);
19
+ return Number.isFinite(time) ? time : null;
20
+ });
21
+ }
22
+
23
+ function createAgentResumeSnapshot(agent) {
24
+ const userTools = [...agent.tools.userTools.values()].map((tool) => ({
25
+ name: tool.name,
26
+ description: tool.description,
27
+ parameters: tool.parameters,
28
+ }));
29
+ return {
30
+ schemaVersion: AGENT_RESUME_SNAPSHOT_SCHEMA_VERSION,
31
+ context: {
32
+ history: agent.context.history,
33
+ tokenCount: agent.context.tokenCount,
34
+ tokenCountCoveredMessageCount: agent.context.tokenCountCoveredMessageCount,
35
+ lastAssistantAt: agent.context._lastAssistantAt,
36
+ messageTimes: captureMessageTimes(agent),
37
+ },
38
+ config: {
39
+ cwd: agent.config._cwd,
40
+ modelAlias: agent.config._modelAlias,
41
+ profileName: agent.config._profileName,
42
+ thinkingEffort: agent.config._thinkingEffort,
43
+ actionStyle: agent.config._actionStyle,
44
+ systemPrompt: agent.config._systemPrompt,
45
+ },
46
+ permission: {
47
+ hasModeOverride: agent.permission.modeOverride !== undefined,
48
+ modeOverride: agent.permission.modeOverride,
49
+ sessionApprovalRulePatterns: [...agent.permission.localSessionApprovalRulePatterns],
50
+ },
51
+ plan: {
52
+ active: agent.planMode._isActive,
53
+ id: agent.planMode._planId,
54
+ filePath: agent.planMode._planFilePath,
55
+ },
56
+ swarm: { active: agent.swarmMode.active },
57
+ usage: { byModel: agent.usage.byModel },
58
+ tools: {
59
+ userTools,
60
+ enabledTools: [...agent.tools.enabledTools],
61
+ mcpAccessPatterns: [...agent.tools.mcpAccessPatterns],
62
+ store: agent.tools.store,
63
+ allowedTools: agent.tools.allowedTools,
64
+ excludedTools: [...agent.tools.excludedTools],
65
+ },
66
+ goal: agent.goal.state ?? null,
67
+ turn: {
68
+ turnId: agent.turn.turnId,
69
+ telegramDeliveryIdentities: agent.turn.telegramDeliveryLedger.snapshot(),
70
+ },
71
+ microCompaction: { cutoff: agent.microCompaction.cutoff },
72
+ };
73
+ }
74
+
75
+ function isValidSnapshot(snapshot) {
76
+ return snapshot !== null
77
+ && typeof snapshot === 'object'
78
+ && snapshot.schemaVersion === AGENT_RESUME_SNAPSHOT_SCHEMA_VERSION
79
+ && snapshot.context !== null
80
+ && typeof snapshot.context === 'object'
81
+ && Array.isArray(snapshot.context.history)
82
+ && snapshot.config !== null
83
+ && typeof snapshot.config === 'object'
84
+ && typeof snapshot.config.cwd === 'string'
85
+ && snapshot.permission !== null
86
+ && typeof snapshot.permission === 'object'
87
+ && snapshot.tools !== null
88
+ && typeof snapshot.tools === 'object'
89
+ && Array.isArray(snapshot.tools.userTools)
90
+ && snapshot.turn !== null
91
+ && typeof snapshot.turn === 'object';
92
+ }
93
+
94
+ function restoreAgentResumeSnapshot(agent, snapshot) {
95
+ if (!isValidSnapshot(snapshot)) return false;
96
+
97
+ const previousRestoring = agent.records._restoring;
98
+ agent.records._restoring = { time: Date.now() };
99
+ try {
100
+ agent.config._cwd = snapshot.config.cwd;
101
+ agent.config._modelAlias = snapshot.config.modelAlias;
102
+ agent.config._profileName = snapshot.config.profileName;
103
+ agent.config._thinkingEffort = snapshot.config.thinkingEffort ?? 'off';
104
+ agent.config._actionStyle = snapshot.config.actionStyle ?? 'default';
105
+ agent.config._systemPrompt = snapshot.config.systemPrompt ?? '';
106
+ agent.kaos.chdir(snapshot.config.cwd);
107
+
108
+ agent.context._history = snapshot.context.history;
109
+ agent.context._tokenCount = Number.isFinite(snapshot.context.tokenCount)
110
+ ? snapshot.context.tokenCount
111
+ : 0;
112
+ agent.context.tokenCountCoveredMessageCount = Number.isInteger(
113
+ snapshot.context.tokenCountCoveredMessageCount,
114
+ ) ? Math.max(
115
+ 0,
116
+ Math.min(snapshot.context.history.length, snapshot.context.tokenCountCoveredMessageCount),
117
+ ) : 0;
118
+ agent.context._lastAssistantAt = Number.isFinite(snapshot.context.lastAssistantAt)
119
+ ? snapshot.context.lastAssistantAt
120
+ : null;
121
+ const messageTimes = Array.isArray(snapshot.context.messageTimes)
122
+ ? snapshot.context.messageTimes
123
+ : [];
124
+ if (agent.records.messageTimes?.set) {
125
+ snapshot.context.history.forEach((message, index) => {
126
+ const time = messageTimes[index];
127
+ if (Number.isFinite(time)) agent.records.messageTimes.set(message, time);
128
+ });
129
+ }
130
+ agent.context.openSteps.clear();
131
+ agent.context.pendingToolResultIds.clear();
132
+ agent.context.deferredMessages = [];
133
+ agent.context.markPendingTokenEstimateDirty();
134
+ agent.injection.onContextClear();
135
+
136
+ agent.permission.modeOverride = snapshot.permission.hasModeOverride === true
137
+ ? snapshot.permission.modeOverride
138
+ : undefined;
139
+ agent.permission.localSessionApprovalRulePatterns = new Set(
140
+ Array.isArray(snapshot.permission.sessionApprovalRulePatterns)
141
+ ? snapshot.permission.sessionApprovalRulePatterns
142
+ : [],
143
+ );
144
+
145
+ agent.planMode._isActive = snapshot.plan?.active === true;
146
+ agent.planMode._planId = agent.planMode._isActive ? snapshot.plan?.id ?? null : null;
147
+ agent.planMode._planFilePath = agent.planMode._isActive
148
+ ? snapshot.plan?.filePath ?? null
149
+ : null;
150
+ agent.swarmMode.active = snapshot.swarm?.active ?? null;
151
+ agent.usage.byModel = copyJsonObject(snapshot.usage?.byModel, {});
152
+ agent.usage.currentTurn = undefined;
153
+
154
+ agent.tools.userTools.clear();
155
+ for (const tool of snapshot.tools.userTools) {
156
+ if (!tool || typeof tool.name !== 'string' || !tool.name) continue;
157
+ agent.tools.registerUserTool({
158
+ name: tool.name,
159
+ description: typeof tool.description === 'string' ? tool.description : '',
160
+ parameters: tool.parameters ?? { type: 'object' },
161
+ });
162
+ }
163
+ agent.tools.enabledTools = new Set(
164
+ Array.isArray(snapshot.tools.enabledTools) ? snapshot.tools.enabledTools : [],
165
+ );
166
+ agent.tools.mcpAccessPatterns = Array.isArray(snapshot.tools.mcpAccessPatterns)
167
+ ? [...snapshot.tools.mcpAccessPatterns]
168
+ : [];
169
+ agent.tools.store = copyJsonObject(snapshot.tools.store, {});
170
+ agent.tools.allowedTools = Array.isArray(snapshot.tools.allowedTools)
171
+ ? [...snapshot.tools.allowedTools]
172
+ : undefined;
173
+ agent.tools.excludedTools = Array.isArray(snapshot.tools.excludedTools)
174
+ ? [...snapshot.tools.excludedTools]
175
+ : [];
176
+ if (agent.config.hasProvider) agent.tools.initializeBuiltinTools();
177
+
178
+ agent.goal.state = snapshot.goal === null
179
+ ? undefined
180
+ : copyJsonObject(snapshot.goal, undefined);
181
+ agent.turn.turnId = Number.isInteger(snapshot.turn.turnId) ? snapshot.turn.turnId : -1;
182
+ agent.turn.activeTurn = 'resuming';
183
+ agent.turn.steerBuffer.length = 0;
184
+ agent.turn.telegramDeliveryLedger.restore(snapshot.turn.telegramDeliveryIdentities);
185
+ agent.microCompaction.cutoff = Number.isInteger(snapshot.microCompaction?.cutoff)
186
+ ? Math.max(0, Math.min(snapshot.context.history.length, snapshot.microCompaction.cutoff))
187
+ : 0;
188
+
189
+ agent.toolResultBatchOffload.reset(agent.context._history);
190
+ agent.userMessageOffload.reset(agent.context._history);
191
+ agent.assistantMessageOffload.reset(agent.context._history);
192
+ agent.replayBuilder.records = agent.context._history.map((message, index) => {
193
+ const time = messageTimes[index];
194
+ return Number.isFinite(time)
195
+ ? { type: 'message', message, time }
196
+ : { type: 'message', message };
197
+ });
198
+ agent.replayBuilder.frozen = false;
199
+ agent.replayBuilder.segmentStart = 0;
200
+ return true;
201
+ } finally {
202
+ agent.records._restoring = previousRestoring;
203
+ }
204
+ }
205
+
206
+ module.exports = {
207
+ AGENT_RESUME_SNAPSHOT_SCHEMA_VERSION,
208
+ createAgentResumeSnapshot,
209
+ restoreAgentResumeSnapshot,
210
+ };
@@ -67,6 +67,22 @@ class TelegramDeliveryLedger {
67
67
  }
68
68
  return identity;
69
69
  }
70
+
71
+ snapshot() {
72
+ return [...this.identities];
73
+ }
74
+
75
+ restore(identities) {
76
+ this.identities.clear();
77
+ if (!Array.isArray(identities)) return;
78
+ for (const identity of identities) {
79
+ if (typeof identity !== 'string' || !identity || this.identities.has(identity)) continue;
80
+ this.identities.add(identity);
81
+ while (this.identities.size > this.limit) {
82
+ this.identities.delete(this.identities.values().next().value);
83
+ }
84
+ }
85
+ }
70
86
  }
71
87
 
72
88
  function repeatedUserMessageKey(message) {
@@ -0,0 +1,254 @@
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 RESUME_CHECKPOINT_SCHEMA_VERSION = 2;
8
+ const MAX_RESUME_SNAPSHOT_BYTES = 4 * 1024 * 1024;
9
+ const MAX_RESUME_CHECKPOINT_BYTES = MAX_RESUME_SNAPSHOT_BYTES + 256 * 1024;
10
+ const WIRE_SAMPLE_BYTES = 4096;
11
+
12
+ function resumeCheckpointPath(wirePath) {
13
+ return path.join(path.dirname(wirePath), 'resume-checkpoint.json');
14
+ }
15
+
16
+ function sha256(value) {
17
+ return crypto.createHash('sha256').update(value).digest('hex');
18
+ }
19
+
20
+ function invalid(reason) {
21
+ return { ok: false, reason };
22
+ }
23
+
24
+ function isNonNegativeInteger(value) {
25
+ return Number.isSafeInteger(value) && value >= 0;
26
+ }
27
+
28
+ function sampleRanges(offset) {
29
+ if (offset === 0) return [];
30
+ const length = Math.min(WIRE_SAMPLE_BYTES, offset);
31
+ const starts = [
32
+ 0,
33
+ Math.max(0, Math.floor((offset - length) / 2)),
34
+ Math.max(0, offset - length),
35
+ ];
36
+ return [...new Set(starts)].map((start) => ({
37
+ start,
38
+ length: Math.min(length, offset - start),
39
+ }));
40
+ }
41
+
42
+ async function readExact(handle, start, length) {
43
+ const buffer = Buffer.alloc(length);
44
+ let filled = 0;
45
+ while (filled < length) {
46
+ const { bytesRead } = await handle.read(
47
+ buffer,
48
+ filled,
49
+ length - filled,
50
+ start + filled,
51
+ );
52
+ if (bytesRead === 0) break;
53
+ filled += bytesRead;
54
+ }
55
+ return filled === length ? buffer : buffer.subarray(0, filled);
56
+ }
57
+
58
+ async function captureWireSamples(handle, offset) {
59
+ const samples = [];
60
+ for (const range of sampleRanges(offset)) {
61
+ const bytes = await readExact(handle, range.start, range.length);
62
+ if (bytes.length !== range.length) {
63
+ throw new Error('wire_changed_during_checkpoint');
64
+ }
65
+ samples.push({ ...range, sha256: sha256(bytes) });
66
+ }
67
+ return samples;
68
+ }
69
+
70
+ async function writeAtomic(filePath, bytes) {
71
+ const directory = path.dirname(filePath);
72
+ const temporaryPath = `${filePath}.tmp-${process.pid}-${crypto.randomBytes(8).toString('hex')}`;
73
+ let handle;
74
+ try {
75
+ handle = await fs.promises.open(temporaryPath, 'wx', 0o600);
76
+ await handle.writeFile(bytes);
77
+ await handle.sync();
78
+ await handle.close();
79
+ handle = undefined;
80
+ await fs.promises.rename(temporaryPath, filePath);
81
+ } finally {
82
+ if (handle) await handle.close().catch(() => {});
83
+ await fs.promises.rm(temporaryPath, { force: true }).catch(() => {});
84
+ }
85
+ }
86
+
87
+ async function writeResumeCheckpoint({ wirePath, wireOffset, recordCount, snapshot }) {
88
+ if (!isNonNegativeInteger(recordCount)) {
89
+ return { written: false, reason: 'record_count_invalid' };
90
+ }
91
+ if (wireOffset !== undefined && !isNonNegativeInteger(wireOffset)) {
92
+ return { written: false, reason: 'wire_offset_invalid' };
93
+ }
94
+
95
+ let snapshotJson;
96
+ try {
97
+ snapshotJson = JSON.stringify(snapshot);
98
+ } catch {
99
+ return { written: false, reason: 'snapshot_not_serializable' };
100
+ }
101
+ if (snapshotJson === undefined) {
102
+ return { written: false, reason: 'snapshot_not_serializable' };
103
+ }
104
+ const snapshotBytes = Buffer.byteLength(snapshotJson, 'utf8');
105
+ if (snapshotBytes > MAX_RESUME_SNAPSHOT_BYTES) {
106
+ return { written: false, reason: 'snapshot_too_large' };
107
+ }
108
+
109
+ const handle = await fs.promises.open(wirePath, 'r');
110
+ let stat;
111
+ let offset;
112
+ let samples;
113
+ try {
114
+ stat = await handle.stat();
115
+ offset = wireOffset ?? stat.size;
116
+ if (offset > stat.size) {
117
+ return { written: false, reason: 'wire_truncated' };
118
+ }
119
+ if (offset > 0) {
120
+ const boundary = await readExact(handle, offset - 1, 1);
121
+ if (boundary.length !== 1 || boundary[0] !== 0x0a) {
122
+ return { written: false, reason: 'wire_not_line_terminated' };
123
+ }
124
+ }
125
+ samples = await captureWireSamples(handle, offset);
126
+ } finally {
127
+ await handle.close();
128
+ }
129
+
130
+ const payload = {
131
+ schemaVersion: RESUME_CHECKPOINT_SCHEMA_VERSION,
132
+ createdAt: new Date().toISOString(),
133
+ wire: {
134
+ offset,
135
+ recordCount,
136
+ sizeAtCheckpoint: offset,
137
+ identity: {
138
+ ino: Number.isSafeInteger(stat.ino) ? stat.ino : null,
139
+ birthtimeMs: Number.isFinite(stat.birthtimeMs) ? stat.birthtimeMs : null,
140
+ samples,
141
+ },
142
+ },
143
+ snapshot,
144
+ snapshotSha256: sha256(snapshotJson),
145
+ };
146
+ const checkpointBytes = Buffer.from(`${JSON.stringify(payload)}\n`, 'utf8');
147
+ if (checkpointBytes.length > MAX_RESUME_CHECKPOINT_BYTES) {
148
+ return { written: false, reason: 'checkpoint_too_large' };
149
+ }
150
+
151
+ await writeAtomic(resumeCheckpointPath(wirePath), checkpointBytes);
152
+ return {
153
+ written: true,
154
+ offset,
155
+ recordCount,
156
+ checkpointPath: resumeCheckpointPath(wirePath),
157
+ };
158
+ }
159
+
160
+ async function loadResumeCheckpoint({ wirePath }) {
161
+ const checkpointPath = resumeCheckpointPath(wirePath);
162
+ let checkpointStat;
163
+ try {
164
+ checkpointStat = await fs.promises.stat(checkpointPath);
165
+ } catch (error) {
166
+ return invalid(error && error.code === 'ENOENT' ? 'checkpoint_missing' : 'checkpoint_unreadable');
167
+ }
168
+ if (checkpointStat.size > MAX_RESUME_CHECKPOINT_BYTES) {
169
+ return invalid('checkpoint_too_large');
170
+ }
171
+
172
+ let checkpoint;
173
+ try {
174
+ checkpoint = JSON.parse(await fs.promises.readFile(checkpointPath, 'utf8'));
175
+ } catch (error) {
176
+ return invalid(error instanceof SyntaxError ? 'checkpoint_invalid_json' : 'checkpoint_unreadable');
177
+ }
178
+ if (!checkpoint || checkpoint.schemaVersion !== RESUME_CHECKPOINT_SCHEMA_VERSION) {
179
+ return invalid('checkpoint_schema_unsupported');
180
+ }
181
+ const wire = checkpoint.wire;
182
+ if (
183
+ !wire
184
+ || !isNonNegativeInteger(wire.offset)
185
+ || !isNonNegativeInteger(wire.recordCount)
186
+ || !wire.identity
187
+ || !Array.isArray(wire.identity.samples)
188
+ || typeof checkpoint.snapshotSha256 !== 'string'
189
+ ) {
190
+ return invalid('checkpoint_invalid_shape');
191
+ }
192
+
193
+ let snapshotJson;
194
+ try {
195
+ snapshotJson = JSON.stringify(checkpoint.snapshot);
196
+ } catch {
197
+ return invalid('checkpoint_invalid_snapshot');
198
+ }
199
+ if (snapshotJson === undefined || Buffer.byteLength(snapshotJson, 'utf8') > MAX_RESUME_SNAPSHOT_BYTES) {
200
+ return invalid('checkpoint_invalid_snapshot');
201
+ }
202
+ if (sha256(snapshotJson) !== checkpoint.snapshotSha256) {
203
+ return invalid('checkpoint_snapshot_mismatch');
204
+ }
205
+
206
+ let handle;
207
+ try {
208
+ handle = await fs.promises.open(wirePath, 'r');
209
+ const stat = await handle.stat();
210
+ if (stat.size < wire.offset) return invalid('wire_truncated');
211
+ if (wire.offset > 0) {
212
+ const boundary = await readExact(handle, wire.offset - 1, 1);
213
+ if (boundary.length !== 1 || boundary[0] !== 0x0a) {
214
+ return invalid('wire_offset_not_line_boundary');
215
+ }
216
+ }
217
+ for (const sample of wire.identity.samples) {
218
+ if (
219
+ !sample
220
+ || !isNonNegativeInteger(sample.start)
221
+ || !isNonNegativeInteger(sample.length)
222
+ || sample.start + sample.length > wire.offset
223
+ || typeof sample.sha256 !== 'string'
224
+ ) {
225
+ return invalid('checkpoint_invalid_shape');
226
+ }
227
+ const bytes = await readExact(handle, sample.start, sample.length);
228
+ if (bytes.length !== sample.length || sha256(bytes) !== sample.sha256) {
229
+ return invalid('wire_identity_mismatch');
230
+ }
231
+ }
232
+ } catch (error) {
233
+ return invalid(error && error.code === 'ENOENT' ? 'wire_missing' : 'wire_unreadable');
234
+ } finally {
235
+ if (handle) await handle.close().catch(() => {});
236
+ }
237
+
238
+ return {
239
+ ok: true,
240
+ offset: wire.offset,
241
+ recordCount: wire.recordCount,
242
+ snapshot: checkpoint.snapshot,
243
+ checkpointPath,
244
+ };
245
+ }
246
+
247
+ module.exports = {
248
+ MAX_RESUME_CHECKPOINT_BYTES,
249
+ MAX_RESUME_SNAPSHOT_BYTES,
250
+ RESUME_CHECKPOINT_SCHEMA_VERSION,
251
+ loadResumeCheckpoint,
252
+ resumeCheckpointPath,
253
+ writeResumeCheckpoint,
254
+ };
@@ -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
@@ -10,6 +10,9 @@ import cognitiveGoalAutostartPolicy from "./bin/cognitive-goal-autostart-policy.
10
10
  import cognitiveGoalTimeTriggerController from "./bin/cognitive-goal-time-trigger-controller.cjs";
11
11
  import runtimeExitLedger from "./bin/runtime-exit-ledger.cjs";
12
12
  import compactionModelPolicy from "./bin/compaction-model-policy.cjs";
13
+ import agentResumeSnapshot from "./bin/agent-resume-snapshot.cjs";
14
+ import sessionResumeCheckpoint from "./bin/session-resume-checkpoint.cjs";
15
+ import sessionScrollbackArchive from "./bin/session-scrollback-archive.cjs";
13
16
  import crypto$1, { createHash, randomBytes, randomInt, randomUUID, timingSafeEqual } from "node:crypto";
14
17
  import * as fs$16 from "node:fs";
15
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";
@@ -44,6 +47,9 @@ const { goalAutostartDecision, goalContinuationDecision } = cognitiveGoalAutosta
44
47
  const { GoalTimeTriggerController } = cognitiveGoalTimeTriggerController;
45
48
  const { recordRuntimeExit } = runtimeExitLedger;
46
49
  const { resolveCompactionModelAlias } = compactionModelPolicy;
50
+ const { createAgentResumeSnapshot, restoreAgentResumeSnapshot } = agentResumeSnapshot;
51
+ const { loadResumeCheckpoint, writeResumeCheckpoint } = sessionResumeCheckpoint;
52
+ const { SessionScrollbackArchive, sessionScrollbackArchiveDirectory } = sessionScrollbackArchive;
47
53
  import { EventEmitter as EventEmitter$1 } from "node:events";
48
54
  import { StringDecoder } from "node:string_decoder";
49
55
  import co from "node:assert";
@@ -234802,6 +234808,7 @@ var init_persistence = __esmMin((() => {
234802
234808
  filePath;
234803
234809
  options;
234804
234810
  pendingRecords = [];
234811
+ persistedRecordCount = 0;
234805
234812
  shouldClear = false;
234806
234813
  directorySynced = false;
234807
234814
  flushPromise;
@@ -234811,10 +234818,15 @@ var init_persistence = __esmMin((() => {
234811
234818
  this.options = options;
234812
234819
  }
234813
234820
  async *read() {
234821
+ yield* this.readFrom(0, 0);
234822
+ }
234823
+ async *readFrom(offset = 0, lineNumber = 0) {
234814
234824
  await this.flush();
234815
234825
  let line = "";
234816
- let lineNumber = 0;
234817
- const stream = createReadStream(this.filePath, { encoding: "utf8" });
234826
+ const stream = createReadStream(this.filePath, {
234827
+ encoding: "utf8",
234828
+ start: offset
234829
+ });
234818
234830
  try {
234819
234831
  for await (const chunk of stream) {
234820
234832
  line += chunk;
@@ -234893,6 +234905,8 @@ var init_persistence = __esmMin((() => {
234893
234905
  try {
234894
234906
  if (content.length > 0) await fh.writeFile(content, "utf8");
234895
234907
  await fh.sync();
234908
+ if (shouldClear) this.persistedRecordCount = batch.length;
234909
+ else this.persistedRecordCount += batch.length;
234896
234910
  } finally {
234897
234911
  await fh.close();
234898
234912
  }
@@ -235224,6 +235238,8 @@ var init_records = __esmMin((() => {
235224
235238
  persistence;
235225
235239
  _restoring = null;
235226
235240
  metadataInitialized = false;
235241
+ resumeCheckpointRestored = false;
235242
+ messageTimes = new WeakMap();
235227
235243
  constructor(agent, persistence) {
235228
235244
  this.agent = agent;
235229
235245
  this.persistence = persistence;
@@ -235237,6 +235253,7 @@ var init_records = __esmMin((() => {
235237
235253
  ...record,
235238
235254
  time: Date.now()
235239
235255
  };
235256
+ if (stamped.type === "context.append_message") this.messageTimes.set(stamped.message, stamped.time);
235240
235257
  if (this.persistence !== void 0 && !this.metadataInitialized && stamped.type !== "metadata") {
235241
235258
  this.persistence.append({
235242
235259
  type: "metadata",
@@ -235251,6 +235268,7 @@ var init_records = __esmMin((() => {
235251
235268
  restore(record) {
235252
235269
  this._restoring = { time: record.time ?? Date.now() };
235253
235270
  try {
235271
+ if (record.type === "context.append_message" && Number.isFinite(record.time)) this.messageTimes.set(record.message, record.time);
235254
235272
  restoreAgentRecord(this.agent, record);
235255
235273
  return this.agent.replayBuilder.finishRestoringRecord(record.type);
235256
235274
  } finally {
@@ -235259,14 +235277,18 @@ var init_records = __esmMin((() => {
235259
235277
  }
235260
235278
  async replay(options = {}) {
235261
235279
  if (!this.persistence) throw new Error("No persistence provided for AgentRecords");
235280
+ this.resumeCheckpointRestored = false;
235262
235281
  const rewriteMigratedRecords = options.rewriteMigratedRecords ?? true;
235282
+ if (rewriteMigratedRecords && await this.tryReplayResumeCheckpoint()) return {};
235263
235283
  let migrations = [];
235264
235284
  let hasMetadata = false;
235265
235285
  let shouldRewrite = false;
235266
235286
  let warning;
235267
235287
  const replayedRecords = rewriteMigratedRecords ? [] : void 0;
235268
235288
  let completed = true;
235289
+ let replayedRecordCount = 0;
235269
235290
  for await (const record of this.persistence.read()) {
235291
+ replayedRecordCount++;
235270
235292
  if (!hasMetadata) {
235271
235293
  if (record.type !== "metadata") throw new Error("AgentRecords replay expected metadata as the first record");
235272
235294
  hasMetadata = true;
@@ -235295,9 +235317,46 @@ var init_records = __esmMin((() => {
235295
235317
  this.persistence.rewrite(replayedRecords);
235296
235318
  await this.persistence.flush();
235297
235319
  }
235320
+ if (completed && this.persistence instanceof FileSystemAgentRecordPersistence) this.persistence.persistedRecordCount = replayedRecordCount;
235298
235321
  if (completed && this.agent.blobStore !== void 0) for (const msg of this.agent.context.history) await this.agent.blobStore.rehydrateParts(msg.content);
235299
235322
  return { warning };
235300
235323
  }
235324
+ async tryReplayResumeCheckpoint() {
235325
+ if (!(this.persistence instanceof FileSystemAgentRecordPersistence)) return false;
235326
+ if (this.agent.replayBuilder.options.range !== void 0) return false;
235327
+ const checkpoint = await loadResumeCheckpoint({ wirePath: this.persistence.filePath });
235328
+ if (!checkpoint.ok) return false;
235329
+ if (!restoreAgentResumeSnapshot(this.agent, checkpoint.snapshot)) return false;
235330
+ this.metadataInitialized = true;
235331
+ let tailRecordCount = 0;
235332
+ for await (const record of this.persistence.readFrom(checkpoint.offset, checkpoint.recordCount)) {
235333
+ this.restore(record);
235334
+ tailRecordCount++;
235335
+ }
235336
+ this.persistence.persistedRecordCount = checkpoint.recordCount + tailRecordCount;
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;
235339
+ return true;
235340
+ }
235341
+ async writeResumeCheckpoint() {
235342
+ if (!(this.persistence instanceof FileSystemAgentRecordPersistence)) return;
235343
+ if (this.agent.replayBuilder.options.range !== void 0) return;
235344
+ try {
235345
+ await this.flush();
235346
+ const wireOffset = statSync(this.persistence.filePath).size;
235347
+ const recordCount = this.persistence.persistedRecordCount;
235348
+ const snapshot = createAgentResumeSnapshot(this.agent);
235349
+ const result = await writeResumeCheckpoint({
235350
+ wirePath: this.persistence.filePath,
235351
+ wireOffset,
235352
+ recordCount,
235353
+ snapshot
235354
+ });
235355
+ if (!result.written) this.agent.log.warn("resume checkpoint was not written", { reason: result.reason });
235356
+ } catch (error) {
235357
+ this.agent.log.warn("resume checkpoint write failed", { error });
235358
+ }
235359
+ }
235301
235360
  async flush() {
235302
235361
  await this.persistence?.flush();
235303
235362
  }
@@ -262495,6 +262554,7 @@ var init_turn = __esmMin((() => {
262495
262554
  this.cognitiveLocalClaimsByTurn.delete(turnId);
262496
262555
  this.stepFailureByTurn.delete(turnId);
262497
262556
  await this.agent.records.flush();
262557
+ await this.agent.records.writeResumeCheckpoint();
262498
262558
  return {
262499
262559
  event: ended,
262500
262560
  stopReason: completedStopReason,
@@ -265772,6 +265832,8 @@ var init_agent = __esmMin((() => {
265772
265832
  await this.cron?.loadFromDisk();
265773
265833
  this.context.finishResume();
265774
265834
  this.turn.finishResume();
265835
+ await this.records.flush();
265836
+ await this.records.writeResumeCheckpoint();
265775
265837
  } finally {
265776
265838
  this.replayBuilder.postRestoring = false;
265777
265839
  }
@@ -316765,6 +316827,8 @@ async function resumeSessionResult(summary, session, warning) {
316765
316827
  const usage = await api.getUsage({ agentId });
316766
316828
  agents[agentId] = {
316767
316829
  type: agent.type,
316830
+ resumeCheckpointRestored: agent.records.resumeCheckpointRestored === true,
316831
+ sessionWirePath: agent.records.persistence instanceof FileSystemAgentRecordPersistence ? agent.records.persistence.filePath : void 0,
316768
316832
  config,
316769
316833
  context,
316770
316834
  replay: agent.replayBuilder.buildResult(),
@@ -506036,6 +506100,27 @@ var ScrollbackBuffer = class {
506036
506100
  archiveLines(lines) {
506037
506101
  return this.archive.append(lines);
506038
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
+ }
506039
506124
  activate() {
506040
506125
  this.active = true;
506041
506126
  this.scrollOffset = 0;
@@ -506046,6 +506131,9 @@ var ScrollbackBuffer = class {
506046
506131
  }
506047
506132
  clear() {
506048
506133
  this.archive.clear();
506134
+ this.resetViewState();
506135
+ }
506136
+ resetViewState() {
506049
506137
  this.snapshotLines = [];
506050
506138
  this.publishedLineCount = 0;
506051
506139
  this.deactivate();
@@ -506357,6 +506445,7 @@ var ScrollbackController = class {
506357
506445
  state;
506358
506446
  buffer;
506359
506447
  disposeListener;
506448
+ suppressReplayArchiveWrites = false;
506360
506449
  constructor(state) {
506361
506450
  this.state = state;
506362
506451
  }
@@ -506371,16 +506460,52 @@ var ScrollbackController = class {
506371
506460
  dispose() {
506372
506461
  this.disposeListener?.();
506373
506462
  this.disposeListener = void 0;
506374
- this.buffer?.clear();
506463
+ this.buffer?.dispose();
506375
506464
  if (this.state.ui instanceof BottomPinnedTUI) this.state.ui.scrollbackBuffer = void 0;
506376
506465
  this.buffer = void 0;
506377
506466
  }
506378
506467
  reset() {
506379
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;
506380
506504
  }
506381
506505
  archiveComponents(components) {
506382
506506
  const buffer = this.buffer;
506383
506507
  if (buffer === void 0 || components.length === 0) return components.length === 0;
506508
+ if (this.suppressReplayArchiveWrites && this.state.appState.isReplaying) return true;
506384
506509
  try {
506385
506510
  const width = Math.max(1, this.state.terminal.columns);
506386
506511
  const lines = components.flatMap((component) => component.render(width));
@@ -509666,6 +509791,7 @@ var SessionReplayRenderer = class {
509666
509791
  this.host.showError(uiText("replay.error.unavailable"));
509667
509792
  return false;
509668
509793
  }
509794
+ this.host.scrollbackController.prepareSessionReplay(main);
509669
509795
  this.hydrateSnapshot(main);
509670
509796
  await this.renderRecords(main);
509671
509797
  this.applyTerminalBackgroundAgentStatuses(main);
@@ -509677,6 +509803,7 @@ var SessionReplayRenderer = class {
509677
509803
  this.host.showError(uiText("replay.error.failed", { error: message }));
509678
509804
  return false;
509679
509805
  } finally {
509806
+ this.host.scrollbackController.finishSessionReplay();
509680
509807
  this.host.setAppState({ isReplaying: false });
509681
509808
  }
509682
509809
  }
@@ -516883,7 +517010,7 @@ var BlunTUI = class {
516883
517010
  if (shouldReplayHistory) {
516884
517011
  await this.sessionReplay.hydrateFromReplay(this.requireSession());
516885
517012
  this.applyStartupPermissionAndPlanToAppState();
516886
- }
517013
+ } else if (this.session !== void 0) this.scrollbackController.prepareNewSession(this.session);
516887
517014
  const resumeState = this.session?.getResumeState();
516888
517015
  if (resumeState?.warning !== void 0) this.showStatus(uiText("blunTui.warning", { warning: resumeState.warning }), "warning");
516889
517016
  if (this.session !== void 0) {
@@ -518845,7 +518972,7 @@ var BlunTUI = class {
518845
518972
  await this.refreshSkillCommands(this.session);
518846
518973
  await this.refreshPluginCommands(this.session);
518847
518974
  } catch {}
518848
- this.clearTranscriptAndRedraw();
518975
+ this.clearTranscriptAndRedraw({ preserveScrollbackArchive: true });
518849
518976
  try {
518850
518977
  await this.sessionReplay.hydrateFromReplay(session);
518851
518978
  } catch (error) {
@@ -518917,7 +519044,8 @@ var BlunTUI = class {
518917
519044
  await this.refreshPluginCommands(this.session);
518918
519045
  } catch {}
518919
519046
  this.sessionEventHandler.startSubscription();
518920
- this.clearTranscriptAndRedraw();
519047
+ this.clearTranscriptAndRedraw({ preserveScrollbackArchive: true });
519048
+ this.scrollbackController.prepareNewSession(session);
518921
519049
  this.showStatus(uiText("blunTui.session.started", { sessionId: session.id }));
518922
519050
  this.showSessionWarnings(session);
518923
519051
  this.showConfigWarningsIfAny();
@@ -519033,9 +519161,10 @@ var BlunTUI = class {
519033
519161
  disposeTranscriptChildren() {
519034
519162
  for (const child of this.state.transcriptContainer.children) if (hasDispose(child)) child.dispose();
519035
519163
  }
519036
- clearTranscriptAndRedraw() {
519164
+ clearTranscriptAndRedraw(options = {}) {
519037
519165
  this.streamingUI.discardPending();
519038
- this.scrollbackController.reset();
519166
+ if (options.preserveScrollbackArchive === true) this.scrollbackController.releaseSessionArchive();
519167
+ else this.scrollbackController.reset();
519039
519168
  this.state.transcriptEntries = [];
519040
519169
  this.streamingUI.disposeActiveCompactionBlock();
519041
519170
  this.streamingUI.resetLiveText();
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "blun-king-cli",
3
- "version": "9.1.458",
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": {