niahere 0.2.62 → 0.2.63
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 +7 -3
- package/src/cli/index.ts +23 -73
- package/src/cli/job.ts +25 -92
- package/src/cli/status.ts +17 -9
- package/src/core/agents.ts +6 -19
- package/src/core/consolidator.ts +14 -28
- package/src/core/daemon.ts +4 -41
- package/src/core/finalizer.ts +31 -3
- package/src/core/health.ts +5 -17
- package/src/core/runner.ts +0 -6
- package/src/core/scheduler.ts +12 -49
- package/src/core/skills.ts +4 -11
- package/src/core/summarizer.ts +7 -21
- package/src/db/connection.ts +0 -11
- package/src/db/models/job.ts +23 -22
- package/src/db/with-db.ts +11 -0
- package/src/mcp/server.ts +1 -1
- package/src/prompts/environment.md +44 -41
- package/src/utils/pid.ts +44 -0
- package/src/utils/schedule.ts +39 -0
package/src/core/consolidator.ts
CHANGED
|
@@ -1,27 +1,13 @@
|
|
|
1
1
|
/**
|
|
2
|
-
* Memory consolidator — stage 1 of
|
|
2
|
+
* Memory consolidator — stage 1 of the two-stage memory pipeline.
|
|
3
3
|
*
|
|
4
|
-
* After a chat session goes idle,
|
|
5
|
-
*
|
|
6
|
-
*
|
|
7
|
-
*
|
|
8
|
-
*
|
|
4
|
+
* After a chat session goes idle, reflects on the transcript and appends
|
|
5
|
+
* CANDIDATE memories to ~/.niahere/self/staging.md. The nightly
|
|
6
|
+
* memory-promoter job handles promotion from staging to memory.md/rules.md.
|
|
7
|
+
* The write-path restriction is enforced by the consolidator prompt, not
|
|
8
|
+
* by tool sandboxing.
|
|
9
9
|
*
|
|
10
|
-
*
|
|
11
|
-
* chat idle → consolidator → staging.md (candidate log, TTL 14d)
|
|
12
|
-
* ↓ nightly promoter (3am)
|
|
13
|
-
* ↓ - count >= 2 required
|
|
14
|
-
* ↓ - durability review
|
|
15
|
-
* memory.md / rules.md
|
|
16
|
-
*
|
|
17
|
-
* Jobs do NOT flow through this path — job-local learnings live in each
|
|
18
|
-
* job's state.md (see runner.ts:buildWorkingMemory). Routing job output
|
|
19
|
-
* into global persona memory caused layer violations (transient incidents
|
|
20
|
-
* promoted to durable facts).
|
|
21
|
-
*
|
|
22
|
-
* The consolidator uses the same agent loop as cron jobs — full Nia system
|
|
23
|
-
* prompt, full tool access. The write-path restriction is enforced by the
|
|
24
|
-
* prompt (the agent is told to only edit staging.md), not by tool sandboxing.
|
|
10
|
+
* See AGENTS.md > "Two-stage memory" for the full architecture.
|
|
25
11
|
*/
|
|
26
12
|
|
|
27
13
|
import { Message } from "../db/models";
|
|
@@ -138,12 +124,16 @@ Report a one-line summary of what you did: "staged N new / reinforced M /
|
|
|
138
124
|
skipped (trivial session)". No preamble.`;
|
|
139
125
|
}
|
|
140
126
|
|
|
141
|
-
/** Run the consolidation agent loop. */
|
|
142
127
|
async function runConsolidation(transcript: string, source: string): Promise<void> {
|
|
143
|
-
await runTask({
|
|
128
|
+
const output = await runTask({
|
|
144
129
|
name: "consolidator",
|
|
145
130
|
prompt: buildConsolidationPrompt(transcript, source),
|
|
146
131
|
});
|
|
132
|
+
// runTask returns {error} on failure instead of throwing; escalate so
|
|
133
|
+
// consolidateSession doesn't mark the session processed on a failed run.
|
|
134
|
+
if (output.error) {
|
|
135
|
+
throw new Error(`consolidator task failed: ${output.error}`);
|
|
136
|
+
}
|
|
147
137
|
}
|
|
148
138
|
|
|
149
139
|
/**
|
|
@@ -178,12 +168,8 @@ export async function consolidateSession(sessionId: string, room: string): Promi
|
|
|
178
168
|
}
|
|
179
169
|
} catch (err) {
|
|
180
170
|
log.error({ err, sessionId, room }, "consolidator: chat extraction failed");
|
|
171
|
+
throw err;
|
|
181
172
|
} finally {
|
|
182
173
|
inFlight.delete(sessionId);
|
|
183
174
|
}
|
|
184
175
|
}
|
|
185
|
-
|
|
186
|
-
// Job runs no longer flow through the global memory consolidator. Each job
|
|
187
|
-
// maintains its own working memory in state.md (see buildWorkingMemory() in
|
|
188
|
-
// runner.ts). This separation prevents transient job-local incidents from
|
|
189
|
-
// being promoted to durable persona memory.
|
package/src/core/daemon.ts
CHANGED
|
@@ -1,9 +1,10 @@
|
|
|
1
|
-
import { closeSync, existsSync, mkdirSync, openSync, readFileSync,
|
|
1
|
+
import { closeSync, existsSync, mkdirSync, openSync, readFileSync, writeFileSync } from "fs";
|
|
2
2
|
import { dirname, resolve as pathResolve } from "path";
|
|
3
3
|
import { fileURLToPath } from "url";
|
|
4
4
|
import { getPaths } from "../utils/paths";
|
|
5
5
|
import { getConfig, resetConfig } from "../utils/config";
|
|
6
6
|
import { log } from "../utils/log";
|
|
7
|
+
import { isRunning, readPid, removePid, writePid } from "../utils/pid";
|
|
7
8
|
import { ActiveEngine, Job } from "../db/models";
|
|
8
9
|
import { runMigrations } from "../db/migrate";
|
|
9
10
|
import { closeDb, getSql } from "../db/connection";
|
|
@@ -15,45 +16,7 @@ import { createNiaMcpServer } from "../mcp/server";
|
|
|
15
16
|
import { setMcpFactory } from "../mcp";
|
|
16
17
|
import { processPending, cleanupOldRequests } from "./finalizer";
|
|
17
18
|
|
|
18
|
-
export
|
|
19
|
-
const { pid: pidPath } = getPaths();
|
|
20
|
-
mkdirSync(dirname(pidPath), { recursive: true });
|
|
21
|
-
writeFileSync(pidPath, String(pid));
|
|
22
|
-
}
|
|
23
|
-
|
|
24
|
-
export function readPid(): number | null {
|
|
25
|
-
const { pid: pidPath } = getPaths();
|
|
26
|
-
if (!existsSync(pidPath)) return null;
|
|
27
|
-
|
|
28
|
-
try {
|
|
29
|
-
return parseInt(readFileSync(pidPath, "utf8").trim(), 10);
|
|
30
|
-
} catch {
|
|
31
|
-
return null;
|
|
32
|
-
}
|
|
33
|
-
}
|
|
34
|
-
|
|
35
|
-
export function removePid(): void {
|
|
36
|
-
const { pid: pidPath } = getPaths();
|
|
37
|
-
try {
|
|
38
|
-
unlinkSync(pidPath);
|
|
39
|
-
} catch {
|
|
40
|
-
// Already gone
|
|
41
|
-
}
|
|
42
|
-
}
|
|
43
|
-
|
|
44
|
-
export function isRunning(): boolean {
|
|
45
|
-
const pid = readPid();
|
|
46
|
-
if (pid === null) return false;
|
|
47
|
-
|
|
48
|
-
try {
|
|
49
|
-
process.kill(pid, 0);
|
|
50
|
-
return true;
|
|
51
|
-
} catch {
|
|
52
|
-
log.warn({ stalePid: pid }, "removing stale pid file (process not running)");
|
|
53
|
-
removePid();
|
|
54
|
-
return false;
|
|
55
|
-
}
|
|
56
|
-
}
|
|
19
|
+
export { isRunning, readPid, removePid, writePid };
|
|
57
20
|
|
|
58
21
|
export function startDaemon(): number {
|
|
59
22
|
const { daemonLog } = getPaths();
|
|
@@ -125,7 +88,7 @@ function waitForExit(timeoutMs: number): void {
|
|
|
125
88
|
/** Return PIDs of running daemon processes (excluding ourselves). */
|
|
126
89
|
export function findDaemonPids(): number[] {
|
|
127
90
|
try {
|
|
128
|
-
const result = Bun.spawnSync(["pgrep", "-f", "src/cli\\.ts run$"]);
|
|
91
|
+
const result = Bun.spawnSync(["pgrep", "-f", "src/cli/index\\.ts run$"]);
|
|
129
92
|
const stdout = new TextDecoder().decode(result.stdout).trim();
|
|
130
93
|
if (!stdout) return [];
|
|
131
94
|
return stdout
|
package/src/core/finalizer.ts
CHANGED
|
@@ -78,15 +78,32 @@ async function processOne(sessionId: string, room: string, messageCount: number)
|
|
|
78
78
|
const requestId = claimed[0].id;
|
|
79
79
|
|
|
80
80
|
try {
|
|
81
|
-
await Promise.allSettled([
|
|
81
|
+
const [consolidateResult, summarizeResult] = await Promise.allSettled([
|
|
82
|
+
consolidateSession(sessionId, room),
|
|
83
|
+
summarizeSession(sessionId, room),
|
|
84
|
+
]);
|
|
85
|
+
|
|
86
|
+
const errors: string[] = [];
|
|
87
|
+
if (consolidateResult.status === "rejected") {
|
|
88
|
+
errors.push(`consolidate: ${formatRejection(consolidateResult.reason)}`);
|
|
89
|
+
}
|
|
90
|
+
if (summarizeResult.status === "rejected") {
|
|
91
|
+
errors.push(`summarize: ${formatRejection(summarizeResult.reason)}`);
|
|
92
|
+
}
|
|
93
|
+
|
|
94
|
+
const finalStatus = errors.length === 0 ? "done" : "failed";
|
|
82
95
|
|
|
83
96
|
await sql`
|
|
84
97
|
UPDATE finalization_requests
|
|
85
|
-
SET status =
|
|
98
|
+
SET status = ${finalStatus}, updated_at = NOW()
|
|
86
99
|
WHERE id = ${requestId}
|
|
87
100
|
`;
|
|
88
101
|
|
|
89
|
-
|
|
102
|
+
if (errors.length === 0) {
|
|
103
|
+
log.info({ sessionId, room, messageCount }, "finalizer: completed");
|
|
104
|
+
} else {
|
|
105
|
+
log.error({ sessionId, room, messageCount, errors }, "finalizer: completed with task failures");
|
|
106
|
+
}
|
|
90
107
|
} catch (err) {
|
|
91
108
|
await sql`
|
|
92
109
|
UPDATE finalization_requests
|
|
@@ -98,6 +115,17 @@ async function processOne(sessionId: string, room: string, messageCount: number)
|
|
|
98
115
|
}
|
|
99
116
|
}
|
|
100
117
|
|
|
118
|
+
/** Normalize a Promise rejection reason into a loggable string. */
|
|
119
|
+
function formatRejection(reason: unknown): string {
|
|
120
|
+
if (reason instanceof Error) return reason.message;
|
|
121
|
+
if (typeof reason === "string") return reason;
|
|
122
|
+
try {
|
|
123
|
+
return JSON.stringify(reason);
|
|
124
|
+
} catch {
|
|
125
|
+
return String(reason);
|
|
126
|
+
}
|
|
127
|
+
}
|
|
128
|
+
|
|
101
129
|
/** Drain all pending finalization requests. Called by daemon on startup and on NOTIFY. */
|
|
102
130
|
export async function processPending(): Promise<void> {
|
|
103
131
|
const sql = getSql();
|
package/src/core/health.ts
CHANGED
|
@@ -2,7 +2,7 @@ import { existsSync, statSync } from "fs";
|
|
|
2
2
|
import { join } from "path";
|
|
3
3
|
import { getConfig, readRawConfig } from "../utils/config";
|
|
4
4
|
import { getPaths } from "../utils/paths";
|
|
5
|
-
import { isRunning, readPid } from "
|
|
5
|
+
import { isRunning, readPid } from "../utils/pid";
|
|
6
6
|
import { errMsg } from "../utils/errors";
|
|
7
7
|
import { localTime } from "../utils/time";
|
|
8
8
|
import { withRetry } from "../utils/retry";
|
|
@@ -123,11 +123,7 @@ export async function runHealthChecks(): Promise<Check[]> {
|
|
|
123
123
|
}),
|
|
124
124
|
);
|
|
125
125
|
const data = (await resp.json()) as { ok: boolean; error?: string };
|
|
126
|
-
results.push(
|
|
127
|
-
data.ok
|
|
128
|
-
? "slack: connected"
|
|
129
|
-
: `slack: ${data.error || "auth failed"}`,
|
|
130
|
-
);
|
|
126
|
+
results.push(data.ok ? "slack: connected" : `slack: ${data.error || "auth failed"}`);
|
|
131
127
|
if (!data.ok)
|
|
132
128
|
checks.push({
|
|
133
129
|
name: "slack",
|
|
@@ -159,10 +155,7 @@ export async function runHealthChecks(): Promise<Check[]> {
|
|
|
159
155
|
// API keys
|
|
160
156
|
const geminiKey = config.gemini_api_key;
|
|
161
157
|
const rawConfig = readRawConfig();
|
|
162
|
-
const openaiKey =
|
|
163
|
-
typeof rawConfig.openai_api_key === "string"
|
|
164
|
-
? rawConfig.openai_api_key
|
|
165
|
-
: null;
|
|
158
|
+
const openaiKey = typeof rawConfig.openai_api_key === "string" ? rawConfig.openai_api_key : null;
|
|
166
159
|
const apiKeys: string[] = [];
|
|
167
160
|
if (geminiKey) apiKeys.push("gemini");
|
|
168
161
|
if (openaiKey) apiKeys.push("openai");
|
|
@@ -174,16 +167,11 @@ export async function runHealthChecks(): Promise<Check[]> {
|
|
|
174
167
|
|
|
175
168
|
// Persona files
|
|
176
169
|
const personaFiles = ["identity.md", "owner.md", "soul.md"];
|
|
177
|
-
const missing = personaFiles.filter(
|
|
178
|
-
(f) => !existsSync(join(paths.selfDir, f)),
|
|
179
|
-
);
|
|
170
|
+
const missing = personaFiles.filter((f) => !existsSync(join(paths.selfDir, f)));
|
|
180
171
|
checks.push({
|
|
181
172
|
name: "persona",
|
|
182
173
|
status: missing.length === 0 ? "ok" : "warn",
|
|
183
|
-
detail:
|
|
184
|
-
missing.length === 0
|
|
185
|
-
? "all files present"
|
|
186
|
-
: "missing: " + missing.join(", "),
|
|
174
|
+
detail: missing.length === 0 ? "all files present" : "missing: " + missing.join(", "),
|
|
187
175
|
});
|
|
188
176
|
|
|
189
177
|
// Daemon log
|
package/src/core/runner.ts
CHANGED
|
@@ -372,12 +372,6 @@ export async function runJob(job: JobInput, onActivity?: ActivityCallback): Prom
|
|
|
372
372
|
};
|
|
373
373
|
writeState(freshState);
|
|
374
374
|
|
|
375
|
-
// Job-local learnings live in state.md (injected into the next run's
|
|
376
|
-
// prompt), not in global self/memory.md. The global memory consolidator
|
|
377
|
-
// only processes chat sessions — routing job output through it created
|
|
378
|
-
// layer violations (transient incidents promoted to durable memory).
|
|
379
|
-
// See src/core/consolidator.ts and AGENTS.md memory architecture notes.
|
|
380
|
-
|
|
381
375
|
return result;
|
|
382
376
|
} catch (err) {
|
|
383
377
|
const duration_ms = Math.round(performance.now() - startMs);
|
package/src/core/scheduler.ts
CHANGED
|
@@ -1,50 +1,10 @@
|
|
|
1
|
-
import { CronExpressionParser } from "cron-parser";
|
|
2
|
-
import { parseDuration } from "../utils/duration";
|
|
3
|
-
import type { ScheduleType } from "../types";
|
|
4
1
|
import { Job } from "../db/models";
|
|
5
2
|
import { runJob } from "./runner";
|
|
6
3
|
import { getConfig } from "../utils/config";
|
|
7
4
|
import { log } from "../utils/log";
|
|
5
|
+
import { computeInitialNextRun, computeNextRun } from "../utils/schedule";
|
|
8
6
|
|
|
9
|
-
export
|
|
10
|
-
scheduleType: ScheduleType,
|
|
11
|
-
schedule: string,
|
|
12
|
-
timezone: string,
|
|
13
|
-
lastRunAt?: Date,
|
|
14
|
-
): Date | null {
|
|
15
|
-
switch (scheduleType) {
|
|
16
|
-
case "cron": {
|
|
17
|
-
const expr = CronExpressionParser.parse(schedule, { tz: timezone });
|
|
18
|
-
return expr.next().toDate();
|
|
19
|
-
}
|
|
20
|
-
case "interval": {
|
|
21
|
-
const ms = parseDuration(schedule);
|
|
22
|
-
const base = lastRunAt || new Date();
|
|
23
|
-
return new Date(base.getTime() + ms);
|
|
24
|
-
}
|
|
25
|
-
case "once":
|
|
26
|
-
return null;
|
|
27
|
-
}
|
|
28
|
-
}
|
|
29
|
-
|
|
30
|
-
export function computeInitialNextRun(
|
|
31
|
-
scheduleType: ScheduleType,
|
|
32
|
-
schedule: string,
|
|
33
|
-
timezone: string,
|
|
34
|
-
): Date {
|
|
35
|
-
switch (scheduleType) {
|
|
36
|
-
case "cron": {
|
|
37
|
-
const expr = CronExpressionParser.parse(schedule, { tz: timezone });
|
|
38
|
-
return expr.next().toDate();
|
|
39
|
-
}
|
|
40
|
-
case "interval": {
|
|
41
|
-
const ms = parseDuration(schedule);
|
|
42
|
-
return new Date(Date.now() + ms);
|
|
43
|
-
}
|
|
44
|
-
case "once":
|
|
45
|
-
return new Date(schedule);
|
|
46
|
-
}
|
|
47
|
-
}
|
|
7
|
+
export { computeInitialNextRun, computeNextRun };
|
|
48
8
|
|
|
49
9
|
function isWithinActiveHours(): boolean {
|
|
50
10
|
const config = getConfig();
|
|
@@ -96,13 +56,16 @@ async function tick(): Promise<void> {
|
|
|
96
56
|
log.info({ job: job.name, type: job.scheduleType }, "scheduler: running job");
|
|
97
57
|
runningJobs.add(job.name);
|
|
98
58
|
|
|
99
|
-
runJob(job)
|
|
100
|
-
|
|
101
|
-
|
|
102
|
-
|
|
103
|
-
|
|
104
|
-
|
|
105
|
-
|
|
59
|
+
runJob(job)
|
|
60
|
+
.then((result) => {
|
|
61
|
+
log.info({ job: job.name, status: result.status, duration: result.duration_ms }, "scheduler: job completed");
|
|
62
|
+
})
|
|
63
|
+
.catch((err) => {
|
|
64
|
+
log.error({ err, job: job.name }, "scheduler: job failed");
|
|
65
|
+
})
|
|
66
|
+
.finally(() => {
|
|
67
|
+
runningJobs.delete(job.name);
|
|
68
|
+
});
|
|
106
69
|
|
|
107
70
|
let nextRun: Date | null = null;
|
|
108
71
|
try {
|
package/src/core/skills.ts
CHANGED
|
@@ -40,14 +40,10 @@ export function scanSkills(): SkillInfo[] {
|
|
|
40
40
|
try {
|
|
41
41
|
meta = (yaml.load(fmMatch[1]) as Record<string, unknown>) || {};
|
|
42
42
|
} catch (err) {
|
|
43
|
-
log.warn(
|
|
44
|
-
{ err, skill: entry.name, path: skillFile },
|
|
45
|
-
"failed to parse skill metadata, skipping",
|
|
46
|
-
);
|
|
43
|
+
log.warn({ err, skill: entry.name, path: skillFile }, "failed to parse skill metadata, skipping");
|
|
47
44
|
continue;
|
|
48
45
|
}
|
|
49
|
-
const name =
|
|
50
|
-
(typeof meta.name === "string" ? meta.name : "") || entry.name;
|
|
46
|
+
const name = (typeof meta.name === "string" ? meta.name : "") || entry.name;
|
|
51
47
|
|
|
52
48
|
const key = name.toLowerCase();
|
|
53
49
|
if (seen.has(key)) continue;
|
|
@@ -55,8 +51,7 @@ export function scanSkills(): SkillInfo[] {
|
|
|
55
51
|
|
|
56
52
|
skills.push({
|
|
57
53
|
name,
|
|
58
|
-
description:
|
|
59
|
-
typeof meta.description === "string" ? meta.description : "",
|
|
54
|
+
description: typeof meta.description === "string" ? meta.description : "",
|
|
60
55
|
source,
|
|
61
56
|
});
|
|
62
57
|
}
|
|
@@ -72,8 +67,6 @@ export function getSkillNames(): string[] {
|
|
|
72
67
|
export function getSkillsSummary(): string {
|
|
73
68
|
const skills = scanSkills();
|
|
74
69
|
if (skills.length === 0) return "";
|
|
75
|
-
const lines = skills.map((s) =>
|
|
76
|
-
s.description ? `- /${s.name}: ${s.description}` : `- /${s.name}`,
|
|
77
|
-
);
|
|
70
|
+
const lines = skills.map((s) => (s.description ? `- /${s.name}: ${s.description}` : `- /${s.name}`));
|
|
78
71
|
return `Available skills:\n${lines.join("\n")}`;
|
|
79
72
|
}
|
package/src/core/summarizer.ts
CHANGED
|
@@ -26,19 +26,14 @@ const MAX_MESSAGES = 30;
|
|
|
26
26
|
/** Format transcript for the summarization prompt. */
|
|
27
27
|
function formatTranscript(messages: SessionMessage[]): string {
|
|
28
28
|
const recent = messages.slice(-MAX_MESSAGES);
|
|
29
|
-
return recent
|
|
30
|
-
.map((m) => `[${m.sender}]: ${m.content.slice(0, 1000)}`)
|
|
31
|
-
.join("\n");
|
|
29
|
+
return recent.map((m) => `[${m.sender}]: ${m.content.slice(0, 1000)}`).join("\n");
|
|
32
30
|
}
|
|
33
31
|
|
|
34
32
|
/**
|
|
35
33
|
* Summarize a session and store the result in the sessions table.
|
|
36
34
|
* Called when a chat engine goes idle — produces a context bridge for the next session.
|
|
37
35
|
*/
|
|
38
|
-
export async function summarizeSession(
|
|
39
|
-
sessionId: string,
|
|
40
|
-
room: string,
|
|
41
|
-
): Promise<void> {
|
|
36
|
+
export async function summarizeSession(sessionId: string, room: string): Promise<void> {
|
|
42
37
|
if (room.includes("placeholder")) return;
|
|
43
38
|
if (inFlight.has(sessionId)) return;
|
|
44
39
|
|
|
@@ -51,10 +46,7 @@ export async function summarizeSession(
|
|
|
51
46
|
|
|
52
47
|
inFlight.add(sessionId);
|
|
53
48
|
|
|
54
|
-
log.info(
|
|
55
|
-
{ sessionId, room, messageCount: messages.length },
|
|
56
|
-
"summarizer: generating session summary",
|
|
57
|
-
);
|
|
49
|
+
log.info({ sessionId, room, messageCount: messages.length }, "summarizer: generating session summary");
|
|
58
50
|
|
|
59
51
|
const transcript = formatTranscript(messages);
|
|
60
52
|
|
|
@@ -76,8 +68,7 @@ Keep it concise — a handoff note, not a report. Output ONLY the summary text.`
|
|
|
76
68
|
const output = await runTask({ name: "summarizer", prompt });
|
|
77
69
|
|
|
78
70
|
if (output.error) {
|
|
79
|
-
|
|
80
|
-
return;
|
|
71
|
+
throw new Error(`summarizer task failed: ${output.error}`);
|
|
81
72
|
}
|
|
82
73
|
|
|
83
74
|
const summary = output.agentText.trim();
|
|
@@ -88,18 +79,13 @@ Keep it concise — a handoff note, not a report. Output ONLY the summary text.`
|
|
|
88
79
|
const firstKey = processedCounts.keys().next().value;
|
|
89
80
|
if (firstKey) processedCounts.delete(firstKey);
|
|
90
81
|
}
|
|
91
|
-
log.info(
|
|
92
|
-
{ sessionId, room, summaryChars: summary.length },
|
|
93
|
-
"summarizer: saved",
|
|
94
|
-
);
|
|
82
|
+
log.info({ sessionId, room, summaryChars: summary.length }, "summarizer: saved");
|
|
95
83
|
} else {
|
|
96
|
-
log.warn(
|
|
97
|
-
{ sessionId, room, length: summary.length },
|
|
98
|
-
"summarizer: output too short or too long, skipped",
|
|
99
|
-
);
|
|
84
|
+
log.warn({ sessionId, room, length: summary.length }, "summarizer: output too short or too long, skipped");
|
|
100
85
|
}
|
|
101
86
|
} catch (err) {
|
|
102
87
|
log.error({ err, sessionId, room }, "summarizer: failed");
|
|
88
|
+
throw err;
|
|
103
89
|
} finally {
|
|
104
90
|
inFlight.delete(sessionId);
|
|
105
91
|
}
|
package/src/db/connection.ts
CHANGED
|
@@ -23,14 +23,3 @@ export async function closeDb(): Promise<void> {
|
|
|
23
23
|
_sql = null;
|
|
24
24
|
}
|
|
25
25
|
}
|
|
26
|
-
|
|
27
|
-
/** Run migrations, execute fn, then close DB. */
|
|
28
|
-
export async function withDb<T>(fn: () => Promise<T>): Promise<T> {
|
|
29
|
-
const { runMigrations } = await import("./migrate");
|
|
30
|
-
await runMigrations();
|
|
31
|
-
try {
|
|
32
|
-
return await fn();
|
|
33
|
-
} finally {
|
|
34
|
-
await closeDb();
|
|
35
|
-
}
|
|
36
|
-
}
|
package/src/db/models/job.ts
CHANGED
|
@@ -1,6 +1,8 @@
|
|
|
1
1
|
import { getSql } from "../connection";
|
|
2
2
|
import { CronExpressionParser } from "cron-parser";
|
|
3
3
|
import { parseDuration } from "../../utils/duration";
|
|
4
|
+
import { computeInitialNextRun } from "../../utils/schedule";
|
|
5
|
+
import { getConfig } from "../../utils/config";
|
|
4
6
|
import type { ScheduleType } from "../../types";
|
|
5
7
|
|
|
6
8
|
/** Validate that a schedule string matches its declared type. Throws on mismatch. */
|
|
@@ -10,26 +12,20 @@ function validateSchedule(schedule: string, scheduleType: ScheduleType): void {
|
|
|
10
12
|
try {
|
|
11
13
|
CronExpressionParser.parse(schedule);
|
|
12
14
|
} catch (err) {
|
|
13
|
-
throw new Error(
|
|
14
|
-
`Invalid cron expression "${schedule}": ${err instanceof Error ? err.message : err}`,
|
|
15
|
-
);
|
|
15
|
+
throw new Error(`Invalid cron expression "${schedule}": ${err instanceof Error ? err.message : err}`);
|
|
16
16
|
}
|
|
17
17
|
break;
|
|
18
18
|
case "interval":
|
|
19
19
|
try {
|
|
20
20
|
parseDuration(schedule);
|
|
21
21
|
} catch (err) {
|
|
22
|
-
throw new Error(
|
|
23
|
-
`Invalid interval "${schedule}": ${err instanceof Error ? err.message : err}`,
|
|
24
|
-
);
|
|
22
|
+
throw new Error(`Invalid interval "${schedule}": ${err instanceof Error ? err.message : err}`);
|
|
25
23
|
}
|
|
26
24
|
break;
|
|
27
25
|
case "once": {
|
|
28
26
|
const d = new Date(schedule);
|
|
29
27
|
if (isNaN(d.getTime())) {
|
|
30
|
-
throw new Error(
|
|
31
|
-
`Invalid timestamp "${schedule}": expected ISO 8601 date`,
|
|
32
|
-
);
|
|
28
|
+
throw new Error(`Invalid timestamp "${schedule}": expected ISO 8601 date`);
|
|
33
29
|
}
|
|
34
30
|
break;
|
|
35
31
|
}
|
|
@@ -89,9 +85,7 @@ export async function create(
|
|
|
89
85
|
validateSchedule(schedule, scheduleType);
|
|
90
86
|
const existing = await get(name);
|
|
91
87
|
if (existing) {
|
|
92
|
-
throw new Error(
|
|
93
|
-
`Job "${name}" already exists. Use \`nia job remove ${name}\` first, or choose a different name.`,
|
|
94
|
-
);
|
|
88
|
+
throw new Error(`Job "${name}" already exists. Use \`nia job remove ${name}\` first, or choose a different name.`);
|
|
95
89
|
}
|
|
96
90
|
const sql = getSql();
|
|
97
91
|
await sql`
|
|
@@ -141,15 +135,25 @@ export async function update(
|
|
|
141
135
|
const model = fields.model !== undefined ? fields.model : existing.model;
|
|
142
136
|
const stateless = fields.stateless ?? existing.stateless;
|
|
143
137
|
|
|
144
|
-
|
|
138
|
+
const scheduleChanged = fields.schedule !== undefined || fields.scheduleType !== undefined;
|
|
139
|
+
if (scheduleChanged) {
|
|
145
140
|
validateSchedule(schedule, scheduleType);
|
|
146
141
|
}
|
|
147
142
|
|
|
148
|
-
|
|
149
|
-
|
|
150
|
-
|
|
151
|
-
|
|
152
|
-
|
|
143
|
+
if (scheduleChanged) {
|
|
144
|
+
const nextRun = computeInitialNextRun(scheduleType, schedule, getConfig().timezone);
|
|
145
|
+
await sql`
|
|
146
|
+
UPDATE jobs
|
|
147
|
+
SET schedule = ${schedule}, schedule_type = ${scheduleType}, prompt = ${prompt}, enabled = ${enabled}, always = ${always}, agent = ${agent}, model = ${model}, stateless = ${stateless}, next_run_at = ${nextRun}, updated_at = NOW()
|
|
148
|
+
WHERE name = ${name}
|
|
149
|
+
`;
|
|
150
|
+
} else {
|
|
151
|
+
await sql`
|
|
152
|
+
UPDATE jobs
|
|
153
|
+
SET schedule = ${schedule}, schedule_type = ${scheduleType}, prompt = ${prompt}, enabled = ${enabled}, always = ${always}, agent = ${agent}, model = ${model}, stateless = ${stateless}, updated_at = NOW()
|
|
154
|
+
WHERE name = ${name}
|
|
155
|
+
`;
|
|
156
|
+
}
|
|
153
157
|
await notifyChange();
|
|
154
158
|
return true;
|
|
155
159
|
}
|
|
@@ -179,10 +183,7 @@ export async function listDue(): Promise<Job[]> {
|
|
|
179
183
|
return rows.map(toJob);
|
|
180
184
|
}
|
|
181
185
|
|
|
182
|
-
export async function markRun(
|
|
183
|
-
name: string,
|
|
184
|
-
nextRunAt: Date | null,
|
|
185
|
-
): Promise<void> {
|
|
186
|
+
export async function markRun(name: string, nextRunAt: Date | null): Promise<void> {
|
|
186
187
|
const sql = getSql();
|
|
187
188
|
if (nextRunAt) {
|
|
188
189
|
await sql`UPDATE jobs SET last_run_at = NOW(), next_run_at = ${nextRunAt}, updated_at = NOW() WHERE name = ${name}`;
|
package/src/mcp/server.ts
CHANGED
|
@@ -290,7 +290,7 @@ export function createNiaMcpServer() {
|
|
|
290
290
|
),
|
|
291
291
|
tool(
|
|
292
292
|
"add_memory",
|
|
293
|
-
"Save a concise factual memory for future reference.
|
|
293
|
+
"Save a concise factual memory for future reference. Call this when the user explicitly asks you to remember something, or when a correction needs an immediate durable record. For observations you notice on your own during a session, let the post-session consolidator handle it via staging.md — don't preemptively save here. RULES: Max 300 chars. One insight per entry. NO raw logs, NO transcripts, NO status dumps.",
|
|
294
294
|
{
|
|
295
295
|
entry: z.string().max(300).describe("A single concise insight (max 300 chars, no raw logs or transcripts)"),
|
|
296
296
|
},
|