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
|
-
|
|
234817
|
-
|
|
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?.
|
|
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.
|
|
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();
|