niahere 0.2.45 → 0.2.46

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.46",
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
+ }
@@ -0,0 +1,84 @@
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 { buildSystemPrompt } from "../chat/identity";
15
+ import { runJobWithClaude } from "./runner";
16
+ import { log } from "../utils/log";
17
+ import { homedir } from "os";
18
+ import type { SessionMessage } from "../types";
19
+
20
+ /** Track sessions already summarized to prevent double runs. */
21
+ const summarized = new Set<string>();
22
+
23
+ /** Max messages to include (most recent). */
24
+ const MAX_MESSAGES = 30;
25
+
26
+ /** Format transcript for the summarization prompt. */
27
+ function formatTranscript(messages: SessionMessage[]): string {
28
+ const recent = messages.slice(-MAX_MESSAGES);
29
+ return recent
30
+ .map((m) => `[${m.sender}]: ${m.content.slice(0, 1000)}`)
31
+ .join("\n");
32
+ }
33
+
34
+ /**
35
+ * Summarize a session and store the result in the sessions table.
36
+ * Called when a chat engine goes idle — produces a context bridge for the next session.
37
+ */
38
+ export async function summarizeSession(sessionId: string, room: string): Promise<void> {
39
+ if (room.includes("placeholder")) return;
40
+ if (summarized.has(sessionId)) return;
41
+ summarized.add(sessionId);
42
+
43
+ try {
44
+ const messages = await Message.getBySession(sessionId);
45
+ if (messages.length < 2) return;
46
+
47
+ log.info({ sessionId, room, messageCount: messages.length }, "summarizer: generating session summary");
48
+
49
+ const systemPrompt = buildSystemPrompt("job");
50
+ const transcript = formatTranscript(messages);
51
+
52
+ const jobPrompt = `Job: session-summary (triggered by session idle in ${room})
53
+
54
+ Generate a brief session summary. This will be shown to your future self at the start of the next session for continuity.
55
+
56
+ ## Conversation
57
+ ${transcript}
58
+
59
+ ## Instructions
60
+ Write a 2-4 sentence summary covering:
61
+ - What was discussed or worked on
62
+ - Any decisions made or outcomes reached
63
+ - Anything pending or unresolved
64
+
65
+ Keep it concise — a handoff note, not a report. Output ONLY the summary text.`;
66
+
67
+ const output = await runJobWithClaude(systemPrompt, jobPrompt, homedir());
68
+
69
+ if (output.error) {
70
+ log.error({ sessionId, room, error: output.error }, "summarizer: failed");
71
+ return;
72
+ }
73
+
74
+ const summary = output.agentText.trim();
75
+ if (summary && summary.length > 10 && summary.length < 2000) {
76
+ await Session.setSummary(sessionId, summary);
77
+ log.info({ sessionId, room, summaryChars: summary.length }, "summarizer: saved");
78
+ } else {
79
+ log.warn({ sessionId, room, length: summary.length }, "summarizer: output too short or too long, skipped");
80
+ }
81
+ } catch (err) {
82
+ log.error({ err, sessionId, room }, "summarizer: failed");
83
+ }
84
+ }
@@ -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`