niahere 0.2.83 → 0.2.85

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/README.md CHANGED
@@ -43,8 +43,8 @@ nia start # starts daemon + registers OS service
43
43
  - **Skills** — loads skills from multiple directories, invokable as slash commands
44
44
  - **Cross-platform service** — launchd (macOS), systemd (Linux), service-aware restart
45
45
  - **MCP tools** — 20 tools for job management, messaging, memory, rules, and channel control
46
- - **Background memory consolidation** — extracts memories from conversations and job runs automatically
47
- - **Session summaries** — handoff notes between sessions for continuity
46
+ - **Background memory consolidation** — stages memory candidates from conversations automatically
47
+ - **Session summaries** — optional handoff notes between sessions for continuity
48
48
  - **Backups** — `nia backup` with auto-backup before updates
49
49
  - **Optional integrations** — add Gmail, Discord, and more via skills
50
50
 
@@ -55,6 +55,8 @@ nia init — interactive setup (db, channels, persona, age
55
55
  nia start / stop — daemon + OS service (launchd/systemd)
56
56
  nia restart — restart daemon (service-aware)
57
57
  nia status — show daemon, jobs, channels, chat rooms
58
+ nia active [--full] — show active engine count or details
59
+ nia model [name] — show or set global Claude model
58
60
  nia health — check daemon, db, channels, config
59
61
  nia chat [-c|-r] [--channel ch] — terminal chat (new by default, -c continue, -r pick)
60
62
  nia run <prompt> — one-shot prompt execution
@@ -119,6 +121,17 @@ All config and data lives in `~/.niahere/`:
119
121
  nia.pid, daemon.log, cron-state.json, cron-audit.jsonl
120
122
  ```
121
123
 
124
+ Post-session background LLM work can be disabled in `config.yaml`:
125
+
126
+ ```yaml
127
+ session_finalization:
128
+ enabled: true
129
+ memory_consolidation: true
130
+ summaries: true
131
+ ```
132
+
133
+ Use `nia config set session_finalization.memory_consolidation false` to stop memory staging, `nia config set session_finalization.summaries false` to stop session summaries, or `nia config set session_finalization.enabled false` to disable both.
134
+
122
135
  ## Contributing
123
136
 
124
137
  **Don't add features. Add skills.**
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "niahere",
3
- "version": "0.2.83",
3
+ "version": "0.2.85",
4
4
  "description": "A personal AI assistant daemon — chat, scheduled jobs, persona system, extensible via skills.",
5
5
  "type": "module",
6
6
  "scripts": {
@@ -22,6 +22,7 @@ import type {
22
22
  import { truncate, formatToolUse } from "../utils/format-activity";
23
23
  import { finalizeSession, cancelPending } from "../core/finalizer";
24
24
  import { log } from "../utils/log";
25
+ import { getConfig } from "../utils/config";
25
26
  import { isRetryableApiError, sleep } from "../utils/retry";
26
27
  import { registerActiveHandle, unregisterActiveHandle } from "../core/active-handles";
27
28
  import { resolveJobPrompt } from "../core/job-prompt";
@@ -108,6 +109,11 @@ export function formatChatError(rawError: string | null | undefined): string {
108
109
  return `[error] ${error}`;
109
110
  }
110
111
 
112
+ export function resolveSdkModel(contextModel?: string | null): string | undefined {
113
+ const model = contextModel || getConfig().model;
114
+ return model && model !== "default" ? model : undefined;
115
+ }
116
+
111
117
  /**
112
118
  * Push-based async iterable for streaming user messages to the SDK.
113
119
  * Keeps the query subprocess alive between messages.
@@ -177,30 +183,40 @@ export async function createChatEngine(opts: EngineOptions): Promise<ChatEngine>
177
183
 
178
184
  // Context overrides: employee > agent > job > default
179
185
  let cwd = homedir();
186
+ let contextModel: string | null | undefined;
180
187
  if (opts.employee) {
181
188
  const empPrompt = buildEmployeePrompt(opts.employee);
182
189
  if (empPrompt) systemPrompt = empPrompt;
183
190
  const emp = getEmployee(opts.employee);
191
+ contextModel = emp?.model;
184
192
  if (emp?.repo && existsSync(emp.repo)) cwd = emp.repo;
185
193
  } else if (opts.agent) {
186
194
  const agents = scanAgents();
187
195
  const agentDef = agents.find((a) => a.name === opts.agent);
188
- if (agentDef) systemPrompt = agentDef.body + "\n\n" + buildContextSuffix("chat");
196
+ if (agentDef) {
197
+ systemPrompt = agentDef.body + "\n\n" + buildContextSuffix("chat");
198
+ contextModel = agentDef.model;
199
+ }
189
200
  } else if (opts.job) {
190
201
  // Job chat: load job and use its context
191
202
  const jobData = await Job.get(opts.job);
192
203
  if (jobData) {
204
+ contextModel = jobData.model;
193
205
  // If job has an employee, use employee prompt
194
206
  if (jobData.employee) {
195
207
  const empPrompt = buildEmployeePrompt(jobData.employee);
196
208
  if (empPrompt) systemPrompt = empPrompt;
197
209
  const emp = getEmployee(jobData.employee);
210
+ if (!contextModel) contextModel = emp?.model;
198
211
  if (emp?.repo && existsSync(emp.repo)) cwd = emp.repo;
199
212
  } else if (jobData.agent) {
200
213
  // If job has an agent, use agent prompt + context
201
214
  const agents = scanAgents();
202
215
  const agentDef = agents.find((a) => a.name === jobData.agent);
203
- if (agentDef) systemPrompt = agentDef.body + "\n\n" + buildContextSuffix("chat");
216
+ if (agentDef) {
217
+ systemPrompt = agentDef.body + "\n\n" + buildContextSuffix("chat");
218
+ if (!contextModel) contextModel = agentDef.model;
219
+ }
204
220
  }
205
221
  const resolvedPrompt = resolveJobPrompt(jobData);
206
222
  const source = resolvedPrompt.source === "file" ? ` from ${resolvedPrompt.filePath}` : "";
@@ -316,6 +332,10 @@ export async function createChatEngine(opts: EngineOptions): Promise<ChatEngine>
316
332
  settingSources: ["project", "user"],
317
333
  skills: [],
318
334
  };
335
+ const model = resolveSdkModel(contextModel);
336
+ if (model) {
337
+ options.model = model;
338
+ }
319
339
 
320
340
  if (sessionId) {
321
341
  options.resume = sessionId;
@@ -0,0 +1,36 @@
1
+ import { ActiveEngine } from "../db/models";
2
+ import { withDb } from "../db/with-db";
3
+ import { errMsg } from "../utils/errors";
4
+ import { dateSortValue, formatTimeLine } from "../utils/format";
5
+
6
+ function hasFullFlag(argv: string[]): boolean {
7
+ return argv.includes("--full");
8
+ }
9
+
10
+ export async function activeCommand(argv: string[] = []): Promise<void> {
11
+ const full = hasFullFlag(argv);
12
+ const now = new Date();
13
+ let engines: Awaited<ReturnType<typeof ActiveEngine.list>> = [];
14
+
15
+ try {
16
+ await withDb(async () => {
17
+ engines = await ActiveEngine.list();
18
+ });
19
+ } catch (err) {
20
+ console.error(`active engines unavailable: ${errMsg(err)}`);
21
+ process.exitCode = 1;
22
+ return;
23
+ }
24
+
25
+ if (!full) {
26
+ console.log(String(engines.length));
27
+ return;
28
+ }
29
+
30
+ console.log(`Active engines: ${engines.length === 0 ? "none" : engines.length}`);
31
+ for (const engine of engines.sort((a, b) => dateSortValue(a.startedAt) - dateSortValue(b.startedAt))) {
32
+ const started = formatTimeLine(engine.startedAt, now);
33
+ const ping = formatTimeLine(engine.lastPing, now);
34
+ console.log(` ${engine.room} (${engine.channel}) • started ${started} • last ping ${ping}`);
35
+ }
36
+ }
package/src/cli/index.ts CHANGED
@@ -11,6 +11,8 @@ import { errMsg } from "../utils/errors";
11
11
  import { fail, ICON_PASS, ICON_WARN } from "../utils/cli";
12
12
  import { jobCommand } from "./job";
13
13
  import { statusCommand } from "./status";
14
+ import { activeCommand } from "./active";
15
+ import { modelCommand } from "./model";
14
16
  import { sendCommand, telegramCommand, slackCommand } from "./channels";
15
17
  import { rulesCommand, memoryCommand } from "./self";
16
18
  import { watchCommand } from "./watch";
@@ -127,6 +129,16 @@ switch (command) {
127
129
  break;
128
130
  }
129
131
 
132
+ case "active": {
133
+ await activeCommand(process.argv.slice(3));
134
+ break;
135
+ }
136
+
137
+ case "model": {
138
+ await modelCommand(process.argv.slice(3));
139
+ break;
140
+ }
141
+
130
142
  case "health": {
131
143
  const { healthCommand } = await import("../commands/health");
132
144
  await healthCommand();
@@ -565,6 +577,8 @@ Daemon:
565
577
  restart [--wait N] [--force] Restart daemon
566
578
  update [--wait N] [--force] Update to latest version
567
579
  status [--json --rooms N --all] Show daemon, jobs, channels
580
+ active [--full] Show active engine count or details
581
+ model [name] Show or set global Claude model
568
582
  health Check daemon, db, channels, config
569
583
  logs [-f] [--channel ch] Daemon logs (filter by channel)
570
584
 
@@ -0,0 +1,29 @@
1
+ import { isRunning, readPid } from "../core/daemon";
2
+ import { getConfig, resetConfig, updateRawConfig } from "../utils/config";
3
+
4
+ function printUsage(): void {
5
+ console.log("Usage: nia model [default|sonnet|opus|opusplan|haiku|<model-id>]");
6
+ }
7
+
8
+ export async function modelCommand(argv: string[] = []): Promise<void> {
9
+ const model = argv[0];
10
+
11
+ if (!model) {
12
+ console.log(`model = ${getConfig().model}`);
13
+ return;
14
+ }
15
+
16
+ if (model === "--help" || model === "-h") {
17
+ printUsage();
18
+ return;
19
+ }
20
+
21
+ updateRawConfig({ model });
22
+ resetConfig();
23
+ console.log(`model = ${model}`);
24
+
25
+ const pid = readPid();
26
+ if (pid && isRunning()) {
27
+ process.kill(pid, "SIGHUP");
28
+ }
29
+ }
@@ -85,6 +85,29 @@ export function validateConfig(): Result {
85
85
  messages.push(`${PASS} runner: ${runner}`);
86
86
  }
87
87
 
88
+ // Session finalization
89
+ const sf = raw.session_finalization as Record<string, unknown> | undefined;
90
+ if (sf) {
91
+ let sessionFinalizationOk = true;
92
+ for (const key of ["enabled", "memory_consolidation", "summaries"]) {
93
+ const val = sf[key];
94
+ if (val !== undefined && typeof val !== "boolean") {
95
+ messages.push(`${FAIL} session_finalization.${key} must be true or false`);
96
+ ok = false;
97
+ sessionFinalizationOk = false;
98
+ }
99
+ }
100
+ if (sessionFinalizationOk) {
101
+ const enabled = sf.enabled !== false;
102
+ const memoryConsolidation = sf.memory_consolidation !== false;
103
+ const summaries = sf.summaries !== false;
104
+ messages.push(
105
+ `${PASS} session_finalization: ${enabled ? "enabled" : "disabled"} ` +
106
+ `(memory_consolidation=${memoryConsolidation}, summaries=${summaries})`,
107
+ );
108
+ }
109
+ }
110
+
88
111
  // Channels
89
112
  const ch = raw.channels as Record<string, unknown> | undefined;
90
113
  if (ch) {
@@ -10,10 +10,25 @@
10
10
  import { getSql } from "../db/connection";
11
11
  import { consolidateSession } from "./consolidator";
12
12
  import { summarizeSession } from "./summarizer";
13
+ import { loadConfig } from "../utils/config";
13
14
  import { log } from "../utils/log";
14
15
 
16
+ type FinalizationTask = "consolidate" | "summarize";
17
+
18
+ function getEnabledTasks(): FinalizationTask[] {
19
+ const { sessionFinalization } = loadConfig();
20
+ if (!sessionFinalization.enabled) return [];
21
+
22
+ const tasks: FinalizationTask[] = [];
23
+ if (sessionFinalization.memoryConsolidation) tasks.push("consolidate");
24
+ if (sessionFinalization.summaries) tasks.push("summarize");
25
+ return tasks;
26
+ }
27
+
15
28
  /** Enqueue a session for finalization. Always returns immediately. */
16
29
  export async function finalizeSession(sessionId: string, room: string): Promise<void> {
30
+ if (getEnabledTasks().length === 0) return;
31
+
17
32
  const sql = getSql();
18
33
 
19
34
  // Get current message count for idempotency
@@ -77,19 +92,30 @@ async function processOne(sessionId: string, room: string, messageCount: number)
77
92
  if (claimed.length === 0) return; // Already claimed or cancelled
78
93
 
79
94
  const requestId = claimed[0].id;
95
+ const tasks = getEnabledTasks();
96
+ if (tasks.length === 0) {
97
+ await sql`
98
+ UPDATE finalization_requests
99
+ SET status = 'done', updated_at = NOW()
100
+ WHERE id = ${requestId}
101
+ `;
102
+ log.info({ sessionId, room, messageCount }, "finalizer: skipped because all tasks are disabled");
103
+ return;
104
+ }
80
105
 
81
106
  try {
82
- const [consolidateResult, summarizeResult] = await Promise.allSettled([
83
- consolidateSession(sessionId, room),
84
- summarizeSession(sessionId, room),
85
- ]);
107
+ const results = await Promise.allSettled(
108
+ tasks.map((task) =>
109
+ task === "consolidate" ? consolidateSession(sessionId, room) : summarizeSession(sessionId, room),
110
+ ),
111
+ );
86
112
 
87
113
  const errors: string[] = [];
88
- if (consolidateResult.status === "rejected") {
89
- errors.push(`consolidate: ${formatRejection(consolidateResult.reason)}`);
90
- }
91
- if (summarizeResult.status === "rejected") {
92
- errors.push(`summarize: ${formatRejection(summarizeResult.reason)}`);
114
+ for (let i = 0; i < results.length; i++) {
115
+ const result = results[i];
116
+ if (result.status === "rejected") {
117
+ errors.push(`${tasks[i]}: ${formatRejection(result.reason)}`);
118
+ }
93
119
  }
94
120
 
95
121
  const finalStatus = errors.length === 0 ? "done" : "failed";
@@ -35,6 +35,12 @@ export interface ChannelsConfig {
35
35
  slack: SlackConfig;
36
36
  }
37
37
 
38
+ export interface SessionFinalizationConfig {
39
+ enabled: boolean;
40
+ memoryConsolidation: boolean;
41
+ summaries: boolean;
42
+ }
43
+
38
44
  export interface Config {
39
45
  model: string;
40
46
  runner: "claude" | "codex";
@@ -43,5 +49,6 @@ export interface Config {
43
49
  database_url: string;
44
50
  log_level: string;
45
51
  gemini_api_key: string | null;
52
+ sessionFinalization: SessionFinalizationConfig;
46
53
  channels: ChannelsConfig;
47
54
  }
@@ -16,6 +16,11 @@ const DEFAULTS: Config = {
16
16
  database_url: DEFAULT_DATABASE_URL,
17
17
  log_level: "info",
18
18
  gemini_api_key: null,
19
+ sessionFinalization: {
20
+ enabled: true,
21
+ memoryConsolidation: true,
22
+ summaries: true,
23
+ },
19
24
  channels: {
20
25
  enabled: true,
21
26
  default: "telegram",
@@ -100,6 +105,14 @@ export function loadConfig(): Config {
100
105
  const gemini_api_key =
101
106
  process.env.GEMINI_API_KEY || (typeof raw.gemini_api_key === "string" ? raw.gemini_api_key : null);
102
107
 
108
+ // Session finalization — controls post-session background LLM work.
109
+ const sf = (raw.session_finalization || {}) as Record<string, unknown>;
110
+ const sessionFinalization = {
111
+ enabled: sf.enabled !== false,
112
+ memoryConsolidation: sf.memory_consolidation !== false,
113
+ summaries: sf.summaries !== false,
114
+ };
115
+
103
116
  // --- Channels (nested under `channels:` in yaml) ---
104
117
  const ch = (raw.channels || {}) as Record<string, unknown>;
105
118
  const chTg = (ch.telegram || {}) as Record<string, unknown>;
@@ -159,6 +172,7 @@ export function loadConfig(): Config {
159
172
  database_url,
160
173
  log_level,
161
174
  gemini_api_key,
175
+ sessionFinalization,
162
176
  channels: {
163
177
  enabled: channelsEnabled,
164
178
  default: defaultChannel,