blun-king-cli 9.1.215 → 9.1.217

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.
@@ -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,190 @@ 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 successfulMistakeRecordCandidateIds(history) {
175
+ const calls = new Map();
176
+ const candidateIds = new Set();
177
+ for (const message of currentUserTurn(history)) {
178
+ for (const call of toolCallsFrom(message)) {
179
+ if (typeof call?.id === 'string' && String(call.name).toLowerCase() === 'mistakerecord') {
180
+ const args = parseArguments(call.arguments);
181
+ calls.set(call.id, typeof args?.candidateId === 'string' ? args.candidateId.trim() : '');
182
+ }
183
+ }
184
+ if (message?.role === 'tool'
185
+ && typeof message.toolCallId === 'string'
186
+ && calls.get(message.toolCallId)
187
+ && message.isError !== true) {
188
+ candidateIds.add(calls.get(message.toolCallId));
189
+ }
190
+ }
191
+ return candidateIds;
192
+ }
193
+
194
+ function newestPending(candidates) {
195
+ for (let index = candidates.length - 1; index >= 0; index -= 1) {
196
+ if (candidates[index].status === 'pending') return candidates[index];
197
+ }
198
+ return null;
199
+ }
200
+
201
+ function markCandidate(candidate, status, nowIso) {
202
+ if (!candidate?.filePath) return;
203
+ atomicCreate(statusMarker(candidate.filePath, status), {
204
+ resolvedAt: nowIso,
205
+ status,
206
+ });
207
+ }
208
+
209
+ function createCandidate(signal, options, nowIso) {
210
+ const profile = profileKey(options.profile);
211
+ const commandHash = crypto.createHash('sha256').update(signal.command).digest('hex');
212
+ const digest = crypto.createHash('sha256').update([
213
+ profile,
214
+ commandHash,
215
+ signal.failedToolCallId,
216
+ signal.successfulToolCallId,
217
+ ].join('\0')).digest('hex').slice(0, 32);
218
+ const id = `vl-${digest}`;
219
+ const filePath = path.join(candidateDirectory(options.homeDir, profile), `${id}.json`);
220
+ atomicCreate(filePath, {
221
+ schemaVersion: SCHEMA_VERSION,
222
+ id,
223
+ type: 'validated_red_green',
224
+ status: 'pending',
225
+ profile,
226
+ verification: {
227
+ toolName: signal.toolName,
228
+ commandHash,
229
+ },
230
+ evidence: {
231
+ failedToolCallId: signal.failedToolCallId,
232
+ successfulToolCallId: signal.successfulToolCallId,
233
+ },
234
+ createdAt: nowIso,
235
+ });
236
+ return readCandidate(filePath);
237
+ }
238
+
239
+ function syncValidatedLearningCandidate(history, options = {}) {
240
+ const signal = detectValidatedLearningSignal(history);
241
+ const homeDir = typeof options.homeDir === 'string' ? options.homeDir.trim() : '';
242
+ if (!homeDir) {
243
+ return signal === null ? null : {
244
+ ...signal,
245
+ source: 'current_turn',
246
+ status: 'pending',
247
+ };
248
+ }
249
+
250
+ const nowMs = (options.now || Date.now)();
251
+ const nowIso = new Date(nowMs).toISOString();
252
+ try {
253
+ let candidates = readValidatedLearningCandidates({
254
+ homeDir,
255
+ profile: options.profile,
256
+ });
257
+ const recordedCandidateIds = successfulMistakeRecordCandidateIds(history);
258
+ for (const candidateId of recordedCandidateIds) {
259
+ const candidate = candidates.find((item) => (
260
+ item.id === candidateId && item.status === 'pending'
261
+ ));
262
+ markCandidate(candidate, 'recorded', nowIso);
263
+ }
264
+ if (recordedCandidateIds.size > 0) {
265
+ candidates = readValidatedLearningCandidates({ homeDir, profile: options.profile });
266
+ }
267
+ if (signal !== null) {
268
+ return {
269
+ ...createCandidate(signal, { homeDir, profile: options.profile }, nowIso),
270
+ source: 'current_turn',
271
+ };
272
+ }
273
+
274
+ for (const candidate of candidates) {
275
+ if (candidate.status !== 'pending') continue;
276
+ if (nowMs - Date.parse(candidate.createdAt) >= CANDIDATE_TTL_MS) {
277
+ markCandidate(candidate, 'expired', nowIso);
278
+ }
279
+ }
280
+ candidates = readValidatedLearningCandidates({ homeDir, profile: options.profile });
281
+ const pending = newestPending(candidates);
282
+ return pending === null ? null : { ...pending, source: 'persisted' };
283
+ } catch {
284
+ return signal === null ? null : {
285
+ ...signal,
286
+ source: 'current_turn',
287
+ status: 'pending',
288
+ };
289
+ }
290
+ }
291
+
104
292
  module.exports = {
105
293
  detectValidatedLearningSignal,
106
294
  normalizeCommand,
295
+ readValidatedLearningCandidates,
296
+ syncValidatedLearningCandidate,
107
297
  };
