blun-king-cli 9.1.444 → 9.1.446

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.
@@ -5,6 +5,11 @@ const DEFAULT_WINDOW_WORDS = 20;
5
5
  const DEFAULT_REQUIRED_OCCURRENCES = 4;
6
6
  const DEFAULT_MAX_CHARS = 24_000;
7
7
  const DEFAULT_CHECK_EVERY_WORDS = 24;
8
+ const DEFAULT_SHORT_BURST_MAX_WORDS = 8;
9
+ const DEFAULT_SHORT_BURST_MIN_OCCURRENCES = 8;
10
+ const DEFAULT_SHORT_BURST_MIN_TOTAL_WORDS = 24;
11
+ const DEFAULT_SHORT_BURST_MIN_WORDS = 2;
12
+ const DEFAULT_SHORT_BURST_SCAN_WORDS = 192;
8
13
 
9
14
  function normalizeWords(text) {
10
15
  return String(text)
@@ -40,16 +45,80 @@ function repeatedWindow(words, options) {
40
45
  return null;
41
46
  }
42
47
 
48
+ function equalWindow(words, left, right, width) {
49
+ for (let offset = 0; offset < width; offset += 1) {
50
+ if (words[left + offset] !== words[right + offset]) return false;
51
+ }
52
+ return true;
53
+ }
54
+
55
+ function canCloseConsecutiveWindow(words, minWords, maxWords) {
56
+ const lastIndex = words.length - 1;
57
+ for (let windowWords = minWords; windowWords <= maxWords; windowWords += 1) {
58
+ const previousIndex = lastIndex - windowWords;
59
+ if (previousIndex >= 0 && words[lastIndex] === words[previousIndex]) return true;
60
+ }
61
+ return false;
62
+ }
63
+
64
+ function repeatedConsecutiveWindow(words, options) {
65
+ const {
66
+ maxWords,
67
+ minOccurrences,
68
+ minTotalWords,
69
+ minWords,
70
+ scanWords,
71
+ firstEnd = 1,
72
+ } = options;
73
+ const sourceStart = Math.max(0, words.length - scanWords);
74
+ const localFirstEnd = Math.max(sourceStart + 1, firstEnd);
75
+
76
+ for (let end = localFirstEnd; end <= words.length; end += 1) {
77
+ for (let windowWords = minWords; windowWords <= maxWords; windowWords += 1) {
78
+ const requiredOccurrences = Math.max(
79
+ minOccurrences,
80
+ Math.ceil(minTotalWords / windowWords),
81
+ );
82
+ const requiredSpan = windowWords * requiredOccurrences;
83
+ const start = end - requiredSpan;
84
+ if (start < sourceStart) continue;
85
+ let count = 1;
86
+ let cursor = start + windowWords;
87
+ while (cursor + windowWords <= end
88
+ && equalWindow(words, start, cursor, windowWords)) {
89
+ count += 1;
90
+ cursor += windowWords;
91
+ }
92
+ if (count < requiredOccurrences) continue;
93
+ return {
94
+ kind: 'short_burst',
95
+ count,
96
+ firstWords: words.slice(start, start + windowWords).join(' '),
97
+ };
98
+ }
99
+ }
100
+ return null;
101
+ }
102
+
43
103
  function createLiveResponseRepetitionGuard(options = {}) {
44
104
  const config = {
45
105
  checkEveryWords: options.checkEveryWords ?? DEFAULT_CHECK_EVERY_WORDS,
46
106
  maxChars: options.maxChars ?? DEFAULT_MAX_CHARS,
47
107
  minWords: options.minWords ?? DEFAULT_MIN_WORDS,
48
108
  requiredOccurrences: options.requiredOccurrences ?? DEFAULT_REQUIRED_OCCURRENCES,
109
+ shortBurstMaxWords: options.shortBurstMaxWords ?? DEFAULT_SHORT_BURST_MAX_WORDS,
110
+ shortBurstMinOccurrences: options.shortBurstMinOccurrences
111
+ ?? DEFAULT_SHORT_BURST_MIN_OCCURRENCES,
112
+ shortBurstMinTotalWords: options.shortBurstMinTotalWords
113
+ ?? DEFAULT_SHORT_BURST_MIN_TOTAL_WORDS,
114
+ shortBurstMinWords: options.shortBurstMinWords ?? DEFAULT_SHORT_BURST_MIN_WORDS,
115
+ shortBurstScanWords: options.shortBurstScanWords ?? DEFAULT_SHORT_BURST_SCAN_WORDS,
49
116
  windowWords: options.windowWords ?? DEFAULT_WINDOW_WORDS,
50
117
  };
51
118
  let text = '';
52
119
  let lastCheckedWords = 0;
120
+ let lastShortBurstLastWord = '';
121
+ let lastShortBurstWordCount = 0;
53
122
  let detection = null;
54
123
 
55
124
  return {
@@ -57,12 +126,46 @@ function createLiveResponseRepetitionGuard(options = {}) {
57
126
  if (detection !== null || typeof delta !== 'string' || delta.length === 0) return detection;
58
127
  text = `${text}${delta}`.slice(-config.maxChars);
59
128
  const words = normalizeWords(text);
129
+ const previousShortBurstWordCount = lastShortBurstWordCount;
130
+ const lastWord = words.at(-1) ?? '';
131
+ const wordCountChanged = words.length !== previousShortBurstWordCount;
132
+ const lastWordChanged = lastWord !== lastShortBurstLastWord;
133
+ const firstChangedEnd = words.length > lastShortBurstWordCount
134
+ ? Math.max(1, lastShortBurstWordCount)
135
+ : words.length;
136
+ const shouldCheckShortBurst = wordCountChanged || (lastWordChanged
137
+ && canCloseConsecutiveWindow(
138
+ words,
139
+ config.shortBurstMinWords,
140
+ config.shortBurstMaxWords,
141
+ ));
142
+ lastShortBurstLastWord = lastWord;
143
+ lastShortBurstWordCount = words.length;
144
+ if (words.length >= config.shortBurstMinTotalWords && shouldCheckShortBurst) {
145
+ const shortBurst = repeatedConsecutiveWindow(words, {
146
+ firstEnd: firstChangedEnd,
147
+ maxWords: config.shortBurstMaxWords,
148
+ minOccurrences: config.shortBurstMinOccurrences,
149
+ minTotalWords: config.shortBurstMinTotalWords,
150
+ minWords: config.shortBurstMinWords,
151
+ scanWords: config.shortBurstScanWords,
152
+ });
153
+ if (shortBurst !== null) {
154
+ detection = {
155
+ ...shortBurst,
156
+ charCount: text.length,
157
+ wordCount: words.length,
158
+ };
159
+ return detection;
160
+ }
161
+ }
60
162
  if (words.length < config.minWords) return null;
61
163
  if (words.length - lastCheckedWords < config.checkEveryWords) return null;
62
164
  lastCheckedWords = words.length;
63
165
  const repeated = repeatedWindow(words, config);
64
166
  if (repeated === null) return null;
65
167
  detection = {
168
+ kind: 'long_window',
66
169
  ...repeated,
67
170
  charCount: text.length,
68
171
  wordCount: words.length,
@@ -80,8 +183,14 @@ module.exports = {
80
183
  DEFAULT_MAX_CHARS,
81
184
  DEFAULT_MIN_WORDS,
82
185
  DEFAULT_REQUIRED_OCCURRENCES,
186
+ DEFAULT_SHORT_BURST_MAX_WORDS,
187
+ DEFAULT_SHORT_BURST_MIN_OCCURRENCES,
188
+ DEFAULT_SHORT_BURST_MIN_TOTAL_WORDS,
189
+ DEFAULT_SHORT_BURST_MIN_WORDS,
190
+ DEFAULT_SHORT_BURST_SCAN_WORDS,
83
191
  DEFAULT_WINDOW_WORDS,
84
192
  createLiveResponseRepetitionGuard,
85
193
  normalizeWords,
194
+ repeatedConsecutiveWindow,
86
195
  repeatedWindow,
87
196
  };
@@ -0,0 +1,107 @@
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 SCHEMA_VERSION = 1;
8
+ const MAX_LESSONS = 5;
9
+ const MAX_HEADING_CHARS = 160;
10
+ const MAX_SEMANTIC_MATCHES = 6;
11
+ const CANDIDATE_ID = /^vl-[a-f0-9]{32}$/u;
12
+
13
+ function cleanText(value, maxChars) {
14
+ const text = String(value ?? '')
15
+ .replace(/[\u0000-\u001f\u007f]+/gu, ' ')
16
+ .replace(/\s+/gu, ' ')
17
+ .trim();
18
+ return text.slice(0, maxChars);
19
+ }
20
+
21
+ function lessonKey(reference, heading) {
22
+ return crypto.createHash('sha256')
23
+ .update(`${String(reference ?? '')}\0${heading}`)
24
+ .digest('hex')
25
+ .slice(0, 24);
26
+ }
27
+
28
+ function normalizeLessons(value) {
29
+ if (!Array.isArray(value)) return [];
30
+ const lessons = [];
31
+ const seen = new Set();
32
+ for (const item of value) {
33
+ const heading = cleanText(item?.heading, MAX_HEADING_CHARS);
34
+ if (!heading) continue;
35
+ const key = lessonKey(item?.reference, heading);
36
+ if (seen.has(key)) continue;
37
+ seen.add(key);
38
+ const semanticMatches = [...new Set(
39
+ (Array.isArray(item?.semanticMatches) ? item.semanticMatches : [])
40
+ .map((tag) => cleanText(tag, 48))
41
+ .filter(Boolean),
42
+ )].sort().slice(0, MAX_SEMANTIC_MATCHES);
43
+ lessons.push({
44
+ lessonKey: key,
45
+ heading,
46
+ ...(semanticMatches.length > 0 ? { semanticMatches } : {}),
47
+ });
48
+ if (lessons.length >= MAX_LESSONS) break;
49
+ }
50
+ return lessons;
51
+ }
52
+
53
+ function atomicCreate(filePath, value) {
54
+ const temporary = `${filePath}.${process.pid}.${crypto.randomBytes(8).toString('hex')}.tmp`;
55
+ fs.writeFileSync(temporary, `${JSON.stringify(value)}\n`, { encoding: 'utf8', mode: 0o600 });
56
+ try {
57
+ fs.linkSync(temporary, filePath);
58
+ return true;
59
+ } catch (error) {
60
+ if (error?.code !== 'EEXIST') throw error;
61
+ return false;
62
+ } finally {
63
+ fs.rmSync(temporary, { force: true });
64
+ }
65
+ }
66
+
67
+ function readTrace(filePath) {
68
+ try {
69
+ return JSON.parse(fs.readFileSync(filePath, 'utf8'));
70
+ } catch {
71
+ return null;
72
+ }
73
+ }
74
+
75
+ function recordValidatedLearningOutcomeTrace(candidate, selectedLessons, options = {}) {
76
+ if (candidate?.source !== 'current_turn'
77
+ || candidate.status !== 'pending'
78
+ || !CANDIDATE_ID.test(String(candidate.id ?? ''))
79
+ || typeof candidate.filePath !== 'string'
80
+ || !candidate.filePath) return null;
81
+ const lessons = normalizeLessons(selectedLessons);
82
+ if (lessons.length === 0) return null;
83
+
84
+ const directory = path.dirname(candidate.filePath);
85
+ const filePath = path.join(directory, `${candidate.id}.lesson-exposure.jsonl`);
86
+ const trace = {
87
+ schemaVersion: SCHEMA_VERSION,
88
+ type: 'lesson_exposure_red_green',
89
+ status: 'observed',
90
+ candidateId: candidate.id,
91
+ outcome: 'validated_red_green',
92
+ causalAttribution: false,
93
+ lessons,
94
+ recordedAt: new Date((options.now || Date.now)()).toISOString(),
95
+ };
96
+ const created = atomicCreate(filePath, trace);
97
+ return {
98
+ created,
99
+ filePath,
100
+ trace: created ? trace : readTrace(filePath),
101
+ };
102
+ }
103
+
104
+ module.exports = {
105
+ normalizeLessons,
106
+ recordValidatedLearningOutcomeTrace,
107
+ };
package/blun.mjs CHANGED
@@ -231592,6 +231592,7 @@ var init_mistake_md = __esmMin((() => {
231592
231592
  reserveChecked = false;
231593
231593
  mistakeDir;
231594
231594
  mistakeFile;
231595
+ selectedLessons = [];
231595
231596
  constructor(agent) {
231596
231597
  super(agent);
231597
231598
  this.mistakeDir = resolveMistakeDir(agent.blunHomeDir);
@@ -231600,6 +231601,10 @@ var init_mistake_md = __esmMin((() => {
231600
231601
  onContextClear() {
231601
231602
  super.onContextClear();
231602
231603
  this.reserveChecked = false;
231604
+ this.selectedLessons = [];
231605
+ }
231606
+ getSelectedLessons() {
231607
+ return this.selectedLessons.map((lesson) => ({ ...lesson }));
231603
231608
  }
231604
231609
  async inject() {
231605
231610
  const injection = await this.getInjection();
@@ -231619,6 +231624,7 @@ var init_mistake_md = __esmMin((() => {
231619
231624
  });
231620
231625
  }
231621
231626
  async getInjection() {
231627
+ this.selectedLessons = [];
231622
231628
  try {
231623
231629
  if (isLowInformationMistakeTurn(this.agent.context.history)) return void 0;
231624
231630
  const sources = await readProjectMistakeSources(this.agent);
@@ -231638,6 +231644,7 @@ var init_mistake_md = __esmMin((() => {
231638
231644
  const query = mistakeQueryTextForTurn(this.agent.context.history, this.agent.goal.getGoal().goal);
231639
231645
  const selected = selectRelevantMistakeSources(sources, query);
231640
231646
  if (!selected.text) return void 0;
231647
+ this.selectedLessons = selected.selected;
231641
231648
  let prefix = "";
231642
231649
  if (!this.reserveChecked && sharedContent) {
231643
231650
  this.reserveChecked = true;
@@ -232243,13 +232250,19 @@ function buildValidatedLearningSignalReminder(signal) {
232243
232250
  lines.push("</validated-learning-signal>");
232244
232251
  return lines.join("\n");
232245
232252
  }
232246
- var ValidatedLearningSignalInjector, shouldSyncValidatedLearningCandidate, syncValidatedLearningCandidate;
232253
+ var ValidatedLearningSignalInjector, recordValidatedLearningOutcomeTrace, shouldSyncValidatedLearningCandidate, syncValidatedLearningCandidate;
232247
232254
  var init_validated_learning_signal = __esmMin((() => {
232248
232255
  init_injector();
232249
232256
  ({ syncValidatedLearningCandidate } = createRequire(import.meta.url)("./bin/validated-learning-signal.cjs"));
232250
232257
  ({ shouldSyncValidatedLearningCandidate } = createRequire(import.meta.url)("./bin/validated-learning-performance-policy.cjs"));
232258
+ ({ recordValidatedLearningOutcomeTrace } = createRequire(import.meta.url)("./bin/validated-learning-outcome-trace.cjs"));
232251
232259
  ValidatedLearningSignalInjector = class extends DynamicInjector {
232252
232260
  injectionVariant = "validated_learning_signal";
232261
+ selectedLessons;
232262
+ constructor(agent, selectedLessons) {
232263
+ super(agent);
232264
+ this.selectedLessons = selectedLessons;
232265
+ }
232253
232266
  async inject() {
232254
232267
  const existing = this.agent.context.history.filter(isValidatedLearningSignalReminder);
232255
232268
  if (!shouldSyncValidatedLearningCandidate(this.agent.context.history)) return;
@@ -232262,6 +232275,9 @@ var init_validated_learning_signal = __esmMin((() => {
232262
232275
  if (existing.length > 0) this.agent.context.removeSystemRemindersMatching(isValidatedLearningSignalReminder);
232263
232276
  return;
232264
232277
  }
232278
+ if (signal.source === "current_turn") try {
232279
+ recordValidatedLearningOutcomeTrace(signal, this.selectedLessons?.() ?? []);
232280
+ } catch {}
232265
232281
  const injection = buildValidatedLearningSignalReminder(signal);
232266
232282
  const expected = `<system-reminder>\n${injection}\n</system-reminder>`;
232267
232283
  if (existing.length === 1 && reminderText(existing[0]) === expected) return;
@@ -233086,13 +233102,14 @@ var init_manager$2 = __esmMin((() => {
233086
233102
  missionContractInjector = null;
233087
233103
  constructor(agent) {
233088
233104
  this.agent = agent;
233105
+ const mistakeMdInjector = new MistakeMdInjector(agent);
233089
233106
  this.injectors = [
233090
233107
  new PluginSessionStartInjector(agent),
233091
233108
  new ErrorMemoryInjector(agent),
233092
233109
  new ToolAwarenessInjector(agent),
233093
- new MistakeMdInjector(agent),
233110
+ mistakeMdInjector,
233094
233111
  new RepeatedAssistantResponseInjector(agent),
233095
- new ValidatedLearningSignalInjector(agent),
233112
+ new ValidatedLearningSignalInjector(agent, () => mistakeMdInjector.getSelectedLessons()),
233096
233113
  new ActionStyleInjector(agent),
233097
233114
  new PlanModeInjector(agent),
233098
233115
  new PermissionModeInjector(agent)
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "blun-king-cli",
3
- "version": "9.1.444",
3
+ "version": "9.1.446",
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": {