niahere 0.2.45 → 0.2.47

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.45",
3
+ "version": "0.2.47",
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": {
@@ -5,12 +5,13 @@ import { existsSync } from "fs";
5
5
  import { join } from "path";
6
6
  import { homedir } from "os";
7
7
  import { randomUUID } from "crypto";
8
- import { buildSystemPrompt } from "./identity";
8
+ import { buildSystemPrompt, getSessionContext } from "./identity";
9
9
  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
13
  import { consolidateSession } from "../core/consolidator";
14
+ import { summarizeSession } from "../core/summarizer";
14
15
  import { log } from "../utils/log";
15
16
 
16
17
  const IDLE_TIMEOUT = 10 * 60 * 1000; // 10 minutes
@@ -112,7 +113,14 @@ function sessionFileExists(sessionId: string, cwd: string): boolean {
112
113
 
113
114
  export async function createChatEngine(opts: EngineOptions): Promise<ChatEngine> {
114
115
  const { room, channel, resume, mcpServers } = opts;
115
- const systemPrompt = buildSystemPrompt("chat", channel);
116
+ let systemPrompt = buildSystemPrompt("chat", channel);
117
+
118
+ // Inject recent session summaries for continuity
119
+ const sessionContext = await getSessionContext(room);
120
+ if (sessionContext) {
121
+ systemPrompt += "\n\n" + sessionContext;
122
+ }
123
+
116
124
  const cwd = homedir();
117
125
 
118
126
  let sessionId: string | null = null;
@@ -151,11 +159,14 @@ export async function createChatEngine(opts: EngineOptions): Promise<ChatEngine>
151
159
  log.warn({ room }, "idle timer fired while request pending, skipping teardown");
152
160
  return;
153
161
  }
154
- // Memory consolidation "hippocampal replay" before sleep
162
+ // Memory consolidation + session summary before "sleep"
155
163
  if (sessionId && messageCount > 0) {
156
164
  consolidateSession(sessionId, room).catch((err) => {
157
165
  log.error({ err, room }, "consolidation failed during idle teardown");
158
166
  });
167
+ summarizeSession(sessionId, room).catch((err) => {
168
+ log.error({ err, room }, "session summary failed during idle teardown");
169
+ });
159
170
  }
160
171
  teardown();
161
172
  }, IDLE_TIMEOUT);
@@ -415,11 +426,14 @@ export async function createChatEngine(opts: EngineOptions): Promise<ChatEngine>
415
426
  },
416
427
 
417
428
  close() {
418
- // Memory consolidation on explicit close (skip if mid-turn — transcript is incomplete)
429
+ // Memory consolidation + session summary on explicit close
419
430
  if (sessionId && messageCount > 0 && !pending) {
420
431
  consolidateSession(sessionId, room).catch((err) => {
421
432
  log.error({ err, room }, "consolidation failed during close");
422
433
  });
434
+ summarizeSession(sessionId, room).catch((err) => {
435
+ log.error({ err, room }, "session summary failed during close");
436
+ });
423
437
  }
424
438
  teardown();
425
439
  ActiveEngine.unregister(room).catch(() => {});
@@ -4,6 +4,7 @@ import { getPaths } from "../utils/paths";
4
4
  import { getEnvironmentPrompt, getModePrompt, getChannelPrompt } from "../prompts";
5
5
  import { getSkillsSummary } from "../core/skills";
6
6
  import { getAgentsSummary } from "../core/agents";
7
+ import { Session } from "../db/models";
7
8
  import type { Mode } from "../types";
8
9
 
9
10
  // Re-export for backwards compat
@@ -43,3 +44,23 @@ export function buildSystemPrompt(mode: Mode = "chat", channel: string = "termin
43
44
 
44
45
  return parts.join("\n\n");
45
46
  }
47
+
48
+ /**
49
+ * Load recent session summaries for a room and format as a context block.
50
+ * Returns empty string if no summaries are available.
51
+ */
52
+ export async function getSessionContext(room: string): Promise<string> {
53
+ try {
54
+ const summaries = await Session.getRecentSummaries(room, 3);
55
+ if (summaries.length === 0) return "";
56
+
57
+ const lines = summaries
58
+ .reverse() // oldest first
59
+ .map((s) => `- (${s.updatedAt}): ${s.summary}`)
60
+ .join("\n");
61
+
62
+ return `## Recent Session Context\nBrief summaries of your last few sessions in this room — use for continuity:\n${lines}`;
63
+ } catch {
64
+ return "";
65
+ }
66
+ }
@@ -18,10 +18,8 @@
18
18
  */
19
19
 
20
20
  import { Message } from "../db/models";
21
- import { buildSystemPrompt } from "../chat/identity";
22
- import { runJobWithClaude } from "./runner";
21
+ import { runTask } from "./runner";
23
22
  import { log } from "../utils/log";
24
- import { homedir } from "os";
25
23
  import type { SessionMessage } from "../types";
26
24
 
27
25
  /** Track sessions already consolidated to prevent double runs. */
@@ -83,15 +81,10 @@ Do NOT message the user about this. Save silently and report a brief summary of
83
81
 
84
82
  /** Run the consolidation agent loop. */
85
83
  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
- }
84
+ await runTask({
85
+ name: "consolidator",
86
+ prompt: buildConsolidationPrompt(transcript, source),
87
+ });
95
88
  }
