blun-king-cli 9.1.458 → 9.1.459
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 = 1;
|
|
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
|
+
};
|
package/blun.mjs
CHANGED
|
@@ -10,6 +10,8 @@ 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";
|
|
13
15
|
import crypto$1, { createHash, randomBytes, randomInt, randomUUID, timingSafeEqual } from "node:crypto";
|
|
14
16
|
import * as fs$16 from "node:fs";
|
|
15
17
|
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 +46,8 @@ const { goalAutostartDecision, goalContinuationDecision } = cognitiveGoalAutosta
|
|
|
44
46
|
const { GoalTimeTriggerController } = cognitiveGoalTimeTriggerController;
|
|
45
47
|
const { recordRuntimeExit } = runtimeExitLedger;
|
|
46
48
|
const { resolveCompactionModelAlias } = compactionModelPolicy;
|
|
49
|
+
const { createAgentResumeSnapshot, restoreAgentResumeSnapshot } = agentResumeSnapshot;
|
|
50
|
+
const { loadResumeCheckpoint, writeResumeCheckpoint } = sessionResumeCheckpoint;
|
|
47
51
|
import { EventEmitter as EventEmitter$1 } from "node:events";
|
|
48
52
|
import { StringDecoder } from "node:string_decoder";
|
|
49
53
|
import co from "node:assert";
|
|
@@ -234802,6 +234806,7 @@ var init_persistence = __esmMin((() => {
|
|
|
234802
234806
|
filePath;
|
|
234803
234807
|
options;
|
|
234804
234808
|
pendingRecords = [];
|
|
234809
|
+
persistedRecordCount = 0;
|
|
234805
234810
|
shouldClear = false;
|
|
234806
234811
|
directorySynced = false;
|
|
234807
234812
|
flushPromise;
|
|
@@ -234811,10 +234816,15 @@ var init_persistence = __esmMin((() => {
|
|
|
234811
234816
|
this.options = options;
|
|
234812
234817
|
}
|
|
234813
234818
|
async *read() {
|
|
234819
|
+
yield* this.readFrom(0, 0);
|
|
234820
|
+
}
|
|
234821
|
+
async *readFrom(offset = 0, lineNumber = 0) {
|
|
234814
234822
|
await this.flush();
|
|
234815
234823
|
let line = "";
|
|
234816
|
-
|
|
234817
|
-
|
|
234824
|
+
const stream = createReadStream(this.filePath, {
|
|
234825
|
+
encoding: "utf8",
|
|
234826
|
+
start: offset
|
|
234827
|
+
});
|
|
234818
234828
|
try {
|
|
234819
234829
|
for await (const chunk of stream) {
|
|
234820
234830
|
line += chunk;
|
|
@@ -234893,6 +234903,8 @@ var init_persistence = __esmMin((() => {
|
|
|
234893
234903
|
try {
|
|
234894
234904
|
if (content.length > 0) await fh.writeFile(content, "utf8");
|
|
234895
234905
|
await fh.sync();
|
|
234906
|
+
if (shouldClear) this.persistedRecordCount = batch.length;
|
|
234907
|
+
else this.persistedRecordCount += batch.length;
|
|
234896
234908
|
} finally {
|
|
234897
234909
|
await fh.close();
|
|
234898
234910
|
}
|
|
@@ -235224,6 +235236,7 @@ var init_records = __esmMin((() => {
|
|
|
235224
235236
|
persistence;
|
|
235225
235237
|
_restoring = null;
|
|
235226
235238
|
metadataInitialized = false;
|
|
235239
|
+
messageTimes = new WeakMap();
|
|
235227
235240
|
constructor(agent, persistence) {
|
|
235228
235241
|
this.agent = agent;
|
|
235229
235242
|
this.persistence = persistence;
|
|
@@ -235237,6 +235250,7 @@ var init_records = __esmMin((() => {
|
|
|
235237
235250
|
...record,
|
|
235238
235251
|
time: Date.now()
|
|
235239
235252
|
};
|
|
235253
|
+
if (stamped.type === "context.append_message") this.messageTimes.set(stamped.message, stamped.time);
|
|
235240
235254
|
if (this.persistence !== void 0 && !this.metadataInitialized && stamped.type !== "metadata") {
|
|
235241
235255
|
this.persistence.append({
|
|
235242
235256
|
type: "metadata",
|
|
@@ -235251,6 +235265,7 @@ var init_records = __esmMin((() => {
|
|
|
235251
235265
|
restore(record) {
|
|
235252
235266
|
this._restoring = { time: record.time ?? Date.now() };
|
|
235253
235267
|
try {
|
|
235268
|
+
if (record.type === "context.append_message" && Number.isFinite(record.time)) this.messageTimes.set(record.message, record.time);
|
|
235254
235269
|
restoreAgentRecord(this.agent, record);
|
|
235255
235270
|
return this.agent.replayBuilder.finishRestoringRecord(record.type);
|
|
235256
235271
|
} finally {
|
|
@@ -235260,13 +235275,16 @@ var init_records = __esmMin((() => {
|
|
|
235260
235275
|
async replay(options = {}) {
|
|
235261
235276
|
if (!this.persistence) throw new Error("No persistence provided for AgentRecords");
|
|
235262
235277
|
const rewriteMigratedRecords = options.rewriteMigratedRecords ?? true;
|
|
235278
|
+
if (rewriteMigratedRecords && await this.tryReplayResumeCheckpoint()) return {};
|
|
235263
235279
|
let migrations = [];
|
|
235264
235280
|
let hasMetadata = false;
|
|
235265
235281
|
let shouldRewrite = false;
|
|
235266
235282
|
let warning;
|
|
235267
235283
|
const replayedRecords = rewriteMigratedRecords ? [] : void 0;
|
|
235268
235284
|
let completed = true;
|
|
235285
|
+
let replayedRecordCount = 0;
|
|
235269
235286
|
for await (const record of this.persistence.read()) {
|
|
235287
|
+
replayedRecordCount++;
|
|
235270
235288
|
if (!hasMetadata) {
|
|
235271
235289
|
if (record.type !== "metadata") throw new Error("AgentRecords replay expected metadata as the first record");
|
|
235272
235290
|
hasMetadata = true;
|
|
@@ -235295,9 +235313,45 @@ var init_records = __esmMin((() => {
|
|
|
235295
235313
|
this.persistence.rewrite(replayedRecords);
|
|
235296
235314
|
await this.persistence.flush();
|
|
235297
235315
|
}
|
|
235316
|
+
if (completed && this.persistence instanceof FileSystemAgentRecordPersistence) this.persistence.persistedRecordCount = replayedRecordCount;
|
|
235298
235317
|
if (completed && this.agent.blobStore !== void 0) for (const msg of this.agent.context.history) await this.agent.blobStore.rehydrateParts(msg.content);
|
|
235299
235318
|
return { warning };
|
|
235300
235319
|
}
|
|
235320
|
+
async tryReplayResumeCheckpoint() {
|
|
235321
|
+
if (!(this.persistence instanceof FileSystemAgentRecordPersistence)) return false;
|
|
235322
|
+
if (this.agent.replayBuilder.options.range !== void 0) return false;
|
|
235323
|
+
const checkpoint = await loadResumeCheckpoint({ wirePath: this.persistence.filePath });
|
|
235324
|
+
if (!checkpoint.ok) return false;
|
|
235325
|
+
if (!restoreAgentResumeSnapshot(this.agent, checkpoint.snapshot)) return false;
|
|
235326
|
+
this.metadataInitialized = true;
|
|
235327
|
+
let tailRecordCount = 0;
|
|
235328
|
+
for await (const record of this.persistence.readFrom(checkpoint.offset, checkpoint.recordCount)) {
|
|
235329
|
+
this.restore(record);
|
|
235330
|
+
tailRecordCount++;
|
|
235331
|
+
}
|
|
235332
|
+
this.persistence.persistedRecordCount = checkpoint.recordCount + tailRecordCount;
|
|
235333
|
+
if (this.agent.blobStore !== void 0) for (const msg of this.agent.context.history) await this.agent.blobStore.rehydrateParts(msg.content);
|
|
235334
|
+
return true;
|
|
235335
|
+
}
|
|
235336
|
+
async writeResumeCheckpoint() {
|
|
235337
|
+
if (!(this.persistence instanceof FileSystemAgentRecordPersistence)) return;
|
|
235338
|
+
if (this.agent.replayBuilder.options.range !== void 0) return;
|
|
235339
|
+
try {
|
|
235340
|
+
await this.flush();
|
|
235341
|
+
const wireOffset = statSync(this.persistence.filePath).size;
|
|
235342
|
+
const recordCount = this.persistence.persistedRecordCount;
|
|
235343
|
+
const snapshot = createAgentResumeSnapshot(this.agent);
|
|
235344
|
+
const result = await writeResumeCheckpoint({
|
|
235345
|
+
wirePath: this.persistence.filePath,
|
|
235346
|
+
wireOffset,
|
|
235347
|
+
recordCount,
|
|
235348
|
+
snapshot
|
|
235349
|
+
});
|
|
235350
|
+
if (!result.written) this.agent.log.warn("resume checkpoint was not written", { reason: result.reason });
|
|
235351
|
+
} catch (error) {
|
|
235352
|
+
this.agent.log.warn("resume checkpoint write failed", { error });
|
|
235353
|
+
}
|
|
235354
|
+
}
|
|
235301
235355
|
async flush() {
|
|
235302
235356
|
await this.persistence?.flush();
|
|
235303
235357
|
}
|
|
@@ -262495,6 +262549,7 @@ var init_turn = __esmMin((() => {
|
|
|
262495
262549
|
this.cognitiveLocalClaimsByTurn.delete(turnId);
|
|
262496
262550
|
this.stepFailureByTurn.delete(turnId);
|
|
262497
262551
|
await this.agent.records.flush();
|
|
262552
|
+
await this.agent.records.writeResumeCheckpoint();
|
|
262498
262553
|
return {
|
|
262499
262554
|
event: ended,
|
|
262500
262555
|
stopReason: completedStopReason,
|
|
@@ -265772,6 +265827,8 @@ var init_agent = __esmMin((() => {
|
|
|
265772
265827
|
await this.cron?.loadFromDisk();
|
|
265773
265828
|
this.context.finishResume();
|
|
265774
265829
|
this.turn.finishResume();
|
|
265830
|
+
await this.records.flush();
|
|
265831
|
+
await this.records.writeResumeCheckpoint();
|
|
265775
265832
|
} finally {
|
|
265776
265833
|
this.replayBuilder.postRestoring = false;
|
|
265777
265834
|
}
|