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 +1 -1
- package/src/chat/engine.ts +18 -4
- package/src/chat/identity.ts +21 -0
- package/src/core/consolidator.ts +5 -12
- package/src/core/runner.ts +50 -6
- package/src/core/summarizer.ts +81 -0
- package/src/db/migrations/009_session_summary.ts +7 -0
- package/src/db/models/session.ts +25 -0
package/package.json
CHANGED
package/src/chat/engine.ts
CHANGED
|
@@ -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
|
-
|
|
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
|
|
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
|
|
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(() => {});
|
package/src/chat/identity.ts
CHANGED
|
@@ -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
|
+
}
|
package/src/core/consolidator.ts
CHANGED
|
@@ -18,10 +18,8 @@
|
|
|
18
18
|
*/
|
|
19
19
|
|
|
20
20
|
import { Message } from "../db/models";
|
|
21
|
-
import {
|
|
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
|
-
|
|
87
|
-
|
|
88
|
-
|
|
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
|
/**
|
package/src/core/runner.ts
CHANGED
|
@@ -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
|
+
}
|
package/src/db/models/session.ts
CHANGED
|
@@ -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`
|