96
89
 
97
90
  /**
@@ -9,6 +9,9 @@ import { getConfig } from "../utils/config";
9
9
  import { buildSystemPrompt } from "../chat/identity";
10
10
  import { scanAgents } from "./agents";
11
11
  import { truncate, formatToolUse } from "../utils/format-activity";
12
+ import { getMcpServers } from "../mcp";
13
+ import { ActiveEngine } from "../db/models";
14
+ import { log } from "../utils/log";
12
15
 
13
16
  export type ActivityCallback = (line: string) => void;
14
17
 
@@ -87,14 +90,21 @@ export async function runJobWithClaude(
87
90
  };
88
91
  }
89
92
 
93
+ const options: Record<string, unknown> = {
94
+ systemPrompt,
95
+ cwd,
96
+ permissionMode: "bypassPermissions",
97
+ sessionId,
98
+ };
99
+
100
+ const mcpServers = getMcpServers();
101
+ if (mcpServers) {
102
+ options.mcpServers = mcpServers;
103
+ }
104
+
90
105
  const handle = query({
91
106
  prompt: singleMessage() as any,
92
- options: {
93
- systemPrompt,
94
- cwd,
95
- permissionMode: "bypassPermissions",
96
- sessionId,
97
- } as any,
107
+ options: options as any,
98
108
  });
99
109
 
100
110
  let agentText = "";
@@ -178,6 +188,40 @@ export async function runJobWithClaude(
178
188
  return { agentText, sessionId: actualSessionId };
179
189
  }
180
190
 
191
+ // ---------------------------------------------------------------------------
192
+ // Background task runner — tracked one-shot agent with full Nia personality
193
+ // ---------------------------------------------------------------------------
194
+
195
+ export interface TaskOptions {
196
+ /** Task name — used for ActiveEngine tracking as _system/{name}. */
197
+ name: string;
198
+ /** The prompt/instruction for the task. */
199
+ prompt: string;
200
+ /** System prompt override. Defaults to buildSystemPrompt("job"). */
201
+ systemPrompt?: string;
202
+ }
203
+
204
+ /**
205
+ * Run a background agent task with ActiveEngine tracking and MCP tools.
206
+ * Use for consolidator, summarizer, and any future background work.
207
+ */
208
+ export async function runTask(opts: TaskOptions): Promise<RunnerOutput> {
209
+ const room = `_system/${opts.name}`;
210
+ await ActiveEngine.register(room, "system").catch(() => {});
211
+ try {
212
+ const systemPrompt = opts.systemPrompt || buildSystemPrompt("job");
213
+ const output = await runJobWithClaude(systemPrompt, opts.prompt, homedir());
214
+ if (output.error) {
215
+ log.error({ task: opts.name, error: output.error }, "task failed");
216
+ } else {
217
+ log.info({ task: opts.name, resultChars: output.agentText.length }, "task completed");
218
+ }
219
+ return output;
220
+ } finally {
221
+ await ActiveEngine.unregister(room).catch(() => {});
222
+ }
223
+ }
224
+
181
225
  // ---------------------------------------------------------------------------
182
226
  // Public API
183
227
  // ---------------------------------------------------------------------------
