blun-king-cli 9.1.215 → 9.1.216
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/bin/validated-learning-signal.cjs +182 -0
- package/blun.mjs +6 -3
- package/package.json +1 -1
|
@@ -1,5 +1,12 @@
|
|
|
1
1
|
'use strict';
|
|
2
2
|
|
|
3
|
+
const crypto = require('node:crypto');
|
|
4
|
+
const fs = require('node:fs');
|
|
5
|
+
const path = require('node:path');
|
|
6
|
+
|
|
7
|
+
const CANDIDATE_TTL_MS = 24 * 60 * 60 * 1000;
|
|
8
|
+
const SCHEMA_VERSION = 1;
|
|
9
|
+
|
|
3
10
|
const COMMAND_TOOLS = new Set([
|
|
4
11
|
'bash',
|
|
5
12
|
'command',
|
|
@@ -101,7 +108,182 @@ function detectValidatedLearningSignal(history) {
|
|
|
101
108
|
return signal;
|
|
102
109
|
}
|
|
103
110
|
|
|
111
|
+
function profileKey(value) {
|
|
112
|
+
const normalized = typeof value === 'string' ? value.trim() : '';
|
|
113
|
+
return (normalized || 'default').replace(/[^a-z0-9._-]/giu, '_').slice(0, 64);
|
|
114
|
+
}
|
|
115
|
+
|
|
116
|
+
function candidateDirectory(homeDir, profile) {
|
|
117
|
+
return path.join(
|
|
118
|
+
path.resolve(homeDir),
|
|
119
|
+
'self-improvement',
|
|
120
|
+
'validated-learning',
|
|
121
|
+
profileKey(profile),
|
|
122
|
+
);
|
|
123
|
+
}
|
|
124
|
+
|
|
125
|
+
function atomicCreate(filePath, value) {
|
|
126
|
+
fs.mkdirSync(path.dirname(filePath), { mode: 0o700, recursive: true });
|
|
127
|
+
const temporary = `${filePath}.${process.pid}.${crypto.randomBytes(8).toString('hex')}.tmp`;
|
|
128
|
+
fs.writeFileSync(temporary, `${JSON.stringify(value)}\n`, { encoding: 'utf8', mode: 0o600 });
|
|
129
|
+
try {
|
|
130
|
+
fs.linkSync(temporary, filePath);
|
|
131
|
+
return true;
|
|
132
|
+
} catch (error) {
|
|
133
|
+
if (error?.code !== 'EEXIST') throw error;
|
|
134
|
+
return false;
|
|
135
|
+
} finally {
|
|
136
|
+
fs.rmSync(temporary, { force: true });
|
|
137
|
+
}
|
|
138
|
+
}
|
|
139
|
+
|
|
140
|
+
function statusMarker(filePath, status) {
|
|
141
|
+
return `${filePath}.${status}`;
|
|
142
|
+
}
|
|
143
|
+
|
|
144
|
+
function readCandidate(filePath) {
|
|
145
|
+
try {
|
|
146
|
+
const candidate = JSON.parse(fs.readFileSync(filePath, 'utf8'));
|
|
147
|
+
for (const status of ['recorded', 'expired']) {
|
|
148
|
+
const markerPath = statusMarker(filePath, status);
|
|
149
|
+
if (!fs.existsSync(markerPath)) continue;
|
|
150
|
+
const marker = JSON.parse(fs.readFileSync(markerPath, 'utf8'));
|
|
151
|
+
return { ...candidate, ...marker, filePath };
|
|
152
|
+
}
|
|
153
|
+
return { ...candidate, filePath };
|
|
154
|
+
} catch {
|
|
155
|
+
return null;
|
|
156
|
+
}
|
|
157
|
+
}
|
|
158
|
+
|
|
159
|
+
function readValidatedLearningCandidates(options = {}) {
|
|
160
|
+
const homeDir = typeof options.homeDir === 'string' ? options.homeDir.trim() : '';
|
|
161
|
+
if (!homeDir) return [];
|
|
162
|
+
const directory = candidateDirectory(homeDir, options.profile);
|
|
163
|
+
try {
|
|
164
|
+
return fs.readdirSync(directory, { withFileTypes: true })
|
|
165
|
+
.filter((entry) => entry.isFile() && entry.name.endsWith('.json'))
|
|
166
|
+
.map((entry) => readCandidate(path.join(directory, entry.name)))
|
|
167
|
+
.filter(Boolean)
|
|
168
|
+
.sort((left, right) => Date.parse(left.createdAt) - Date.parse(right.createdAt));
|
|
169
|
+
} catch {
|
|
170
|
+
return [];
|
|
171
|
+
}
|
|
172
|
+
}
|
|
173
|
+
|
|
174
|
+
function currentTurnHasSuccessfulMistakeRecord(history) {
|
|
175
|
+
const calls = new Set();
|
|
176
|
+
for (const message of currentUserTurn(history)) {
|
|
177
|
+
for (const call of toolCallsFrom(message)) {
|
|
178
|
+
if (typeof call?.id === 'string' && String(call.name).toLowerCase() === 'mistakerecord') {
|
|
179
|
+
calls.add(call.id);
|
|
180
|
+
}
|
|
181
|
+
}
|
|
182
|
+
if (message?.role === 'tool'
|
|
183
|
+
&& typeof message.toolCallId === 'string'
|
|
184
|
+
&& calls.has(message.toolCallId)
|
|
185
|
+
&& message.isError !== true) {
|
|
186
|
+
return true;
|
|
187
|
+
}
|
|
188
|
+
}
|
|
189
|
+
return false;
|
|
190
|
+
}
|
|
191
|
+
|
|
192
|
+
function newestPending(candidates) {
|
|
193
|
+
for (let index = candidates.length - 1; index >= 0; index -= 1) {
|
|
194
|
+
if (candidates[index].status === 'pending') return candidates[index];
|
|
195
|
+
}
|
|
196
|
+
return null;
|
|
197
|
+
}
|
|
198
|
+
|
|
199
|
+
function markCandidate(candidate, status, nowIso) {
|
|
200
|
+
if (!candidate?.filePath) return;
|
|
201
|
+
atomicCreate(statusMarker(candidate.filePath, status), {
|
|
202
|
+
resolvedAt: nowIso,
|
|
203
|
+
status,
|
|
204
|
+
});
|
|
205
|
+
}
|
|
206
|
+
|
|
207
|
+
function createCandidate(signal, options, nowIso) {
|
|
208
|
+
const profile = profileKey(options.profile);
|
|
209
|
+
const commandHash = crypto.createHash('sha256').update(signal.command).digest('hex');
|
|
210
|
+
const digest = crypto.createHash('sha256').update([
|
|
211
|
+
profile,
|
|
212
|
+
commandHash,
|
|
213
|
+
signal.failedToolCallId,
|
|
214
|
+
signal.successfulToolCallId,
|
|
215
|
+
].join('\0')).digest('hex').slice(0, 32);
|
|
216
|
+
const id = `vl-${digest}`;
|
|
217
|
+
const filePath = path.join(candidateDirectory(options.homeDir, profile), `${id}.json`);
|
|
218
|
+
atomicCreate(filePath, {
|
|
219
|
+
schemaVersion: SCHEMA_VERSION,
|
|
220
|
+
id,
|
|
221
|
+
type: 'validated_red_green',
|
|
222
|
+
status: 'pending',
|
|
223
|
+
profile,
|
|
224
|
+
verification: {
|
|
225
|
+
toolName: signal.toolName,
|
|
226
|
+
commandHash,
|
|
227
|
+
},
|
|
228
|
+
evidence: {
|
|
229
|
+
failedToolCallId: signal.failedToolCallId,
|
|
230
|
+
successfulToolCallId: signal.successfulToolCallId,
|
|
231
|
+
},
|
|
232
|
+
createdAt: nowIso,
|
|
233
|
+
});
|
|
234
|
+
return readCandidate(filePath);
|
|
235
|
+
}
|
|
236
|
+
|
|
237
|
+
function syncValidatedLearningCandidate(history, options = {}) {
|
|
238
|
+
const signal = detectValidatedLearningSignal(history);
|
|
239
|
+
const homeDir = typeof options.homeDir === 'string' ? options.homeDir.trim() : '';
|
|
240
|
+
if (!homeDir) {
|
|
241
|
+
return signal === null ? null : {
|
|
242
|
+
...signal,
|
|
243
|
+
source: 'current_turn',
|
|
244
|
+
status: 'pending',
|
|
245
|
+
};
|
|
246
|
+
}
|
|
247
|
+
|
|
248
|
+
const nowMs = (options.now || Date.now)();
|
|
249
|
+
const nowIso = new Date(nowMs).toISOString();
|
|
250
|
+
try {
|
|
251
|
+
let candidates = readValidatedLearningCandidates({
|
|
252
|
+
homeDir,
|
|
253
|
+
profile: options.profile,
|
|
254
|
+
});
|
|
255
|
+
if (currentTurnHasSuccessfulMistakeRecord(history)) {
|
|
256
|
+
markCandidate(newestPending(candidates), 'recorded', nowIso);
|
|
257
|
+
candidates = readValidatedLearningCandidates({ homeDir, profile: options.profile });
|
|
258
|
+
}
|
|
259
|
+
if (signal !== null) {
|
|
260
|
+
return {
|
|
261
|
+
...createCandidate(signal, { homeDir, profile: options.profile }, nowIso),
|
|
262
|
+
source: 'current_turn',
|
|
263
|
+
};
|
|
264
|
+
}
|
|
265
|
+
|
|
266
|
+
for (const candidate of candidates) {
|
|
267
|
+
if (candidate.status !== 'pending') continue;
|
|
268
|
+
if (nowMs - Date.parse(candidate.createdAt) >= CANDIDATE_TTL_MS) {
|
|
269
|
+
markCandidate(candidate, 'expired', nowIso);
|
|
270
|
+
}
|
|
271
|
+
}
|
|
272
|
+
candidates = readValidatedLearningCandidates({ homeDir, profile: options.profile });
|
|
273
|
+
const pending = newestPending(candidates);
|
|
274
|
+
return pending === null ? null : { ...pending, source: 'persisted' };
|
|
275
|
+
} catch {
|
|
276
|
+
return signal === null ? null : {
|
|
277
|
+
...signal,
|
|
278
|
+
source: 'current_turn',
|
|
279
|
+
status: 'pending',
|
|
280
|
+
};
|
|
281
|
+
}
|
|
282
|
+
}
|
|
283
|
+
|
|
104
284
|
module.exports = {
|
|
105
285
|
detectValidatedLearningSignal,
|
|
106
286
|
normalizeCommand,
|
|
287
|
+
readValidatedLearningCandidates,
|
|
288
|
+
syncValidatedLearningCandidate,
|
|
107
289
|
};
|
package/blun.mjs
CHANGED
|
@@ -232147,14 +232147,17 @@ function buildValidatedLearningSignalReminder() {
|
|
|
232147
232147
|
"</validated-learning-signal>"
|
|
232148
232148
|
].join("\n");
|
|
232149
232149
|
}
|
|
232150
|
-
var ValidatedLearningSignalInjector,
|
|
232150
|
+
var ValidatedLearningSignalInjector, syncValidatedLearningCandidate;
|
|
232151
232151
|
var init_validated_learning_signal = __esmMin((() => {
|
|
232152
232152
|
init_injector();
|
|
232153
|
-
({
|
|
232153
|
+
({ syncValidatedLearningCandidate } = createRequire(import.meta.url)("./bin/validated-learning-signal.cjs"));
|
|
232154
232154
|
ValidatedLearningSignalInjector = class extends DynamicInjector {
|
|
232155
232155
|
injectionVariant = "validated_learning_signal";
|
|
232156
232156
|
async inject() {
|
|
232157
|
-
const signal =
|
|
232157
|
+
const signal = syncValidatedLearningCandidate(this.agent.context.history, {
|
|
232158
|
+
homeDir: process.env["BLUN_SHARED_HOME"] ?? process.env["BLUN_HOME"] ?? "",
|
|
232159
|
+
profile: process.env["BLUN_PROFILE"] ?? "default"
|
|
232160
|
+
});
|
|
232158
232161
|
const existing = this.agent.context.history.filter(isValidatedLearningSignalReminder);
|
|
232159
232162
|
if (signal === null) {
|
|
232160
232163
|
if (existing.length > 0) this.agent.context.removeSystemRemindersMatching(isValidatedLearningSignalReminder);
|