niahere 0.2.62 → 0.2.64
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/chat/employee-prompt.ts +75 -0
- package/src/chat/engine.ts +40 -7
- package/src/chat/identity.ts +8 -1
- package/src/chat/repl.ts +52 -25
- package/src/cli/employee-add.ts +124 -0
- package/src/cli/employee.ts +167 -0
- package/src/cli/index.ts +43 -77
- package/src/cli/job.ts +36 -93
- package/src/cli/status.ts +18 -9
- package/src/commands/backup.ts +3 -7
- package/src/core/agents.ts +8 -20
- package/src/core/consolidator.ts +14 -28
- package/src/core/daemon.ts +4 -41
- package/src/core/employees.ts +116 -0
- package/src/core/finalizer.ts +31 -3
- package/src/core/health.ts +5 -17
- package/src/core/runner.ts +15 -9
- package/src/core/scheduler.ts +12 -49
- package/src/core/skills.ts +6 -12
- package/src/core/summarizer.ts +7 -21
- package/src/db/connection.ts +0 -11
- package/src/db/migrations/015_jobs_employee.ts +7 -0
- package/src/db/models/job.ts +34 -28
- package/src/db/with-db.ts +11 -0
- package/src/mcp/server.ts +15 -2
- package/src/mcp/tools.ts +13 -2
- package/src/prompts/environment.md +44 -41
- package/src/types/employee.ts +14 -0
- package/src/types/engine.ts +9 -2
- package/src/types/index.ts +1 -0
- package/src/types/job.ts +1 -0
- package/src/types/paths.ts +1 -0
- package/src/utils/paths.ts +1 -0
- package/src/utils/pid.ts +44 -0
- package/src/utils/schedule.ts +39 -0
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
|
}
|
|
@@ -44,6 +40,7 @@ export interface Job {
|
|
|
44
40
|
always: boolean;
|
|
45
41
|
scheduleType: ScheduleType;
|
|
46
42
|
agent: string | null;
|
|
43
|
+
employee: string | null;
|
|
47
44
|
model: string | null;
|
|
48
45
|
stateless: boolean;
|
|
49
46
|
nextRunAt: string | null;
|
|
@@ -61,6 +58,7 @@ function toJob(r: Record<string, any>): Job {
|
|
|
61
58
|
always: r.always ?? false,
|
|
62
59
|
scheduleType: r.schedule_type || "cron",
|
|
63
60
|
agent: r.agent || null,
|
|
61
|
+
employee: r.employee || null,
|
|
64
62
|
model: r.model || null,
|
|
65
63
|
stateless: r.stateless ?? false,
|
|
66
64
|
nextRunAt: r.next_run_at ? String(r.next_run_at) : null,
|
|
@@ -85,18 +83,17 @@ export async function create(
|
|
|
85
83
|
agent?: string,
|
|
86
84
|
stateless = false,
|
|
87
85
|
model?: string,
|
|
86
|
+
employee?: string,
|
|
88
87
|
): Promise<void> {
|
|
89
88
|
validateSchedule(schedule, scheduleType);
|
|
90
89
|
const existing = await get(name);
|
|
91
90
|
if (existing) {
|
|
92
|
-
throw new Error(
|
|
93
|
-
`Job "${name}" already exists. Use \`nia job remove ${name}\` first, or choose a different name.`,
|
|
94
|
-
);
|
|
91
|
+
throw new Error(`Job "${name}" already exists. Use \`nia job remove ${name}\` first, or choose a different name.`);
|
|
95
92
|
}
|
|
96
93
|
const sql = getSql();
|
|
97
94
|
await sql`
|
|
98
|
-
INSERT INTO jobs (name, schedule, prompt, always, schedule_type, next_run_at, agent, stateless, model)
|
|
99
|
-
VALUES (${name}, ${schedule}, ${prompt}, ${always}, ${scheduleType}, ${nextRunAt ?? null}, ${agent ?? null}, ${stateless}, ${model ?? null})
|
|
95
|
+
INSERT INTO jobs (name, schedule, prompt, always, schedule_type, next_run_at, agent, stateless, model, employee)
|
|
96
|
+
VALUES (${name}, ${schedule}, ${prompt}, ${always}, ${scheduleType}, ${nextRunAt ?? null}, ${agent ?? null}, ${stateless}, ${model ?? null}, ${employee ?? null})
|
|
100
97
|
`;
|
|
101
98
|
await notifyChange();
|
|
102
99
|
}
|
|
@@ -104,14 +101,14 @@ export async function create(
|
|
|
104
101
|
export async function list(): Promise<Job[]> {
|
|
105
102
|
const sql = getSql();
|
|
106
103
|
const rows =
|
|
107
|
-
await sql`SELECT name, schedule, prompt, enabled, always, schedule_type, agent, model, stateless, next_run_at, last_run_at, created_at, updated_at FROM jobs ORDER BY name`;
|
|
104
|
+
await sql`SELECT name, schedule, prompt, enabled, always, schedule_type, agent, employee, model, stateless, next_run_at, last_run_at, created_at, updated_at FROM jobs ORDER BY name`;
|
|
108
105
|
return rows.map(toJob);
|
|
109
106
|
}
|
|
110
107
|
|
|
111
108
|
export async function get(name: string): Promise<Job | null> {
|
|
112
109
|
const sql = getSql();
|
|
113
110
|
const rows =
|
|
114
|
-
await sql`SELECT name, schedule, prompt, enabled, always, schedule_type, agent, model, stateless, next_run_at, last_run_at, created_at, updated_at FROM jobs WHERE name = ${name}`;
|
|
111
|
+
await sql`SELECT name, schedule, prompt, enabled, always, schedule_type, agent, employee, model, stateless, next_run_at, last_run_at, created_at, updated_at FROM jobs WHERE name = ${name}`;
|
|
115
112
|
return rows.length > 0 ? toJob(rows[0]) : null;
|
|
116
113
|
}
|
|
117
114
|
|
|
@@ -123,6 +120,7 @@ export async function update(
|
|
|
123
120
|
enabled: boolean;
|
|
124
121
|
always: boolean;
|
|
125
122
|
agent: string | null;
|
|
123
|
+
employee: string | null;
|
|
126
124
|
model: string | null;
|
|
127
125
|
stateless: boolean;
|
|
128
126
|
scheduleType: ScheduleType;
|
|
@@ -138,18 +136,29 @@ export async function update(
|
|
|
138
136
|
const enabled = fields.enabled ?? existing.enabled;
|
|
139
137
|
const always = fields.always ?? existing.always;
|
|
140
138
|
const agent = fields.agent !== undefined ? fields.agent : existing.agent;
|
|
139
|
+
const employee = fields.employee !== undefined ? fields.employee : existing.employee;
|
|
141
140
|
const model = fields.model !== undefined ? fields.model : existing.model;
|
|
142
141
|
const stateless = fields.stateless ?? existing.stateless;
|
|
143
142
|
|
|
144
|
-
|
|
143
|
+
const scheduleChanged = fields.schedule !== undefined || fields.scheduleType !== undefined;
|
|
144
|
+
if (scheduleChanged) {
|
|
145
145
|
validateSchedule(schedule, scheduleType);
|
|
146
146
|
}
|
|
147
147
|
|
|
148
|
-
|
|
149
|
-
|
|
150
|
-
|
|
151
|
-
|
|
152
|
-
|
|
148
|
+
if (scheduleChanged) {
|
|
149
|
+
const nextRun = computeInitialNextRun(scheduleType, schedule, getConfig().timezone);
|
|
150
|
+
await sql`
|
|
151
|
+
UPDATE jobs
|
|
152
|
+
SET schedule = ${schedule}, schedule_type = ${scheduleType}, prompt = ${prompt}, enabled = ${enabled}, always = ${always}, agent = ${agent}, employee = ${employee}, model = ${model}, stateless = ${stateless}, next_run_at = ${nextRun}, updated_at = NOW()
|
|
153
|
+
WHERE name = ${name}
|
|
154
|
+
`;
|
|
155
|
+
} else {
|
|
156
|
+
await sql`
|
|
157
|
+
UPDATE jobs
|
|
158
|
+
SET schedule = ${schedule}, schedule_type = ${scheduleType}, prompt = ${prompt}, enabled = ${enabled}, always = ${always}, agent = ${agent}, employee = ${employee}, model = ${model}, stateless = ${stateless}, updated_at = NOW()
|
|
159
|
+
WHERE name = ${name}
|
|
160
|
+
`;
|
|
161
|
+
}
|
|
153
162
|
await notifyChange();
|
|
154
163
|
return true;
|
|
155
164
|
}
|
|
@@ -164,14 +173,14 @@ export async function remove(name: string): Promise<boolean> {
|
|
|
164
173
|
export async function listEnabled(): Promise<Job[]> {
|
|
165
174
|
const sql = getSql();
|
|
166
175
|
const rows =
|
|
167
|
-
await sql`SELECT name, schedule, prompt, enabled, always, schedule_type, agent, model, stateless, next_run_at, last_run_at, created_at, updated_at FROM jobs WHERE enabled = TRUE ORDER BY name`;
|
|
176
|
+
await sql`SELECT name, schedule, prompt, enabled, always, schedule_type, agent, employee, model, stateless, next_run_at, last_run_at, created_at, updated_at FROM jobs WHERE enabled = TRUE ORDER BY name`;
|
|
168
177
|
return rows.map(toJob);
|
|
169
178
|
}
|
|
170
179
|
|
|
171
180
|
export async function listDue(): Promise<Job[]> {
|
|
172
181
|
const sql = getSql();
|
|
173
182
|
const rows = await sql`
|
|
174
|
-
SELECT name, schedule, prompt, enabled, always, schedule_type, agent, model, stateless, next_run_at, last_run_at, created_at, updated_at
|
|
183
|
+
SELECT name, schedule, prompt, enabled, always, schedule_type, agent, employee, model, stateless, next_run_at, last_run_at, created_at, updated_at
|
|
175
184
|
FROM jobs
|
|
176
185
|
WHERE enabled = TRUE AND next_run_at <= NOW()
|
|
177
186
|
ORDER BY next_run_at
|
|
@@ -179,10 +188,7 @@ export async function listDue(): Promise<Job[]> {
|
|
|
179
188
|
return rows.map(toJob);
|
|
180
189
|
}
|
|
181
190
|
|
|
182
|
-
export async function markRun(
|
|
183
|
-
name: string,
|
|
184
|
-
nextRunAt: Date | null,
|
|
185
|
-
): Promise<void> {
|
|
191
|
+
export async function markRun(name: string, nextRunAt: Date | null): Promise<void> {
|
|
186
192
|
const sql = getSql();
|
|
187
193
|
if (nextRunAt) {
|
|
188
194
|
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
|
@@ -23,6 +23,10 @@ export function createNiaMcpServer() {
|
|
|
23
23
|
.string()
|
|
24
24
|
.optional()
|
|
25
25
|
.describe("Agent name to use for this job (loads agent's AGENT.md as system prompt)"),
|
|
26
|
+
employee: z
|
|
27
|
+
.string()
|
|
28
|
+
.optional()
|
|
29
|
+
.describe("Employee name to use for this job (loads employee identity, runs in employee's repo)"),
|
|
26
30
|
stateless: z
|
|
27
31
|
.boolean()
|
|
28
32
|
.default(false)
|
|
@@ -38,7 +42,7 @@ export function createNiaMcpServer() {
|
|
|
38
42
|
),
|
|
39
43
|
tool(
|
|
40
44
|
"update_job",
|
|
41
|
-
"Update an existing job's schedule, prompt, always flag, agent, model, stateless, or schedule_type. Only pass fields you want to change.",
|
|
45
|
+
"Update an existing job's schedule, prompt, always flag, agent, employee, model, stateless, or schedule_type. Only pass fields you want to change.",
|
|
42
46
|
{
|
|
43
47
|
name: z.string().describe("Job name to update"),
|
|
44
48
|
schedule: z
|
|
@@ -48,6 +52,7 @@ export function createNiaMcpServer() {
|
|
|
48
52
|
prompt: z.string().optional().describe("New prompt"),
|
|
49
53
|
always: z.boolean().optional().describe("If true, runs 24/7 ignoring active hours"),
|
|
50
54
|
agent: z.string().nullable().optional().describe("Agent name (set null to remove agent)"),
|
|
55
|
+
employee: z.string().nullable().optional().describe("Employee name (set null to remove employee)"),
|
|
51
56
|
model: z.string().nullable().optional().describe("Model override (set null to remove and use default)"),
|
|
52
57
|
stateless: z
|
|
53
58
|
.boolean()
|
|
@@ -290,7 +295,7 @@ export function createNiaMcpServer() {
|
|
|
290
295
|
),
|
|
291
296
|
tool(
|
|
292
297
|
"add_memory",
|
|
293
|
-
"Save a concise factual memory for future reference.
|
|
298
|
+
"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
299
|
{
|
|
295
300
|
entry: z.string().max(300).describe("A single concise insight (max 300 chars, no raw logs or transcripts)"),
|
|
296
301
|
},
|
|
@@ -306,6 +311,14 @@ export function createNiaMcpServer() {
|
|
|
306
311
|
content: [{ type: "text" as const, text: handlers.listAgents() }],
|
|
307
312
|
}),
|
|
308
313
|
),
|
|
314
|
+
tool(
|
|
315
|
+
"list_employees",
|
|
316
|
+
"List all employees with their role, project, status, and model. Employees are persistent co-founders/team members scoped to projects.",
|
|
317
|
+
{},
|
|
318
|
+
async () => ({
|
|
319
|
+
content: [{ type: "text" as const, text: handlers.listEmployees() }],
|
|
320
|
+
}),
|
|
321
|
+
),
|
|
309
322
|
],
|
|
310
323
|
});
|
|
311
324
|
}
|
package/src/mcp/tools.ts
CHANGED
|
@@ -9,6 +9,7 @@ import { getChannel } from "../channels/registry";
|
|
|
9
9
|
import { log } from "../utils/log";
|
|
10
10
|
import { classifyMime } from "../utils/attachment";
|
|
11
11
|
import { scanAgents } from "../core/agents";
|
|
12
|
+
import { listEmployeesForMcp } from "../core/employees";
|
|
12
13
|
|
|
13
14
|
export async function listJobs(): Promise<string> {
|
|
14
15
|
const jobs = await Job.list();
|
|
@@ -23,6 +24,7 @@ export async function addJob(args: {
|
|
|
23
24
|
schedule_type?: ScheduleType;
|
|
24
25
|
always?: boolean;
|
|
25
26
|
agent?: string;
|
|
27
|
+
employee?: string;
|
|
26
28
|
model?: string;
|
|
27
29
|
stateless?: boolean;
|
|
28
30
|
}): Promise<string> {
|
|
@@ -42,10 +44,12 @@ export async function addJob(args: {
|
|
|
42
44
|
args.agent,
|
|
43
45
|
stateless,
|
|
44
46
|
args.model,
|
|
47
|
+
args.employee,
|
|
45
48
|
);
|
|
46
49
|
const agentNote = args.agent ? ` [agent: ${args.agent}]` : "";
|
|
50
|
+
const employeeNote = args.employee ? ` [employee: ${args.employee}]` : "";
|
|
47
51
|
const modelNote = args.model ? ` [model: ${args.model}]` : "";
|
|
48
|
-
return `Job "${args.name}" created (${scheduleType}: ${args.schedule})${agentNote}${modelNote}. Next run: ${nextRunAt.toISOString()}`;
|
|
52
|
+
return `Job "${args.name}" created (${scheduleType}: ${args.schedule})${agentNote}${employeeNote}${modelNote}. Next run: ${nextRunAt.toISOString()}`;
|
|
49
53
|
}
|
|
50
54
|
|
|
51
55
|
export async function updateJob(args: {
|
|
@@ -54,6 +58,7 @@ export async function updateJob(args: {
|
|
|
54
58
|
prompt?: string;
|
|
55
59
|
always?: boolean;
|
|
56
60
|
agent?: string | null;
|
|
61
|
+
employee?: string | null;
|
|
57
62
|
model?: string | null;
|
|
58
63
|
stateless?: boolean;
|
|
59
64
|
schedule_type?: "cron" | "interval" | "once";
|
|
@@ -65,6 +70,7 @@ export async function updateJob(args: {
|
|
|
65
70
|
stateless: boolean;
|
|
66
71
|
model: string | null;
|
|
67
72
|
agent: string | null;
|
|
73
|
+
employee: string | null;
|
|
68
74
|
scheduleType: "cron" | "interval" | "once";
|
|
69
75
|
}> = {};
|
|
70
76
|
if (args.schedule) fields.schedule = args.schedule;
|
|
@@ -73,10 +79,11 @@ export async function updateJob(args: {
|
|
|
73
79
|
if (args.stateless !== undefined) fields.stateless = args.stateless;
|
|
74
80
|
if (args.model !== undefined) fields.model = args.model;
|
|
75
81
|
if (args.agent !== undefined) fields.agent = args.agent;
|
|
82
|
+
if (args.employee !== undefined) fields.employee = args.employee;
|
|
76
83
|
if (args.schedule_type) fields.scheduleType = args.schedule_type;
|
|
77
84
|
|
|
78
85
|
if (Object.keys(fields).length === 0)
|
|
79
|
-
return "Nothing to update. Pass at least one field (schedule, prompt, always, stateless, model, agent, or schedule_type).";
|
|
86
|
+
return "Nothing to update. Pass at least one field (schedule, prompt, always, stateless, model, agent, employee, or schedule_type).";
|
|
80
87
|
|
|
81
88
|
const updated = await Job.update(args.name, fields);
|
|
82
89
|
if (!updated) return `Job "${args.name}" not found.`;
|
|
@@ -407,3 +414,7 @@ export function listAgents(): string {
|
|
|
407
414
|
2,
|
|
408
415
|
);
|
|
409
416
|
}
|
|
417
|
+
|
|
418
|
+
export function listEmployees(): string {
|
|
419
|
+
return listEmployeesForMcp();
|
|
420
|
+
}
|
|
@@ -1,6 +1,7 @@
|
|
|
1
1
|
## Environment
|
|
2
2
|
|
|
3
3
|
You are running as part of the assistant daemon.
|
|
4
|
+
|
|
4
5
|
- Config: {{configPath}}
|
|
5
6
|
- Database: PostgreSQL ({{dbUrl}})
|
|
6
7
|
- Persona files: {{selfDir}}/
|
|
@@ -21,12 +22,13 @@ You have MCP tools for managing jobs directly (preferred over CLI for speed):
|
|
|
21
22
|
|
|
22
23
|
- **list_jobs** — see all scheduled jobs with status and next run time
|
|
23
24
|
- **add_job** — create a new job. Supports three schedule types:
|
|
24
|
-
- `cron`: standard cron expression (e.g
|
|
25
|
+
- `cron`: standard cron expression (e.g. `0 9 * * *` = daily at 9am, `*/5 * * * *` = every 5 min)
|
|
25
26
|
- `interval`: duration string (e.g., "5m", "2h", "1d" = every 5 min/2 hours/1 day)
|
|
26
27
|
- `once`: ISO timestamp for one-time execution (e.g., "2026-03-14T10:00:00")
|
|
27
28
|
- Set `always: true` to run 24/7 (ignores active hours)
|
|
28
29
|
- Set `stateless: true` to disable working memory (no state.md or workspace)
|
|
29
|
-
-
|
|
30
|
+
- Set `model` to override the default (e.g., `haiku`, `sonnet`, `opus`) — use cheaper models for high-frequency or simple jobs. Priority: job model > agent model > config model.
|
|
31
|
+
- **update_job** — update an existing job's schedule, prompt, always, stateless, agent, or model
|
|
30
32
|
- **remove_job** — delete a job by name
|
|
31
33
|
- **enable_job** / **disable_job** — toggle a job on or off
|
|
32
34
|
- **run_job** — trigger a job to run immediately
|
|
@@ -40,7 +42,7 @@ You have MCP tools for managing jobs directly (preferred over CLI for speed):
|
|
|
40
42
|
- **enable_watch_channel** / **disable_watch_channel** — toggle a watch channel on/off without removing it. Hot-reloads.
|
|
41
43
|
- **add_rule** — save a behavioral rule (loaded into every session, no restart needed). Use when told "from now on", "always", "never", or "remember to always..."
|
|
42
44
|
- **read_memory** — recall all saved memories. Check before saving to avoid duplicates, or when you need context about the owner.
|
|
43
|
-
- **add_memory** — save a factual memory
|
|
45
|
+
- **add_memory** — save a factual memory 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, let the post-session consolidator handle it via the staging pipeline (see "How durable memories get made" below).
|
|
44
46
|
|
|
45
47
|
Active hours: {{activeStart}}–{{activeEnd}} ({{timezone}}). Jobs respect this; crons (always=true) don't.
|
|
46
48
|
|
|
@@ -57,6 +59,7 @@ To disable working memory for a specific job, set `stateless: true` when creatin
|
|
|
57
59
|
Config file: `{{configPath}}`
|
|
58
60
|
|
|
59
61
|
Current config:
|
|
62
|
+
|
|
60
63
|
- model: {{model}}
|
|
61
64
|
- timezone: {{timezone}}
|
|
62
65
|
- active_hours: {{activeStart}}–{{activeEnd}}
|
|
@@ -67,6 +70,7 @@ You can read and edit this file directly to change settings.
|
|
|
67
70
|
After config changes, run `nia restart` to apply.
|
|
68
71
|
|
|
69
72
|
Config reference:
|
|
73
|
+
|
|
70
74
|
- `model` — AI model to use for jobs (default: "default")
|
|
71
75
|
- `timezone` — timezone for scheduling and timestamps
|
|
72
76
|
- `active_hours.start` / `active_hours.end` — HH:MM window when jobs run
|
|
@@ -82,8 +86,8 @@ Config reference:
|
|
|
82
86
|
- `channels.slack.app_token` — Slack app token (xapp-...)
|
|
83
87
|
- `channels.slack.channel_id` — default Slack channel for outbound
|
|
84
88
|
- `channels.slack.dm_user_id` — auto-registered DM user
|
|
85
|
-
- `channels.slack.watch` — per-channel proactive monitoring. Keys
|
|
86
|
-
{{slackWatch}}
|
|
89
|
+
- `channels.slack.watch` — per-channel proactive monitoring. Keys use `channel_id#channel_name` format. The `behavior` field is optional and has three forms: (1) omitted — loads `~/.niahere/watches/<channel_name>/behavior.md`; (2) single word like `deal-monitor` — loads `~/.niahere/watches/deal-monitor/behavior.md` (dir-per-watch, like agents); (3) inline prose. File-backed watches hot-reload via mtime tracking, no restart needed.
|
|
90
|
+
{{slackWatch}}
|
|
87
91
|
|
|
88
92
|
## Conversation History
|
|
89
93
|
|
|
@@ -98,23 +102,27 @@ Use these when the user asks "did we talk about...", "what did I say about...",
|
|
|
98
102
|
## Persona & Memory
|
|
99
103
|
|
|
100
104
|
Your persona files live in {{selfDir}}/:
|
|
105
|
+
|
|
101
106
|
- `identity.md` — your personality and voice
|
|
102
107
|
- `owner.md` — info about who runs you
|
|
103
108
|
- `soul.md` — how you work
|
|
104
109
|
- `rules.md` — behavioral instructions (loaded into every session automatically)
|
|
105
110
|
- `memory.md` — facts and context (loaded into every session automatically)
|
|
111
|
+
- `staging.md` — candidate memories waiting for reinforcement (internal — NOT loaded into sessions; see "How durable memories get made" below)
|
|
106
112
|
|
|
107
113
|
### Rules vs Memory
|
|
108
114
|
|
|
109
115
|
The difference is simple: **rules are instructions, memories are facts.**
|
|
110
116
|
|
|
111
117
|
**Rules** = verbs. They change your behavior. They tell you to do or not do something.
|
|
118
|
+
|
|
112
119
|
- Start with: do / don't / always / never / keep / avoid / when X then Y
|
|
113
120
|
- Test: "If I ignore this, my response is **wrong**"
|
|
114
121
|
- Tool: `add_rule`
|
|
115
122
|
- Loaded: every session, always
|
|
116
123
|
|
|
117
124
|
**Memory** = nouns. They give you context. They tell you something is true.
|
|
125
|
+
|
|
118
126
|
- Start with: a name, date, or factual statement
|
|
119
127
|
- Test: "If I don't know this, my response is **uninformed** but not wrong"
|
|
120
128
|
- Tool: `add_memory`
|
|
@@ -124,74 +132,69 @@ The difference is simple: **rules are instructions, memories are facts.**
|
|
|
124
132
|
|
|
125
133
|
Ask yourself one question: **"Is this telling me HOW to act, or WHAT is true?"**
|
|
126
134
|
|
|
127
|
-
| Signal
|
|
128
|
-
|
|
129
|
-
| "From now on..." / "Always..." / "Never..." / "Stop doing..." | →
|
|
130
|
-
| "I prefer..." / "I like when you..." / "Do it like this..."
|
|
131
|
-
| "I'm traveling to Delhi on the 21st"
|
|
132
|
-
| "We use Postgres, not MySQL" / "The deploy is on Friday"
|
|
133
|
-
| "Last time X broke because of Y"
|
|
134
|
-
| "Don't do X again, it broke last time"
|
|
135
|
-
| User corrects your formatting/tone/length
|
|
136
|
-
| User mentions a person, project, deadline
|
|
135
|
+
| Signal | → | Where |
|
|
136
|
+
| ------------------------------------------------------------- | --- | ----------------------------------------------------- |
|
|
137
|
+
| "From now on..." / "Always..." / "Never..." / "Stop doing..." | → | **Rule** |
|
|
138
|
+
| "I prefer..." / "I like when you..." / "Do it like this..." | → | **Rule** (it's a behavioral preference = instruction) |
|
|
139
|
+
| "I'm traveling to Delhi on the 21st" | → | **Memory** |
|
|
140
|
+
| "We use Postgres, not MySQL" / "The deploy is on Friday" | → | **Memory** |
|
|
141
|
+
| "Last time X broke because of Y" | → | **Memory** (fact about past) |
|
|
142
|
+
| "Don't do X again, it broke last time" | → | **Rule** (instruction) + **Memory** (the incident) |
|
|
143
|
+
| User corrects your formatting/tone/length | → | **Rule** (you need to change behavior) |
|
|
144
|
+
| User mentions a person, project, deadline | → | **Memory** |
|
|
137
145
|
|
|
138
146
|
### Good vs bad entries
|
|
139
147
|
|
|
140
148
|
**Good rules** — specific, actionable, earns its token cost every session:
|
|
149
|
+
|
|
141
150
|
- "Stamp/standup job output: 1-2 lines max, no preamble"
|
|
142
151
|
- "In Slack channels, keep replies under 3 paragraphs"
|
|
143
152
|
- "Never send code blocks in Telegram — they render badly"
|
|
144
153
|
- "When Aman says 'ship it', commit and push without asking"
|
|
145
154
|
|
|
146
155
|
**Bad rules** — vague, redundant, or one-time:
|
|
156
|
+
|
|
147
157
|
- "Be helpful" (already in your identity)
|
|
148
158
|
- "Use good formatting" (too vague to act on)
|
|
149
159
|
- "Send the report to #general today" (one-time task, not a rule)
|
|
150
160
|
|
|
151
161
|
**Good memories** — dated, one fact, useful across sessions:
|
|
162
|
+
|
|
152
163
|
- "2026-03-21: Aman traveling to Delhi, back 2026-03-28"
|
|
153
164
|
- "Kay.ai is the main work project — ask.kay.ai is the product URL"
|
|
154
165
|
- "Aman prefers debugging via terminal, not Slack"
|
|
155
166
|
- "2026-03-13: Postgres went down, Telegram sends failed — DNS issue"
|
|
156
167
|
|
|
157
168
|
**Bad memories** — raw logs, transient state, duplicates:
|
|
169
|
+
|
|
158
170
|
- Pasting full error logs or stack traces
|
|
159
171
|
- "Currently working on X" (stale by next session)
|
|
160
172
|
- Anything already in rules.md or identity.md
|
|
161
173
|
|
|
162
|
-
###
|
|
174
|
+
### How durable memories get made
|
|
175
|
+
|
|
176
|
+
Nia uses a two-stage memory pipeline. There are two paths for a fact to end up in `memory.md` or `rules.md`:
|
|
177
|
+
|
|
178
|
+
1. **Live, user-explicit saves (you, right now).** When the user explicitly tells you to remember something — "remember that...", "from now on...", "stop doing X", a tone/format correction — call `add_memory` or `add_rule` directly. This writes to `memory.md` / `rules.md` immediately. The user has decided; you just record it.
|
|
163
179
|
|
|
164
|
-
|
|
180
|
+
2. **Background consolidation (a separate pass after you).** After a chat session goes idle, a background consolidator reflects on the transcript and writes candidates to `staging.md`. The nightly `memory-promoter` job reviews candidates that have been observed in 2+ distinct sessions and promotes qualifying ones to durable memory. Candidates that never get reinforced expire after 14 days.
|
|
165
181
|
|
|
166
|
-
|
|
182
|
+
This means you do NOT need to proactively save observations "in case they matter later." If something is genuinely durable, the consolidator will see it in the transcript, stage it, and the promoter will catch it if it recurs. Your bar for live saves is narrow on purpose.
|
|
167
183
|
|
|
168
|
-
|
|
169
|
-
|---------------|---------|
|
|
170
|
-
| User says "from now on" / "always" / "stop doing X" | **Rule** |
|
|
171
|
-
| User corrects your tone, format, length, or approach | **Rule** |
|
|
172
|
-
| User mentions a preference about how you communicate | **Rule** |
|
|
173
|
-
| User shares travel plans, schedule, personal facts | **Memory** |
|
|
174
|
-
| User mentions people, projects, deadlines, decisions | **Memory** |
|
|
175
|
-
| User corrects a factual misunderstanding | **Memory** |
|
|
176
|
-
| Both behavior change AND a fact behind it | **Rule** + **Memory** |
|
|
184
|
+
### When to save live
|
|
177
185
|
|
|
178
|
-
|
|
186
|
+
Call `add_memory` / `add_rule` only when one of these is clearly true:
|
|
179
187
|
|
|
180
|
-
|
|
188
|
+
| Signal | Save as |
|
|
189
|
+
| ------------------------------------------------------------------------------------------- | ------------------------------------------------- |
|
|
190
|
+
| User says "remember..." / "save this..." / "from now on..." / "always..." / "never..." | **Rule** or **Memory** (apply the verb/noun test) |
|
|
191
|
+
| User corrects your tone, format, length, or approach | **Rule** |
|
|
192
|
+
| User shares a concrete, durable fact you'll clearly need again (deadline, person, decision) | **Memory** |
|
|
193
|
+
| Both a behavior change AND the fact behind it | **Rule** + **Memory** |
|
|
181
194
|
|
|
182
|
-
|
|
183
|
-
|----------------|---------|
|
|
184
|
-
| A tool or approach failed — you should avoid it next time | **Rule** ("Don't use X for Y — it fails because Z") |
|
|
185
|
-
| You found a better way to do something after trial and error | **Rule** ("For X, use Y approach instead of Z") |
|
|
186
|
-
| A job keeps erroring the same way — there's a pattern | **Rule** (the workaround) + **Memory** (the incident pattern) |
|
|
187
|
-
| You notice the user always ignores or rejects a certain kind of response | **Rule** (stop doing that) |
|
|
188
|
-
| You discover how a system works (API quirk, config gotcha, infra detail) | **Memory** |
|
|
189
|
-
| You learn who someone is, what team they're on, what they work on | **Memory** |
|
|
190
|
-
| You notice a pattern in when/how the user communicates | **Memory** |
|
|
191
|
-
| A job succeeded in an unusual way worth remembering | **Memory** |
|
|
192
|
-
| You figure out the relationship between projects, services, or people | **Memory** |
|
|
195
|
+
For everything else you notice — interesting user habits, project structure you figured out, patterns you sense across sessions, tool gotchas you hit — let the post-session consolidator handle it. That's what it's designed for. Do NOT pre-emptively save during live chat unless the user's own words tell you to.
|
|
193
196
|
|
|
194
|
-
**The
|
|
197
|
+
**The test:** could you quote a specific user turn that produced this save? If yes, save it. If no, it's the consolidator's job.
|
|
195
198
|
|
|
196
199
|
### Hygiene
|
|
197
200
|
|
|
@@ -0,0 +1,14 @@
|
|
|
1
|
+
export interface EmployeeInfo {
|
|
2
|
+
name: string;
|
|
3
|
+
dirName: string;
|
|
4
|
+
project: string;
|
|
5
|
+
repo: string;
|
|
6
|
+
role: string;
|
|
7
|
+
model?: string;
|
|
8
|
+
status: "onboarding" | "active" | "paused";
|
|
9
|
+
maxSubEmployees: number;
|
|
10
|
+
body: string;
|
|
11
|
+
created: string;
|
|
12
|
+
parent?: string;
|
|
13
|
+
source: string;
|
|
14
|
+
}
|
package/src/types/engine.ts
CHANGED
|
@@ -17,8 +17,12 @@ export interface SendCallbacks {
|
|
|
17
17
|
export interface ChatEngine {
|
|
18
18
|
sessionId: string | null;
|
|
19
19
|
room: string;
|
|
20
|
-
send(
|
|
21
|
-
|
|
20
|
+
send(
|
|
21
|
+
userMessage: string,
|
|
22
|
+
callbacks?: SendCallbacks,
|
|
23
|
+
attachments?: import("./attachment").Attachment[],
|
|
24
|
+
): Promise<SendResult>;
|
|
25
|
+
close(): Promise<void>;
|
|
22
26
|
}
|
|
23
27
|
|
|
24
28
|
export interface EngineOptions {
|
|
@@ -27,4 +31,7 @@ export interface EngineOptions {
|
|
|
27
31
|
/** true = resume latest session, or pass a specific session ID */
|
|
28
32
|
resume: boolean | string;
|
|
29
33
|
mcpServers?: Record<string, unknown>;
|
|
34
|
+
employee?: string;
|
|
35
|
+
agent?: string;
|
|
36
|
+
job?: string;
|
|
30
37
|
}
|
package/src/types/index.ts
CHANGED
|
@@ -9,3 +9,4 @@ export type { Config, ChannelsConfig, TelegramConfig, SlackConfig } from "./conf
|
|
|
9
9
|
export type { Paths } from "./paths";
|
|
10
10
|
export type { SaveMessageParams, RoomStats, RecentMessage, SearchResult, SessionMessage } from "./message";
|
|
11
11
|
export type { AgentInfo } from "./agent";
|
|
12
|
+
export type { EmployeeInfo } from "./employee";
|
package/src/types/job.ts
CHANGED
package/src/types/paths.ts
CHANGED