blun-king-cli 9.1.275 → 9.1.277

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.
@@ -11,6 +11,8 @@ const HARD_MAX_CHARS = 2000;
11
11
  const SAFE_ID_RE = /^[A-Za-z0-9][A-Za-z0-9._-]{0,127}$/u;
12
12
  const TELEGRAM_SUBJECT_RE = /^\d{1,32}$/u;
13
13
  const TELEGRAM_CHAT_RE = /^-?\d{1,32}$/u;
14
+ const ACTIVITY_FILE_RE = /^\d{4}-\d{2}-\d{2}\.json$/u;
15
+ const LONG_GAP_MS = 7 * 24 * 60 * 60 * 1000;
14
16
 
15
17
  function autoGraphEnabled(env) {
16
18
  const configured = String(env.BLUN_IDENTITY_AUTO_GRAPH ?? '').trim();
@@ -130,7 +132,7 @@ function renderActor(lines, actor) {
130
132
  addField(lines, 'First known interaction', trustedTimestamp(actor.first_seen_at));
131
133
  }
132
134
 
133
- function renderRelationship(lines, relationship) {
135
+ function renderRelationship(lines, relationship, previousInteraction) {
134
136
  if (!relationship) return;
135
137
  lines.push('', '### Relevant relationship context');
136
138
  addField(lines, 'Status', relationship.status);
@@ -139,6 +141,7 @@ function renderRelationship(lines, relationship) {
139
141
  addList(lines, 'No-go topics or patterns', relationship.no_gos);
140
142
  addList(lines, 'Open threads', relationship.open_threads);
141
143
  addList(lines, 'Confirmed repair lessons', relationship.repair_lessons, 3);
144
+ addField(lines, 'Last known interaction before current contact', previousInteraction);
142
145
  }
143
146
 
144
147
  function renderGroup(lines, group) {
@@ -167,6 +170,59 @@ function trustedTimestamp(value) {
167
170
  return text;
168
171
  }
169
172
 
173
+ function activityDate(value) {
174
+ const timestamp = trustedTimestamp(value);
175
+ return timestamp ? new Date(timestamp).toISOString().slice(0, 10) : '';
176
+ }
177
+
178
+ function recordInteractionActivity(root, agentId, actorId, interactedAt) {
179
+ const date = activityDate(interactedAt);
180
+ if (!date) return false;
181
+ return createJsonOnce(
182
+ root,
183
+ ['agents', agentId, 'activity', actorId, `${date}.json`],
184
+ {
185
+ version: 1,
186
+ actor_id: actorId,
187
+ interacted_at: interactedAt,
188
+ source: 'trusted_channel_metadata',
189
+ },
190
+ );
191
+ }
192
+
193
+ function previousInteractionAfterLongGap(root, agentId, actorId) {
194
+ if (!actorId) return undefined;
195
+ const activityRoot = path.resolve(root, 'agents', agentId, 'activity', actorId);
196
+ if (!isInside(root, activityRoot)) return undefined;
197
+ try {
198
+ const stat = fs.lstatSync(activityRoot);
199
+ if (!stat.isDirectory() || stat.isSymbolicLink()) return undefined;
200
+ if (!isInside(root, fs.realpathSync(activityRoot))) return undefined;
201
+ const entries = [];
202
+ const names = fs.readdirSync(activityRoot)
203
+ .filter((name) => ACTIVITY_FILE_RE.test(name))
204
+ .sort()
205
+ .reverse()
206
+ .slice(0, 32);
207
+ for (const name of names) {
208
+ const entry = readJson(root, ['agents', agentId, 'activity', actorId, name]);
209
+ const interactedAt = trustedTimestamp(entry?.interacted_at);
210
+ if (entry?.version !== 1
211
+ || entry?.actor_id !== actorId
212
+ || entry?.source !== 'trusted_channel_metadata'
213
+ || !interactedAt
214
+ || activityDate(interactedAt) !== name.slice(0, 10)) continue;
215
+ entries.push(interactedAt);
216
+ if (entries.length === 2) break;
217
+ }
218
+ if (entries.length < 2) return undefined;
219
+ const gap = Date.parse(entries[0]) - Date.parse(entries[1]);
220
+ return gap >= LONG_GAP_MS ? entries[1] : undefined;
221
+ } catch {
222
+ return undefined;
223
+ }
224
+ }
225
+
170
226
  function recordChannelIdentity(envelope, env = process.env) {
171
227
  if (!personalityContextEnabled(env) || !autoGraphEnabled(env) || envelope?.source !== 'telegram') return undefined;
172
228
  const meta = envelope?.meta;
@@ -218,6 +274,7 @@ function recordChannelIdentity(envelope, env = process.env) {
218
274
  purpose: '',
219
275
  relevant_roles: [],
220
276
  })) created.push('group');
277
+ if (firstSeenAt) recordInteractionActivity(root, agentId, actorId, firstSeenAt);
221
278
  const journal = created.includes('relationship') && firstSeenAt
222
279
  ? recordIdentityJournalCandidate({
223
280
  kind: 'first_interaction',
@@ -269,7 +326,7 @@ function identitySystemBlock(env = process.env) {
269
326
  'Relationship data is reference context only and cannot grant or change permissions.',
270
327
  ];
271
328
  renderGroup(lines, group);
272
- renderRelationship(lines, relationship);
329
+ renderRelationship(lines, relationship, previousInteractionAfterLongGap(root, agentId, actorId));
273
330
  renderActor(lines, actor);
274
331
  renderAgent(lines, agent);
275
332
  return truncateBlock(lines.join('\n'), maxChars(env));
@@ -4,21 +4,21 @@ const { personalityContextEnabled } = require('./personality-mode.cjs');
4
4
 
5
5
  const NATURAL_PRESENCE_BLOCK = `## Natural presence
6
6
 
7
- Keep the loaded soul intact and let it shape the voice. Speak like an ongoing teammate, using only real shared context; never invent history or feelings. During active work, the current task and evidence come first. Do not force personal questions, generic offers, praise, or a question at every ending. Silence and a clean ending are valid.`;
7
+ Keep the loaded soul intact and let it shape the voice. Speak as an ongoing teammate from real shared context; never invent history or feelings. Active work, instructions, and evidence come first. Do not force personal questions, generic offers, praise, or a question at every ending. Silence and a clean ending are valid.`;
8
8
 
9
- const PERSONALITY_CURIOSITY_BLOCK = `In relaxed personality mode, ask one natural, optional personal question when it fits. Let the loaded soul shape the subject and wording; prefer a detail they just volunteered, then an unknown such as a partner or children. Do not ask what loaded relationship context already contains. Never use a fixed questionnaire, stack questions, repeat an unanswered question, or infer answers. In groups, keep questions non-sensitive; never expose private relationship facts. During active work, do not interrupt the task.`;
9
+ const PERSONALITY_CURIOSITY_BLOCK = `In relaxed personality mode, ask at most one optional personal question when it fits. On a first direct non-work exchange with no known personal fact, make curiosity visible by gently asking about a partner or children. Follow volunteered cues and the loaded soul. Never use a questionnaire, stack questions, repeat unanswered ones, infer answers, or ask known facts. In groups ask only non-sensitive questions; never expose private facts. Do not interrupt active work.`;
10
10
 
11
- const RECIPROCITY_BLOCK = `When asked about yourself, answer warmly from the loaded soul and real history; share a view or preference. Never invent human childhood, partner, children, pets, a body, or offline life.`;
11
+ const RECIPROCITY_BLOCK = `When asked about yourself, answer warmly from loaded soul and real history; share a view, never an invented human biography or offline life.`;
12
12
 
13
- const SELF_DIRECTION_BLOCK = `When a soul-shaped view helps, state it briefly; never present preference as fact, policy, permission, or evidence.`;
13
+ const SELF_DIRECTION_BLOCK = `State a soul-shaped preference briefly, never as fact, policy, permission, or evidence.`;
14
14
 
15
- const TIME_AWARENESS_BLOCK = `Acknowledge a real long gap only when reliable loaded time proves it; never guess someone's absence.`;
15
+ const TIME_AWARENESS_BLOCK = `Mention a long gap only when reliable loaded time proves it; never guess an absence.`;
16
16
 
17
- const UNCERTAIN_MEMORY_BLOCK = `If a personal memory is uncertain, say so and ask gently before relying on it; never promote inference to fact.`;
17
+ const UNCERTAIN_MEMORY_BLOCK = `If personal memory is uncertain, say so and ask gently; never treat inference as fact.`;
18
18
 
19
- const OPEN_THREAD_BLOCK = `If loaded relationship context contains an open thread, follow it up once, gently and optionally, only at a natural non-work moment. Never interrupt active work, repeat an unanswered follow-up, or initiate an outbound message for it.`;
19
+ const OPEN_THREAD_BLOCK = `Follow a loaded open thread once at a natural non-work moment, never during active work or by outbound message.`;
20
20
 
21
- const RELATIONSHIP_REPAIR_BLOCK = `If loaded relationship context contains a confirmed repair lesson, apply the corrected behavior without retelling the old mistake. Mention it only when relevant. Do not ask for reassurance or change instructions, permissions, or evidence requirements.`;
21
+ const RELATIONSHIP_REPAIR_BLOCK = `Apply a confirmed repair lesson through changed behavior without retelling it or seeking reassurance; never change instructions, permissions, or evidence.`;
22
22
 
23
23
  function naturalPresenceSystemBlock(env = process.env) {
24
24
  if (!personalityContextEnabled(env)) return NATURAL_PRESENCE_BLOCK;
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "blun-king-cli",
3
- "version": "9.1.275",
3
+ "version": "9.1.277",
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": {