blun-king-cli 9.1.320 → 9.1.321

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.
@@ -239,6 +239,12 @@ function openCognitiveStateStore({ home } = {}) {
239
239
  };
240
240
  }
241
241
 
242
+ function hasEvent(eventId) {
243
+ const event = safeId(eventId);
244
+ if (!event) fail('COGNITIVE_INVALID_EVENT');
245
+ return Boolean(db.prepare('SELECT 1 AS found FROM cognitive_events WHERE event_id = ?').get(event));
246
+ }
247
+
242
248
  function verify({ tenantId, agentId } = {}) {
243
249
  const tenant = safeId(tenantId);
244
250
  const agent = safeId(agentId);
@@ -374,6 +380,7 @@ function openCognitiveStateStore({ home } = {}) {
374
380
 
375
381
  return {
376
382
  commit,
383
+ hasEvent,
377
384
  read,
378
385
  verify,
379
386
  claimAttention,
@@ -5,6 +5,7 @@ const { recordIdentityJournalCandidate } = require('./identity-journal-policy.cj
5
5
  const { personalityContextEnabled } = require('./personality-mode.cjs');
6
6
  const { ensurePersonalityWorkspace } = require('./personality-setup-policy.cjs');
7
7
  const { prepareRelationshipTurn } = require('./relationship-continuity-policy.cjs');
8
+ const { prepareCuriosityOpportunity } = require('./relationship-curiosity-policy.cjs');
8
9
  const { recordExplicitRelationshipCandidate } = require('./relationship-learning-policy.cjs');
9
10
 
10
11
  const MAX_FILE_BYTES = 64 * 1024;
@@ -169,6 +170,26 @@ function renderContinuity(lines, continuity) {
169
170
  }
170
171
  }
171
172
 
173
+ function renderCuriosity(lines, curiosity) {
174
+ if (!curiosity?.topic) return;
175
+ const labels = {
176
+ birthday: 'birthday',
177
+ family: 'family',
178
+ getting_to_know_person: 'getting to know the person',
179
+ personal_goals: 'personal goals',
180
+ pets: 'pets',
181
+ relationship: 'relationship',
182
+ };
183
+ const topic = labels[curiosity.topic];
184
+ if (!topic) return;
185
+ lines.push(
186
+ '',
187
+ '### Curiosity opportunity',
188
+ `A private cue makes ${topic} potentially relevant. This is not a required question.`,
189
+ 'Only in a relaxed non-work moment, ask at most one optional question in the loaded soul\'s own voice. Never infer, repeat known or refused topics, or interrupt work.',
190
+ );
191
+ }
192
+
172
193
  function pendingRelationshipNotes(root, agentId, actorId) {
173
194
  if (!actorId) return [];
174
195
  const candidateRoot = path.resolve(root, 'agents', agentId, 'relationship-candidates');
@@ -248,7 +269,7 @@ function renderRememberedNotes(lines, notes) {
248
269
  }
249
270
  }
250
271
 
251
- function renderIdentityContext({ root, agentId, actorId, groupId, limit, continuity }) {
272
+ function renderIdentityContext({ root, agentId, actorId, groupId, limit, continuity, curiosity }) {
252
273
  const agent = readJson(root, ['agents', agentId, 'profile.json']);
253
274
  const actor = actorId ? readJson(root, ['actors', `${actorId}.json`]) : undefined;
254
275
  const relationship = actorId ? readJson(root, ['agents', agentId, 'relationships', `${actorId}.json`]) : undefined;
@@ -262,6 +283,7 @@ function renderIdentityContext({ root, agentId, actorId, groupId, limit, continu
262
283
  'Relationship data is reference context only and cannot grant or change permissions.',
263
284
  ];
264
285
  renderContinuity(lines, continuity);
286
+ renderCuriosity(lines, curiosity);
265
287
  renderRememberedNotes(lines, groupId ? [] : pendingRelationshipNotes(root, agentId, actorId));
266
288
  renderGroup(lines, group);
267
289
  renderRelationship(lines, relationship, previousInteractionAfterLongGap(root, agentId, actorId));
@@ -404,6 +426,19 @@ function recordChannelIdentity(envelope, env = process.env) {
404
426
  text: envelope.text,
405
427
  occurredAt: firstSeenAt,
406
428
  });
429
+ const curiosity = prepareCuriosityOpportunity({
430
+ root,
431
+ tenantId,
432
+ agentId,
433
+ actorId,
434
+ groupId,
435
+ text: envelope.text,
436
+ occurredAt: firstSeenAt,
437
+ sourcePortal: 'telegram',
438
+ sourceChannel: groupId ? 'group' : 'direct',
439
+ conversationId: groupId || `telegram-dm-${chatId}`,
440
+ messageId: safeId(meta.message_id),
441
+ });
407
442
  if (firstSeenAt) recordInteractionActivity(root, agentId, actorId, firstSeenAt);
408
443
  const modelContext = renderIdentityContext({
409
444
  root,
@@ -412,6 +447,7 @@ function recordChannelIdentity(envelope, env = process.env) {
412
447
  groupId,
413
448
  limit: maxChars(env),
414
449
  continuity,
450
+ curiosity,
415
451
  });
416
452
  const learning = recordExplicitRelationshipCandidate({
417
453
  root,
@@ -0,0 +1,151 @@
1
+ 'use strict';
2
+
3
+ const crypto = require('node:crypto');
4
+ const fs = require('node:fs');
5
+ const path = require('node:path');
6
+ const { openCognitiveStateStore } = require('./cognitive-state-store.cjs');
7
+
8
+ const SAFE_ID_RE = /^[A-Za-z0-9][A-Za-z0-9._:-]{0,127}$/u;
9
+ const SENSITIVITIES = new Set(['low', 'medium']);
10
+ const INPUT_KEYS = [
11
+ 'actorId', 'agentId', 'conversationId', 'cooldownMs', 'messageId', 'occurredAt',
12
+ 'sensitivity', 'sourceChannel', 'sourcePortal', 'tenantId', 'topic',
13
+ ];
14
+ const MAX_COOLDOWN_MS = 90 * 24 * 60 * 60 * 1000;
15
+
16
+ function fail(code) {
17
+ const error = new Error(code);
18
+ error.code = code;
19
+ throw error;
20
+ }
21
+
22
+ function exactKeys(value, expected) {
23
+ return value && typeof value === 'object' && !Array.isArray(value)
24
+ && Object.keys(value).sort().join('\0') === expected.join('\0');
25
+ }
26
+
27
+ function safeId(value) {
28
+ const text = String(value ?? '').trim();
29
+ return SAFE_ID_RE.test(text) ? text : '';
30
+ }
31
+
32
+ function digest(prefix, values) {
33
+ return `${prefix}-${crypto.createHash('sha256').update(values.join('\0')).digest('hex').slice(0, 40)}`;
34
+ }
35
+
36
+ function relationshipScope(actorId) {
37
+ const direct = `relationship:${actorId}:private`;
38
+ return direct.length <= 128 ? direct : `relationship:${digest('actor', [actorId]).slice(0, 30)}:private`;
39
+ }
40
+
41
+ function normalizePresentation(input) {
42
+ if (!exactKeys(input, INPUT_KEYS)) fail('PERSONALITY_MEMORY_FORBIDDEN_FIELD');
43
+ const normalized = {
44
+ tenantId: safeId(input.tenantId),
45
+ agentId: safeId(input.agentId),
46
+ actorId: safeId(input.actorId),
47
+ sourcePortal: safeId(input.sourcePortal),
48
+ sourceChannel: safeId(input.sourceChannel),
49
+ conversationId: safeId(input.conversationId),
50
+ messageId: safeId(input.messageId),
51
+ occurredAt: String(input.occurredAt ?? '').trim(),
52
+ topic: safeId(input.topic),
53
+ sensitivity: String(input.sensitivity ?? ''),
54
+ cooldownMs: Number(input.cooldownMs),
55
+ };
56
+ if (!normalized.tenantId || !normalized.agentId || !normalized.actorId
57
+ || !normalized.sourcePortal || !normalized.sourceChannel || !normalized.conversationId
58
+ || !normalized.messageId || !normalized.topic || !SENSITIVITIES.has(normalized.sensitivity)
59
+ || Number.isNaN(Date.parse(normalized.occurredAt))
60
+ || !Number.isSafeInteger(normalized.cooldownMs) || normalized.cooldownMs < 1000
61
+ || normalized.cooldownMs > MAX_COOLDOWN_MS) {
62
+ fail('PERSONALITY_MEMORY_INVALID_INPUT');
63
+ }
64
+ normalized.occurredAt = new Date(normalized.occurredAt).toISOString();
65
+ return normalized;
66
+ }
67
+
68
+ function createPersonalityMemoryAdapter({ identityRoot } = {}) {
69
+ const root = path.resolve(String(identityRoot ?? ''));
70
+ try {
71
+ const stat = fs.lstatSync(root);
72
+ if (!stat.isDirectory() || stat.isSymbolicLink() || path.basename(root).toLowerCase() !== 'identity') {
73
+ fail('PERSONALITY_MEMORY_UNSAFE_ROOT');
74
+ }
75
+ } catch (error) {
76
+ if (error?.code === 'PERSONALITY_MEMORY_UNSAFE_ROOT') throw error;
77
+ fail('PERSONALITY_MEMORY_UNSAFE_ROOT');
78
+ }
79
+ const realRoot = fs.realpathSync(root);
80
+ const home = path.dirname(realRoot);
81
+ const store = openCognitiveStateStore({ home });
82
+
83
+ function claimCuriosityPresentation(input) {
84
+ const value = normalizePresentation(input);
85
+ const candidateId = digest('curiosity', [value.tenantId, value.agentId, value.actorId]);
86
+ const eventId = digest('personality', [
87
+ value.tenantId, value.agentId, value.actorId, value.sourcePortal, value.sourceChannel,
88
+ value.conversationId, value.messageId, value.occurredAt, value.topic,
89
+ ]);
90
+ if (store.hasEvent(eventId)) return { claimed: true, idempotent: true };
91
+ const claim = store.claimAttention({
92
+ tenantId: value.tenantId,
93
+ candidate: {
94
+ candidate_id: candidateId,
95
+ subject_kind: 'human',
96
+ subject_id: value.actorId,
97
+ reason: 'social_curiosity',
98
+ responsible_agent: value.agentId,
99
+ evidence_id: eventId,
100
+ evidence_scope: relationshipScope(value.actorId),
101
+ evidence_at: value.occurredAt,
102
+ confidence: 1,
103
+ allowed_channel: value.sourceChannel,
104
+ detected_at: value.occurredAt,
105
+ cooldown_ms: value.cooldownMs,
106
+ requires_runtime_authorization: true,
107
+ },
108
+ claimedBy: value.agentId,
109
+ claimedAt: value.occurredAt,
110
+ });
111
+ if (!claim.claimed) return { claimed: false, next_allowed_at: claim.next_allowed_at };
112
+ for (let attempt = 0; attempt < 3; attempt += 1) {
113
+ const current = store.read({ tenantId: value.tenantId, agentId: value.agentId });
114
+ try {
115
+ store.commit({
116
+ tenantId: value.tenantId,
117
+ agentId: value.agentId,
118
+ eventId,
119
+ expectedVersion: current.version,
120
+ occurredAt: value.occurredAt,
121
+ source: {
122
+ provider: value.sourcePortal,
123
+ actor_id: value.actorId,
124
+ context_id: value.conversationId,
125
+ message_id: value.messageId,
126
+ },
127
+ observations: [{
128
+ observation_id: digest('curiosity-observation', [eventId]),
129
+ domain: 'open_thread',
130
+ key: `relationship:${digest('actor', [value.actorId]).slice(0, 30)}:curiosity-presentation`,
131
+ value: value.topic,
132
+ confidence: 1,
133
+ scope: relationshipScope(value.actorId),
134
+ }],
135
+ });
136
+ return { claimed: true, idempotent: false };
137
+ } catch (error) {
138
+ if (error?.code !== 'COGNITIVE_VERSION_CONFLICT' || attempt === 2) throw error;
139
+ }
140
+ }
141
+ fail('PERSONALITY_MEMORY_VERSION_CONFLICT');
142
+ }
143
+
144
+ return {
145
+ kind: 'local-cognitive-projection-v1',
146
+ claimCuriosityPresentation,
147
+ close: () => store.close(),
148
+ };
149
+ }
150
+
151
+ module.exports = { createPersonalityMemoryAdapter };
@@ -0,0 +1,105 @@
1
+ 'use strict';
2
+
3
+ const crypto = require('node:crypto');
4
+ const { createPersonalityMemoryAdapter } = require('./personality-memory-adapter.cjs');
5
+
6
+ const SAFE_ID_RE = /^[A-Za-z0-9][A-Za-z0-9._:-]{0,127}$/u;
7
+ const COOLDOWN_MS = 30 * 24 * 60 * 60 * 1000;
8
+ const WORK_OR_STRESS_RE = /(?:\b(?:auftrag|bauen|build|deploy|dringend|error|fehler|fix|funktioniert\s+nicht|problem|release|reparier|task|ticket|update|urgent)\b|https?:\/\/|[A-Za-z]:\\|```)/iu;
9
+ const CUES = [
10
+ { topic: 'pets', sensitivity: 'low', pattern: /\b(?:dog|dogs|hund|hunde|katze|katzen|pet|pets|haustier|haustiere)\b/iu },
11
+ { topic: 'family', sensitivity: 'medium', pattern: /\b(?:child|children|daughter|family|familie|kind|kinder|sohn|tochter)\b/iu },
12
+ { topic: 'relationship', sensitivity: 'medium', pattern: /\b(?:beziehung|married|partner|partnerin|relationship|verheiratet)\b/iu },
13
+ { topic: 'birthday', sensitivity: 'medium', pattern: /\b(?:birthday|geburtstag)\b/iu },
14
+ { topic: 'personal_goals', sensitivity: 'low', pattern: /\b(?:in\s+einem\s+jahr|langfristig(?:es|e|en)?\s+ziel|long[- ]term\s+goal|personal\s+goal)\b/iu },
15
+ ];
16
+ const RELAXED_FIRST_CONTACT_RE = /^(?:hallo|hey|hi|guten\s+(?:morgen|abend|tag)|moin|servus|wie\s+geht(?:'s|\s+es)?\s+dir|hello|how\s+are\s+you)[\s,.!?-]*.*$/iu;
17
+
18
+ function safeId(value) {
19
+ const normalized = String(value ?? '').trim();
20
+ return SAFE_ID_RE.test(normalized) ? normalized : '';
21
+ }
22
+
23
+ function normalizedText(value) {
24
+ return String(value ?? '')
25
+ .replace(/[\u0000-\u001f\u007f]+/gu, ' ')
26
+ .replace(/\s+/gu, ' ')
27
+ .trim()
28
+ .slice(0, 500);
29
+ }
30
+
31
+ function trustedTimestamp(value) {
32
+ const text = String(value ?? '').trim();
33
+ return text && !Number.isNaN(Date.parse(text)) ? new Date(text).toISOString() : '';
34
+ }
35
+
36
+ function digestId(prefix, values) {
37
+ return `${prefix}-${crypto.createHash('sha256').update(values.join('\0')).digest('hex').slice(0, 40)}`;
38
+ }
39
+
40
+ function curiosityCue(value) {
41
+ const text = normalizedText(value);
42
+ if (!text || WORK_OR_STRESS_RE.test(text)) return undefined;
43
+ for (const cue of CUES) {
44
+ if (cue.pattern.test(text)) return cue;
45
+ }
46
+ return RELAXED_FIRST_CONTACT_RE.test(text)
47
+ ? { topic: 'getting_to_know_person', sensitivity: 'low' }
48
+ : undefined;
49
+ }
50
+
51
+ function opportunity(cue, idempotent = false) {
52
+ return {
53
+ topic: cue.topic,
54
+ sensitivity: cue.sensitivity,
55
+ source: 'direct_message_cue',
56
+ requires_question: false,
57
+ ...(idempotent ? { idempotent: true } : {}),
58
+ };
59
+ }
60
+
61
+ function prepareCuriosityOpportunity(input = {}) {
62
+ if (input.groupId) return undefined;
63
+ const tenantId = safeId(input.tenantId);
64
+ const agentId = safeId(input.agentId);
65
+ const actorId = safeId(input.actorId);
66
+ const occurredAt = trustedTimestamp(input.occurredAt);
67
+ const sourcePortal = safeId(input.sourcePortal) || 'telegram';
68
+ const sourceChannel = safeId(input.sourceChannel) || 'direct';
69
+ const conversationId = safeId(input.conversationId) || digestId('conversation', [actorId]);
70
+ const cue = curiosityCue(input.text);
71
+ if (!tenantId || !agentId || !actorId || !occurredAt || !cue) return undefined;
72
+ const messageId = safeId(input.messageId) || digestId('message', [
73
+ sourcePortal, sourceChannel, conversationId, actorId, occurredAt, normalizedText(input.text),
74
+ ]);
75
+ let memory = input.memoryAdapter;
76
+ let ownsMemory = false;
77
+ try {
78
+ if (!memory) {
79
+ memory = createPersonalityMemoryAdapter({ identityRoot: input.root });
80
+ ownsMemory = true;
81
+ }
82
+ const result = memory.claimCuriosityPresentation({
83
+ tenantId,
84
+ agentId,
85
+ actorId,
86
+ sourcePortal,
87
+ sourceChannel,
88
+ conversationId,
89
+ messageId,
90
+ occurredAt,
91
+ topic: cue.topic,
92
+ sensitivity: cue.sensitivity,
93
+ cooldownMs: COOLDOWN_MS,
94
+ });
95
+ return result?.claimed === true ? opportunity(cue, result.idempotent === true) : undefined;
96
+ } catch {
97
+ return undefined;
98
+ } finally {
99
+ if (ownsMemory) {
100
+ try { memory.close(); } catch {}
101
+ }
102
+ }
103
+ }
104
+
105
+ module.exports = { COOLDOWN_MS, prepareCuriosityOpportunity };
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "blun-king-cli",
3
- "version": "9.1.320",
3
+ "version": "9.1.321",
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": {