@@ -0,0 +1,81 @@
1
+ /**
2
+ * Session summarizer — generates a brief handoff note when a session ends.
3
+ *
4
+ * Separate from memory consolidation. The consolidator extracts durable facts
5
+ * (memories/rules). The summarizer produces a short, ephemeral context bridge
6
+ * so the next session knows what just happened.
7
+ *
8
+ * Summaries are stored directly in the sessions table (summary column) via SQL.
9
+ * The last few summaries are injected into buildSystemPrompt() so new sessions
10
+ * have continuity without needing full transcript access.
11
+ */
12
+
13
+ import { Message, Session } from "../db/models";
14
+ import { runTask } from "./runner";
15
+ import { log } from "../utils/log";
16
+ import type { SessionMessage } from "../types";
17
+
18
+ /** Track sessions already summarized to prevent double runs. */
19
+ const summarized = new Set<string>();
20
+
21
+ /** Max messages to include (most recent). */
22
+ const MAX_MESSAGES = 30;
23
+
24
+ /** Format transcript for the summarization prompt. */
25
+ function formatTranscript(messages: SessionMessage[]): string {
26
+ const recent = messages.slice(-MAX_MESSAGES);
27
+ return recent
28
+ .map((m) => `[${m.sender}]: ${m.content.slice(0, 1000)}`)
29
+ .join("\n");
30
+ }
31
+
32
+ /**
33
+ * Summarize a session and store the result in the sessions table.
34
+ * Called when a chat engine goes idle — produces a context bridge for the next session.
35
+ */
36
+ export async function summarizeSession(sessionId: string, room: string): Promise<void> {
37
+ if (room.includes("placeholder")) return;
38
+ if (summarized.has(sessionId)) return;
39
+ summarized.add(sessionId);
40
+
41
+ try {
42
+ const messages = await Message.getBySession(sessionId);
43
+ if (messages.length < 2) return;
44
+
45
+ log.info({ sessionId, room, messageCount: messages.length }, "summarizer: generating session summary");
46
+
47
+ const transcript = formatTranscript(messages);
48
+
49
+ const prompt = `Job: session-summary (triggered by session idle in ${room})
50
+
51
+ Generate a brief session summary. This will be shown to your future self at the start of the next session for continuity.
52
+
53
+ ## Conversation
54
+ ${transcript}
55
+
56
+ ## Instructions
57
+ Write a 2-4 sentence summary covering:
58
+ - What was discussed or worked on
59
+ - Any decisions made or outcomes reached
60
+ - Anything pending or unresolved
61
+
62
+ Keep it concise — a handoff note, not a report. Output ONLY the summary text.`;
63
+
64
+ const output = await runTask({ name: "summarizer", prompt });
65
+
66
+ if (output.error) {
67
+ log.error({ sessionId, room, error: output.error }, "summarizer: failed");
68
+ return;
69
+ }
70
+
71
+ const summary = output.agentText.trim();
72
+ if (summary && summary.length > 10 && summary.length < 2000) {
73
+ await Session.setSummary(sessionId, summary);
74
+ log.info({ sessionId, room, summaryChars: summary.length }, "summarizer: saved");
75
+ } else {
76
+ log.warn({ sessionId, room, length: summary.length }, "summarizer: output too short or too long, skipped");
77
+ }
78
+ } catch (err) {
79
+ log.error({ err, sessionId, room }, "summarizer: failed");
80
+ }
81
+ }
@@ -0,0 +1,7 @@
1
+ import type postgres from "postgres";
2
+
3
+ export const name = "009_session_summary";
4
+
5
+ export async function up(sql: postgres.Sql): Promise<void> {
6
+ await sql`ALTER TABLE sessions ADD COLUMN IF NOT EXISTS summary TEXT`;
7
+ }
@@ -88,6 +88,31 @@ export async function touch(id: string): Promise<void> {
88
88
  await sql`UPDATE sessions SET updated_at = NOW() WHERE id = ${id}`;
89
89
  }
90
90
 
91
+ export async function setSummary(id: string, summary: string): Promise<void> {
92
+ const sql = getSql();
93
+ await sql`UPDATE sessions SET summary = ${summary} WHERE id = ${id}`;
94
+ }
95
+
96
+ export async function getRecentSummaries(room: string, limit = 3): Promise<Array<{ summary: string; updatedAt: string }>> {
97
+ const sql = getSql();
98
+ // Match summaries from sessions in the same channel (e.g. slack-dm-U...-*)
99
+ // by extracting the room prefix (everything before the last -N index)
100
+ const prefix = room.replace(/-\d+$/, "");
101
+ const rows = await sql`
102
+ SELECT summary, updated_at
103
+ FROM sessions
104
+ WHERE room LIKE ${prefix + "-%"}
105
+ AND summary IS NOT NULL
106
+ AND id != ${""}
107
+ ORDER BY updated_at DESC
108
+ LIMIT ${limit}
109
+ `;
110
+ return rows.map((r) => ({
111
+ summary: String(r.summary),
112
+ updatedAt: String(r.updated_at),
113
+ }));
114
+ }
115
+
91
116
  export async function getLatestRoomIndex(prefix: string): Promise<number> {
92
117
  const sql = getSql();
93
118
  const rows = await sql`