package/blun.mjs CHANGED
@@ -232137,30 +232137,37 @@ var init_repeated_assistant_response = __esmMin((() => {
232137
232137
  function isValidatedLearningSignalReminder(message) {
232138
232138
  return message.origin?.kind === "injection" && message.origin.variant === "validated_learning_signal";
232139
232139
  }
232140
- function buildValidatedLearningSignalReminder() {
232141
- return [
232140
+ function buildValidatedLearningSignalReminder(signal) {
232141
+ const lines = [
232142
232142
  "<validated-learning-signal>",
232143
232143
  "The same verification command failed and now succeeds in this user turn.",
232144
232144
  "Use MistakeRecord only when the red-to-green result exposes a reusable lesson.",
232145
232145
  "Do not record transient environment failures, raw command output, or a success with no general repetition guard.",
232146
- "If there is a reusable lesson, record the incorrect assumption, the measured correction, and the concrete condition that would cause it again.",
232147
- "</validated-learning-signal>"
232148
- ].join("\n");
232146
+ "If there is a reusable lesson, record the incorrect assumption, the measured correction, and the concrete condition that would cause it again."
232147
+ ];
232148
+ if (typeof signal?.id === "string" && signal.id.length > 0) {
232149
+ lines.push(`When recording this lesson, pass candidateId "${signal.id}" so only this candidate is resolved.`);
232150
+ }
232151
+ lines.push("</validated-learning-signal>");
232152
+ return lines.join("\n");
232149
232153
  }
232150
- var ValidatedLearningSignalInjector, detectValidatedLearningSignal;
232154
+ var ValidatedLearningSignalInjector, syncValidatedLearningCandidate;
232151
232155
  var init_validated_learning_signal = __esmMin((() => {
232152
232156
  init_injector();
232153
- ({ detectValidatedLearningSignal } = createRequire(import.meta.url)("./bin/validated-learning-signal.cjs"));
232157
+ ({ syncValidatedLearningCandidate } = createRequire(import.meta.url)("./bin/validated-learning-signal.cjs"));
232154
232158
  ValidatedLearningSignalInjector = class extends DynamicInjector {
232155
232159
  injectionVariant = "validated_learning_signal";
232156
232160
  async inject() {
232157
- const signal = detectValidatedLearningSignal(this.agent.context.history);
232161
+ const signal = syncValidatedLearningCandidate(this.agent.context.history, {
232162
+ homeDir: process.env["BLUN_SHARED_HOME"] ?? process.env["BLUN_HOME"] ?? "",
232163
+ profile: process.env["BLUN_PROFILE"] ?? "default"
232164
+ });
232158
232165
  const existing = this.agent.context.history.filter(isValidatedLearningSignalReminder);
232159
232166
  if (signal === null) {
232160
232167
  if (existing.length > 0) this.agent.context.removeSystemRemindersMatching(isValidatedLearningSignalReminder);
232161
232168
  return;
232162
232169
  }
232163
- const injection = buildValidatedLearningSignalReminder();
232170
+ const injection = buildValidatedLearningSignalReminder(signal);
232164
232171
  const expected = `<system-reminder>\n${injection}\n</system-reminder>`;
232165
232172
  if (existing.length === 1 && reminderText(existing[0]) === expected) return;
232166
232173
  this.agent.context.removeSystemRemindersMatching(isValidatedLearningSignalReminder);
@@ -263282,7 +263289,8 @@ var init_mistake_record = __esmMin((() => {
263282
263289
  about: string().min(1).describe("Who made the original claim (agent name, or \"me\" for yourself)."),
263283
263290
  claim: string().min(1).describe("What was claimed — one sentence."),
263284
263291
  measurement: string().min(1).describe("What was actually measured — one sentence."),
263285
- condition: string().min(1).describe("Under what condition this mistake will happen again. Required — without it the entry is discarded by A2.")
263292
+ condition: string().min(1).describe("Under what condition this mistake will happen again. Required — without it the entry is discarded by A2."),
263293
+ candidateId: string().optional().describe("Optional validated-learning candidate ID. Pass the exact ID from the current reminder so only that candidate is resolved.")
263286
263294
  }).strict();
263287
263295
  MistakeRecordTool = class {
263288
263296
  agent;
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "blun-king-cli",
3
- "version": "9.1.215",
3
+ "version": "9.1.217",
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": {