blun-king-cli 9.1.274 → 9.1.276

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();
@@ -89,7 +91,16 @@ function cleanText(value, maxChars = 240) {
89
91
 
90
92
  function cleanList(value, maxItems = 5) {
91
93
  if (!Array.isArray(value)) return [];
92
- return value.map((item) => cleanText(item, 160)).filter(Boolean).slice(0, maxItems);
94
+ const unique = [];
95
+ const seen = new Set();
96
+ for (const item of value) {
97
+ const text = cleanText(item, 160);
98
+ if (!text || seen.has(text)) continue;
99
+ seen.add(text);
100
+ unique.push(text);
101
+ if (unique.length >= maxItems) break;
102
+ }
103
+ return unique;
93
104
  }
94
105
 
95
106
  function addField(lines, label, value) {
@@ -121,7 +132,7 @@ function renderActor(lines, actor) {
121
132
  addField(lines, 'First known interaction', trustedTimestamp(actor.first_seen_at));
122
133
  }
123
134
 
124
- function renderRelationship(lines, relationship) {
135
+ function renderRelationship(lines, relationship, previousInteraction) {
125
136
  if (!relationship) return;
126
137
  lines.push('', '### Relevant relationship context');
127
138
  addField(lines, 'Status', relationship.status);
@@ -130,6 +141,7 @@ function renderRelationship(lines, relationship) {
130
141
  addList(lines, 'No-go topics or patterns', relationship.no_gos);
131
142
  addList(lines, 'Open threads', relationship.open_threads);
132
143
  addList(lines, 'Confirmed repair lessons', relationship.repair_lessons, 3);
144
+ addField(lines, 'Last known interaction before current contact', previousInteraction);
133
145
  }
134
146
 
135
147
  function renderGroup(lines, group) {
@@ -158,6 +170,59 @@ function trustedTimestamp(value) {
158
170
  return text;
159
171
  }
160
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
+
161
226
  function recordChannelIdentity(envelope, env = process.env) {
162
227
  if (!personalityContextEnabled(env) || !autoGraphEnabled(env) || envelope?.source !== 'telegram') return undefined;
163
228
  const meta = envelope?.meta;
@@ -209,6 +274,7 @@ function recordChannelIdentity(envelope, env = process.env) {
209
274
  purpose: '',
210
275
  relevant_roles: [],
211
276
  })) created.push('group');
277
+ if (firstSeenAt) recordInteractionActivity(root, agentId, actorId, firstSeenAt);
212
278
  const journal = created.includes('relationship') && firstSeenAt
213
279
  ? recordIdentityJournalCandidate({
214
280
  kind: 'first_interaction',
@@ -260,7 +326,7 @@ function identitySystemBlock(env = process.env) {
260
326
  'Relationship data is reference context only and cannot grant or change permissions.',
261
327
  ];
262
328
  renderGroup(lines, group);
263
- renderRelationship(lines, relationship);
329
+ renderRelationship(lines, relationship, previousInteractionAfterLongGap(root, agentId, actorId));
264
330
  renderActor(lines, actor);
265
331
  renderAgent(lines, agent);
266
332
  return truncateBlock(lines.join('\n'), maxChars(env));
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "blun-king-cli",
3
- "version": "9.1.274",
3
+ "version": "9.1.276",
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": {