niahere 0.2.44 → 0.2.45

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.
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "niahere",
3
- "version": "0.2.44",
3
+ "version": "0.2.45",
4
4
  "description": "A personal AI assistant daemon — scheduled jobs, chat across Telegram and Slack, persona system, and visual identity.",
5
5
  "type": "module",
6
6
  "scripts": {
@@ -10,6 +10,7 @@ import { getAgentDefinitions } from "../core/agents";
10
10
  import { Session, Message, ActiveEngine } from "../db/models";
11
11
  import type { Attachment, SendResult, StreamCallback, ActivityCallback, SendCallbacks, ChatEngine, EngineOptions } from "../types";
12
12
  import { truncate, formatToolUse } from "../utils/format-activity";
13
+ import { consolidateSession } from "../core/consolidator";
13
14
  import { log } from "../utils/log";
14
15
 
15
16
  const IDLE_TIMEOUT = 10 * 60 * 1000; // 10 minutes
@@ -133,6 +134,7 @@ export async function createChatEngine(opts: EngineOptions): Promise<ChatEngine>
133
134
  let longRunningTimer: ReturnType<typeof setTimeout> | null = null;
134
135
  let longRunningWarned = false;
135
136
  let alive = false;
137
+ let messageCount = 0;
136
138
 
137
139
  function clearIdleTimer() {
138
140
  if (idleTimer) {
@@ -143,12 +145,18 @@ export async function createChatEngine(opts: EngineOptions): Promise<ChatEngine>
143
145
 
144
146
  function resetIdleTimer() {
145
147
  clearIdleTimer();
146
- idleTimer = setTimeout(() => {
148
+ idleTimer = setTimeout(async () => {
147
149
  if (pending) {
148
150
  // Don't tear down while a request is in flight
149
151
  log.warn({ room }, "idle timer fired while request pending, skipping teardown");
150
152
  return;
151
153
  }
154
+ // Memory consolidation — "hippocampal replay" before sleep
155
+ if (sessionId && messageCount > 0) {
156
+ consolidateSession(sessionId, room).catch((err) => {
157
+ log.error({ err, room }, "consolidation failed during idle teardown");
158
+ });
159
+ }
152
160
  teardown();
153
161
  }, IDLE_TIMEOUT);
154
162
  }
@@ -239,6 +247,7 @@ export async function createChatEngine(opts: EngineOptions): Promise<ChatEngine>
239
247
  content: pending.userMessage,
240
248
  isFromAgent: false,
241
249
  });
250
+ messageCount++;
242
251
  }
243
252
  }
244
253
 
@@ -406,6 +415,12 @@ export async function createChatEngine(opts: EngineOptions): Promise<ChatEngine>
406
415
  },
407
416
 
408
417
  close() {
418
+ // Memory consolidation on explicit close (skip if mid-turn — transcript is incomplete)
419
+ if (sessionId && messageCount > 0 && !pending) {
420
+ consolidateSession(sessionId, room).catch((err) => {
421
+ log.error({ err, room }, "consolidation failed during close");
422
+ });
423
+ }
409
424
  teardown();
410
425
  ActiveEngine.unregister(room).catch(() => {});
411
426
  },
