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.
@@ -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
- log.error({ sessionId, room, error: output.error }, "summarizer: failed");
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
  }
@@ -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
- }
@@ -0,0 +1,7 @@
1
+ import type postgres from "postgres";
2
+
3
+ export const name = "015_jobs_employee";
4
+
5
+ export async function up(sql: postgres.Sql): Promise<void> {
6
+ await sql`ALTER TABLE jobs ADD COLUMN IF NOT EXISTS employee TEXT`;
7
+ }
@@ -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
- if (fields.schedule || fields.scheduleType) {
143
+ const scheduleChanged = fields.schedule !== undefined || fields.scheduleType !== undefined;
144
+ if (scheduleChanged) {
145
145
  validateSchedule(schedule, scheduleType);
146
146
  }
147
147
 
148
- await sql`
149
- UPDATE jobs
150
- SET schedule = ${schedule}, schedule_type = ${scheduleType}, prompt = ${prompt}, enabled = ${enabled}, always = ${always}, agent = ${agent}, model = ${model}, stateless = ${stateless}, updated_at = NOW()
151
- WHERE name = ${name}
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}`;
@@ -0,0 +1,11 @@
1
+ import { closeDb } from "./connection";
2
+ import { runMigrations } from "./migrate";
3
+
4
+ export async function withDb<T>(fn: () => Promise<T>): Promise<T> {
5
+ await runMigrations();
6
+ try {
7
+ return await fn();
8
+ } finally {
9
+ await closeDb();
10
+ }
11
+ }
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. Proactively save personal facts (travel, schedule), work context (decisions, deadlines), and corrections — don't wait to be asked. RULES: Max 300 chars. One insight per entry. NO raw logs, NO transcripts, NO status dumps.",
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., "0 9 * * *" = daily at 9am, "*/5 * * * *" = every 5 min)
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
- - **update_job**update an existing job's schedule, prompt, always, stateless, or agent
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. Proactively save personal facts, work context, corrections don't wait to be asked.
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 are `channel_id#channel_name` format.
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 | → | Where |
128
- |--------|---|-------|
129
- | "From now on..." / "Always..." / "Never..." / "Stop doing..." | → | **Rule** |
130
- | "I prefer..." / "I like when you..." / "Do it like this..." | → | **Rule** (it's a behavioral preference = instruction) |
131
- | "I'm traveling to Delhi on the 21st" | → | **Memory** |
132
- | "We use Postgres, not MySQL" / "The deploy is on Friday" | → | **Memory** |
133
- | "Last time X broke because of Y" | → | **Memory** (fact about past) |
134
- | "Don't do X again, it broke last time" | → | **Rule** (instruction) + **Memory** (the incident) |
135
- | User corrects your formatting/tone/length | → | **Rule** (you need to change behavior) |
136
- | User mentions a person, project, deadline | → | **Memory** |
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
- ### When to save (be proactive)
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
- Rules and memories don't only come from the user telling you things. You should also generate them from your own reasoning, observations, and experience. **Think of yourself as learning, not just recording.**
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
- #### From the user (explicit)
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
- | You notice... | Save as |
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
- #### From your own thinking (self-generated)
186
+ Call `add_memory` / `add_rule` only when one of these is clearly true:
179
187
 
180
- You are not a passive recorder. Reflect on your own experience and save learnings:
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
- | You realize... | Save as |
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 key principle:** if you'd want to know this at the start of your next session, save it now. Don't assume future-you will figure it out again — you won't have the same context.
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
+ }
@@ -17,8 +17,12 @@ export interface SendCallbacks {
17
17
  export interface ChatEngine {
18
18
  sessionId: string | null;
19
19
  room: string;
20
- send(userMessage: string, callbacks?: SendCallbacks, attachments?: import("./attachment").Attachment[]): Promise<SendResult>;
21
- close(): void;
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
  }
@@ -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
@@ -5,6 +5,7 @@ export interface JobInput {
5
5
  schedule: string;
6
6
  prompt: string;
7
7
  agent?: string | null;
8
+ employee?: string | null;
8
9
  model?: string | null;
9
10
  stateless?: boolean;
10
11
  }
@@ -11,4 +11,5 @@ export interface Paths {
11
11
  skillsDir: string;
12
12
  imagesDir: string;
13
13
  watchesDir: string;
14
+ employeesDir: string;
14
15
  }
@@ -21,5 +21,6 @@ export function getPaths(): Paths {
21
21
  skillsDir: resolve(home, "skills"),
22
22
  imagesDir: resolve(home, "images"),
23
23
  watchesDir: resolve(home, "watches"),
24
+ employeesDir: resolve(home, "employees"),
24
25
  };
25
26
  }