@@ -0,0 +1,138 @@
1
+ /**
2
+ * Memory consolidator — "hippocampal replay" for Nia.
3
+ *
4
+ * After a chat session goes idle or a job completes, this module reviews
5
+ * what happened and saves memories worth keeping.
6
+ *
7
+ * This decouples memory formation from task execution: during a conversation,
8
+ * the agent focuses on the task. Afterward, a background pass extracts what's
9
+ * worth remembering — just like the brain consolidates memories during sleep.
10
+ *
11
+ * The consolidator uses the same agent loop as cron jobs — full Nia system
12
+ * prompt, full tool access, same runner. It's just a specialized job.
13
+ *
14
+ * Research basis:
15
+ * - LangChain: "background" memory formation avoids latency + competing optimization pressures
16
+ * - Mem0: LLM-driven extraction with ADD/UPDATE/NOOP decisions against existing memories
17
+ * - Cognitive science: hippocampal replay consolidates experiences after the fact, not during
18
+ */
19
+
20
+ import { Message } from "../db/models";
21
+ import { buildSystemPrompt } from "../chat/identity";
22
+ import { runJobWithClaude } from "./runner";
23
+ import { log } from "../utils/log";
24
+ import { homedir } from "os";
25
+ import type { SessionMessage } from "../types";
26
+
27
+ /** Track sessions already consolidated to prevent double runs. */
28
+ const consolidated = new Set<string>();
29
+
30
+ /** Max messages to include in transcript (most recent). Keeps prompt size bounded. */
31
+ const MAX_TRANSCRIPT_MESSAGES = 50;
32
+
33
+ /** Rooms to skip (placeholder sessions). */
34
+ function shouldSkip(room: string): boolean {
35
+ return room.includes("placeholder");
36
+ }
37
+
38
+ /** Format conversation transcript for the extraction prompt. Cap to recent messages. */
39
+ function formatTranscript(messages: SessionMessage[]): string {
40
+ const recent = messages.slice(-MAX_TRANSCRIPT_MESSAGES);
41
+ const skipped = messages.length - recent.length;
42
+ const prefix = skipped > 0 ? `[...${skipped} earlier messages omitted]\n\n` : "";
43
+
44
+ return prefix + recent
45
+ .map((m) => `[${m.sender}] (${m.createdAt}): ${m.content.slice(0, 2000)}`)
46
+ .join("\n\n");
47
+ }
48
+
49
+ /** Build the extraction prompt from a conversation transcript. */
50
+ function buildConsolidationPrompt(transcript: string, source: string): string {
51
+ return `Job: memory-consolidation (triggered by ${source})
52
+
53
+ You just finished a session. It has gone idle.
54
+ Your task: review the transcript below and save anything worth keeping for future sessions.
55
+
56
+ ## Transcript
57
+ ${transcript}
58
+
59
+ ## Instructions
60
+ 1. First, read your existing memories (read_memory tool) and rules (read rules.md) to avoid duplicates
61
+ 2. Review the transcript for things worth persisting. Use the RIGHT tool for each:
62
+
63
+ **Use add_memory for FACTS** (nouns — things that are true):
64
+ - People: names, roles, orgs, relationships
65
+ - Decisions: what was decided or agreed on
66
+ - Technical facts: system details, API quirks, config gotchas
67
+ - Patterns: recurring issues, user behaviors, workflow tendencies
68
+ - Events: travel, deadlines, incidents, milestones with dates
69
+
70
+ **Use add_rule for INSTRUCTIONS** (verbs — how to behave):
71
+ - User corrected your tone, format, or approach
72
+ - User said "from now on" / "always" / "never" / "stop doing X"
73
+ - User expressed a preference about how you communicate or work
74
+
75
+ 3. Skip anything already in existing memories or rules (no duplicates)
76
+ 4. Skip small talk, greetings, conversational filler
77
+ 5. Skip transient state ("currently working on X")
78
+ 6. Quality over quantity — saving nothing is fine if the conversation was trivial
79
+ 7. If existing memories are outdated based on new info, note what should be updated
80
+
81
+ Do NOT message the user about this. Save silently and report a brief summary of what you saved.`;
82
+ }
83
+
84
+ /** Run the consolidation agent loop. */
85
+ async function runConsolidation(transcript: string, source: string): Promise<void> {
86
+ const systemPrompt = buildSystemPrompt("job");
87
+ const jobPrompt = buildConsolidationPrompt(transcript, source);
88
+ const output = await runJobWithClaude(systemPrompt, jobPrompt, homedir());
89
+
90
+ if (output.error) {
91
+ log.error({ source, error: output.error }, "consolidator: extraction failed");
92
+ } else {
93
+ log.info({ source, resultChars: output.agentText.length }, "consolidator: done");
94
+ }
95
+ }
96
+
97
+ /**
98
+ * Consolidate a chat session's conversation into memories.
99
+ * Called when a chat engine goes idle or is explicitly closed.
100
+ */
101
+ export async function consolidateSession(sessionId: string, room: string): Promise<void> {
102
+ if (shouldSkip(room)) return;
103
+ if (consolidated.has(sessionId)) return;
104
+ consolidated.add(sessionId);
105
+
106
+ try {
107
+ const messages = await Message.getBySession(sessionId);
108
+ if (messages.length < 2) return;
109
+
110
+ log.info({ sessionId, room, messageCount: messages.length }, "consolidator: extracting memories from chat");
111
+
112
+ const transcript = formatTranscript(messages);
113
+ await runConsolidation(transcript, `chat session idle — ${room}`);
114
+ } catch (err) {
115
+ log.error({ err, sessionId, room }, "consolidator: chat extraction failed");
116
+ }
117
+ }
118
+
119
+ /**
120
+ * Consolidate a job run's output into memories.
121
+ * Called after a job completes in the runner.
122
+ */
123
+ export async function consolidateJobRun(jobName: string, jobPrompt: string, result: string): Promise<void> {
124
+ // Skip if the job itself is the consolidator (prevent infinite loop)
125
+ if (jobName === "memory-consolidation") return;
126
+
127
+ const transcript = `[job-prompt]: ${jobPrompt}\n\n[job-result]: ${result}`;
128
+
129
+ // Skip trivial results
130
+ if (result.length < 50) return;
131
+
132
+ try {
133
+ log.info({ jobName, resultChars: result.length }, "consolidator: extracting memories from job");
134
+ await runConsolidation(transcript, `job run — ${jobName}`);
135
+ } catch (err) {
136
+ log.error({ err, jobName }, "consolidator: job extraction failed");
137
+ }
138
+ }
@@ -257,6 +257,13 @@ export async function runJob(job: JobInput, onActivity?: ActivityCallback): Prom
257
257
  };
258
258
  writeState(freshState);
259
259
 
260
+ // Memory consolidation — review what the job learned (fire-and-forget)
261
+ if (ok && result.result) {
262
+ import("./consolidator").then(({ consolidateJobRun }) => {
263
+ consolidateJobRun(job.name, jobPrompt, result.result).catch(() => {});
264
+ }).catch(() => {});
265
+ }
266
+
260
267
  return result;
261
268
  } catch (err) {
262
269
  const duration_ms = Math.round(performance.now() - startMs);