niahere 0.2.64 → 0.2.66
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/package.json +1 -1
- package/src/cli/index.ts +13 -5
- package/src/cli/job.ts +31 -11
- package/src/cli/status.ts +10 -8
- package/src/core/consolidator.ts +15 -6
- package/src/core/engine-guard.ts +98 -0
- package/src/core/scheduler.ts +2 -2
- package/src/db/migrations/016_jobs_status.ts +10 -0
- package/src/db/models/job.ts +26 -18
- package/src/mcp/server.ts +16 -0
- package/src/mcp/tools.ts +12 -2
- package/src/prompts/environment.md +18 -2
- package/src/types/enums.ts +3 -0
- package/src/types/index.ts +1 -1
package/package.json
CHANGED
package/src/cli/index.ts
CHANGED
|
@@ -16,6 +16,7 @@ import { rulesCommand, memoryCommand } from "./self";
|
|
|
16
16
|
import { watchCommand } from "./watch";
|
|
17
17
|
import { agentCommand } from "./agent";
|
|
18
18
|
import { employeeCommand } from "./employee";
|
|
19
|
+
import { guardActiveEngines, parseGuardFlags } from "../core/engine-guard";
|
|
19
20
|
|
|
20
21
|
// Set LOG_LEVEL from config before anything else logs
|
|
21
22
|
try {
|
|
@@ -112,7 +113,8 @@ switch (command) {
|
|
|
112
113
|
|
|
113
114
|
case "stop": {
|
|
114
115
|
if (!isRunning()) fail("nia is not running");
|
|
115
|
-
|
|
116
|
+
const stopGuard = parseGuardFlags(process.argv.slice(3));
|
|
117
|
+
if (!(await guardActiveEngines("stop", stopGuard))) process.exit(1);
|
|
116
118
|
const { unregisterService } = await import("../commands/service");
|
|
117
119
|
await unregisterService();
|
|
118
120
|
stopDaemon();
|
|
@@ -132,9 +134,10 @@ switch (command) {
|
|
|
132
134
|
}
|
|
133
135
|
|
|
134
136
|
case "restart": {
|
|
137
|
+
const restartGuard = parseGuardFlags(process.argv.slice(3));
|
|
138
|
+
if (!(await guardActiveEngines("restart", restartGuard))) process.exit(1);
|
|
135
139
|
const { isServiceInstalled, restartService } = await import("../commands/service");
|
|
136
140
|
if (isServiceInstalled()) {
|
|
137
|
-
// Service-aware: unload (stops KeepAlive respawn), kill, then reload
|
|
138
141
|
await restartService();
|
|
139
142
|
} else {
|
|
140
143
|
stopDaemon();
|
|
@@ -494,8 +497,13 @@ switch (command) {
|
|
|
494
497
|
}
|
|
495
498
|
|
|
496
499
|
case "update": {
|
|
500
|
+
const updateGuard = parseGuardFlags(process.argv.slice(3));
|
|
497
501
|
const { version: currentVersion } = await import("../../package.json");
|
|
498
502
|
console.log(`Current: v${currentVersion}`);
|
|
503
|
+
|
|
504
|
+
// Check active engines before doing anything destructive
|
|
505
|
+
if (isRunning() && !(await guardActiveEngines("update", updateGuard))) process.exit(1);
|
|
506
|
+
|
|
499
507
|
// Auto-backup before update
|
|
500
508
|
try {
|
|
501
509
|
const { createBackup } = await import("../commands/backup");
|
|
@@ -553,8 +561,9 @@ switch (command) {
|
|
|
553
561
|
|
|
554
562
|
Daemon:
|
|
555
563
|
start Start daemon + register service
|
|
556
|
-
stop
|
|
557
|
-
restart
|
|
564
|
+
stop [--wait N] [--force] Stop daemon (guards active engines)
|
|
565
|
+
restart [--wait N] [--force] Restart daemon
|
|
566
|
+
update [--wait N] [--force] Update to latest version
|
|
558
567
|
status [--json --rooms N --all] Show daemon, jobs, channels
|
|
559
568
|
health Check daemon, db, channels, config
|
|
560
569
|
logs [-f] [--channel ch] Daemon logs (filter by channel)
|
|
@@ -586,7 +595,6 @@ System:
|
|
|
586
595
|
backup [list] Create or list backups
|
|
587
596
|
validate Validate config.yaml
|
|
588
597
|
db <sub> Database setup/status/migrate
|
|
589
|
-
update Update to latest version
|
|
590
598
|
init Initial setup
|
|
591
599
|
test [-v] Run tests`;
|
|
592
600
|
|
package/src/cli/job.ts
CHANGED
|
@@ -39,6 +39,8 @@ Commands:
|
|
|
39
39
|
remove <name> Delete a job
|
|
40
40
|
enable <name> Enable a job
|
|
41
41
|
disable <name> Disable a job
|
|
42
|
+
archive <name> Archive a job (out of sight)
|
|
43
|
+
unarchive <name> Unarchive back to disabled
|
|
42
44
|
run <name> Run a job once
|
|
43
45
|
log [name] Show recent run history`;
|
|
44
46
|
|
|
@@ -46,7 +48,7 @@ async function pickJob(prompt = "Pick a job"): Promise<string> {
|
|
|
46
48
|
let jobs: {
|
|
47
49
|
name: string;
|
|
48
50
|
schedule: string;
|
|
49
|
-
|
|
51
|
+
status: string;
|
|
50
52
|
prompt: string;
|
|
51
53
|
}[] = [];
|
|
52
54
|
try {
|
|
@@ -68,7 +70,7 @@ async function pickJob(prompt = "Pick a job"): Promise<string> {
|
|
|
68
70
|
try {
|
|
69
71
|
const items = jobs.map((j) => ({
|
|
70
72
|
name: j.name,
|
|
71
|
-
label: `${j.
|
|
73
|
+
label: `${j.status === "active" ? "●" : "○"} ${j.name} ${j.schedule}`,
|
|
72
74
|
}));
|
|
73
75
|
const name = await pickFromList(rl, items, prompt);
|
|
74
76
|
if (!name) fail("Invalid selection.");
|
|
@@ -94,12 +96,20 @@ export async function jobCommand(): Promise<void> {
|
|
|
94
96
|
if (jobs.length === 0) {
|
|
95
97
|
console.log("No jobs configured. Use `nia job add` or `nia job import`.");
|
|
96
98
|
} else {
|
|
97
|
-
|
|
99
|
+
const visible = jobs.filter((j) => j.status !== "archived");
|
|
100
|
+
const archived = jobs.filter((j) => j.status === "archived");
|
|
101
|
+
for (const job of visible) {
|
|
102
|
+
const icon = job.status === "active" ? "●" : "○";
|
|
98
103
|
const tag = job.always ? " always" : "";
|
|
99
104
|
const type = job.scheduleType !== "cron" ? ` (${job.scheduleType})` : "";
|
|
100
105
|
const agentTag = job.agent ? ` [${job.agent}]` : "";
|
|
101
106
|
const empTag = job.employee ? ` [emp:${job.employee}]` : "";
|
|
102
|
-
console.log(` ${
|
|
107
|
+
console.log(` ${icon} ${job.name} ${job.schedule}${type}${tag}${agentTag}${empTag}`);
|
|
108
|
+
}
|
|
109
|
+
if (archived.length > 0) {
|
|
110
|
+
console.log(
|
|
111
|
+
`\n ${archived.length} archived job${archived.length > 1 ? "s" : ""} (nia job list --all to show)`,
|
|
112
|
+
);
|
|
103
113
|
}
|
|
104
114
|
}
|
|
105
115
|
});
|
|
@@ -178,15 +188,23 @@ export async function jobCommand(): Promise<void> {
|
|
|
178
188
|
}
|
|
179
189
|
|
|
180
190
|
case "enable":
|
|
181
|
-
case "disable":
|
|
191
|
+
case "disable":
|
|
192
|
+
case "archive":
|
|
193
|
+
case "unarchive": {
|
|
182
194
|
const name = process.argv[4];
|
|
183
195
|
if (!name) fail(`Usage: nia job ${subcommand} <name>`);
|
|
184
|
-
const
|
|
196
|
+
const statusMap: Record<string, string> = {
|
|
197
|
+
enable: "active",
|
|
198
|
+
disable: "disabled",
|
|
199
|
+
archive: "archived",
|
|
200
|
+
unarchive: "disabled",
|
|
201
|
+
};
|
|
202
|
+
const newStatus = statusMap[subcommand] as "active" | "disabled" | "archived";
|
|
185
203
|
|
|
186
204
|
try {
|
|
187
205
|
await withDb(async () => {
|
|
188
|
-
const updated = await Job.update(name, {
|
|
189
|
-
console.log(updated ? `Job "${name}" ${
|
|
206
|
+
const updated = await Job.update(name, { status: newStatus });
|
|
207
|
+
console.log(updated ? `Job "${name}" → ${newStatus}.` : `Job not found: ${name}`);
|
|
190
208
|
});
|
|
191
209
|
} catch (err) {
|
|
192
210
|
fail(`Failed: ${errMsg(err)}`);
|
|
@@ -280,9 +298,10 @@ export async function jobCommand(): Promise<void> {
|
|
|
280
298
|
const job = await Job.get(name);
|
|
281
299
|
if (!job) fail(`Job not found: ${name}`);
|
|
282
300
|
|
|
283
|
-
|
|
301
|
+
const icon = job.status === "active" ? "●" : job.status === "archived" ? "◌" : "○";
|
|
302
|
+
console.log(` ${icon} ${job.name}`);
|
|
284
303
|
console.log(` schedule: ${job.schedule} (${job.scheduleType})`);
|
|
285
|
-
console.log(`
|
|
304
|
+
console.log(` status: ${job.status}`);
|
|
286
305
|
console.log(` always: ${job.always}`);
|
|
287
306
|
if (job.agent) console.log(` agent: ${job.agent}`);
|
|
288
307
|
if (job.employee) console.log(` employee: ${job.employee}`);
|
|
@@ -333,7 +352,8 @@ export async function jobCommand(): Promise<void> {
|
|
|
333
352
|
? `${info.status} (${localTime(new Date(info.lastRun))}, ${formatDuration(info.duration_ms)})`
|
|
334
353
|
: "never run";
|
|
335
354
|
const tag = job.always ? " always" : "";
|
|
336
|
-
|
|
355
|
+
const icon = job.status === "active" ? "●" : job.status === "archived" ? "◌" : "○";
|
|
356
|
+
console.log(` ${icon} ${job.name} [${job.schedule}]${tag} ${status}`);
|
|
337
357
|
if (info?.error) console.log(` error: ${info.error}`);
|
|
338
358
|
});
|
|
339
359
|
} catch (err) {
|
package/src/cli/status.ts
CHANGED
|
@@ -20,7 +20,7 @@ type StatusOptions = {
|
|
|
20
20
|
type JobStatusLine = {
|
|
21
21
|
name: string;
|
|
22
22
|
schedule: string;
|
|
23
|
-
|
|
23
|
+
jobStatus: string;
|
|
24
24
|
always: boolean;
|
|
25
25
|
scheduleType: ScheduleType;
|
|
26
26
|
agent: string | null;
|
|
@@ -111,7 +111,7 @@ export async function statusCommand(argv: string[] = []): Promise<void> {
|
|
|
111
111
|
if (options.json) {
|
|
112
112
|
const sortedJobs = [...jobs].sort(
|
|
113
113
|
(a, b) =>
|
|
114
|
-
|
|
114
|
+
(b.status === "active" ? 1 : 0) - (a.status === "active" ? 1 : 0) ||
|
|
115
115
|
dateSortValue(a.nextRunAt) - dateSortValue(b.nextRunAt) ||
|
|
116
116
|
a.name.localeCompare(b.name),
|
|
117
117
|
);
|
|
@@ -122,7 +122,7 @@ export async function statusCommand(argv: string[] = []): Promise<void> {
|
|
|
122
122
|
return {
|
|
123
123
|
name: job.name,
|
|
124
124
|
schedule: job.schedule,
|
|
125
|
-
|
|
125
|
+
jobStatus: job.status,
|
|
126
126
|
always: job.always,
|
|
127
127
|
scheduleType: job.scheduleType,
|
|
128
128
|
agent: job.agent,
|
|
@@ -144,7 +144,7 @@ export async function statusCommand(argv: string[] = []): Promise<void> {
|
|
|
144
144
|
return {
|
|
145
145
|
name,
|
|
146
146
|
schedule: "unavailable",
|
|
147
|
-
|
|
147
|
+
jobStatus: "disabled",
|
|
148
148
|
always: false,
|
|
149
149
|
scheduleType: "cron",
|
|
150
150
|
agent: null,
|
|
@@ -232,13 +232,15 @@ export async function statusCommand(argv: string[] = []): Promise<void> {
|
|
|
232
232
|
console.log("\nJobs:");
|
|
233
233
|
const sortedJobs = [...jobs].sort(
|
|
234
234
|
(a, b) =>
|
|
235
|
-
|
|
235
|
+
(b.status === "active" ? 1 : 0) - (a.status === "active" ? 1 : 0) ||
|
|
236
236
|
dateSortValue(a.nextRunAt) - dateSortValue(b.nextRunAt) ||
|
|
237
237
|
a.name.localeCompare(b.name),
|
|
238
238
|
);
|
|
239
239
|
|
|
240
240
|
// Hide completed one-shot jobs
|
|
241
|
-
const visibleJobs = sortedJobs.filter(
|
|
241
|
+
const visibleJobs = sortedJobs.filter(
|
|
242
|
+
(j) => j.status !== "archived" && !(j.scheduleType === "once" && j.status !== "active" && j.lastRunAt),
|
|
243
|
+
);
|
|
242
244
|
|
|
243
245
|
for (const job of visibleJobs) {
|
|
244
246
|
const stateInfo = state[job.name];
|
|
@@ -246,7 +248,7 @@ export async function statusCommand(argv: string[] = []): Promise<void> {
|
|
|
246
248
|
const lastRun = stateInfo?.lastRun ?? job.lastRunAt ?? null;
|
|
247
249
|
const nextRun = job.nextRunAt ?? null;
|
|
248
250
|
const stale =
|
|
249
|
-
job.
|
|
251
|
+
job.status === "active" &&
|
|
250
252
|
status !== "running" &&
|
|
251
253
|
nextRun !== null &&
|
|
252
254
|
safeDate(nextRun) !== null &&
|
|
@@ -263,7 +265,7 @@ export async function statusCommand(argv: string[] = []): Promise<void> {
|
|
|
263
265
|
const agentTag = job.agent ? ` [${job.agent}]` : "";
|
|
264
266
|
const empTag = job.employee ? ` [emp:${job.employee}]` : "";
|
|
265
267
|
console.log(
|
|
266
|
-
` ${job.
|
|
268
|
+
` ${job.status === "active" ? "\u25cf" : "\u25cb"} ${job.name.padEnd(20)} ${job.status}${agentTag}${empTag}`,
|
|
267
269
|
);
|
|
268
270
|
console.log(
|
|
269
271
|
` ${statusIcon} ${status} last: ${lastText} next: ${nextText} duration: ${durationText}${staleText}`,
|
package/src/core/consolidator.ts
CHANGED
|
@@ -58,8 +58,9 @@ Nia uses a two-stage memory architecture. You are stage 1.
|
|
|
58
58
|
|
|
59
59
|
Your persona includes guidance to "save proactively" — that guidance applies
|
|
60
60
|
to LIVE chat, where you act on immediate user instruction. In THIS
|
|
61
|
-
consolidation pass,
|
|
62
|
-
|
|
61
|
+
consolidation pass, be selective but not paralyzed. If you see a genuine
|
|
62
|
+
learning, stage it. The promoter handles quality gating — your job is to
|
|
63
|
+
not miss real signals, not to be maximally conservative.
|
|
63
64
|
|
|
64
65
|
## Transcript
|
|
65
66
|
|
|
@@ -81,13 +82,20 @@ Answer these questions silently. If the answer to all of them is "nothing",
|
|
|
81
82
|
stop here and do not write anything.
|
|
82
83
|
|
|
83
84
|
1. What did the user correct, clarify, or teach you in this session?
|
|
85
|
+
(Includes: "no, do it this way", "don't use X", "always check Y first")
|
|
84
86
|
2. What NEW fact about the user, their projects, or their systems do you
|
|
85
87
|
now know that you did not at session start?
|
|
88
|
+
(Includes: architecture decisions, workflow patterns, tool preferences,
|
|
89
|
+
team structure, external system details discovered during task execution)
|
|
86
90
|
3. What decision was made that will constrain future work?
|
|
91
|
+
(Includes: "we're using X not Y", config changes, deployment patterns)
|
|
92
|
+
4. What did the user explicitly ask to be remembered?
|
|
87
93
|
|
|
88
|
-
Trivial small talk, greetings,
|
|
89
|
-
|
|
90
|
-
|
|
94
|
+
Trivial small talk, greetings, and pure status updates are NOT answers.
|
|
95
|
+
But corrections made DURING task execution ("no, check DynamoDB not S3"),
|
|
96
|
+
architecture learned while debugging ("ah, this service talks to X via Y"),
|
|
97
|
+
and workflow patterns revealed by how the user works — these ARE answers.
|
|
98
|
+
The bar is: would a fresh Nia session benefit from knowing this?
|
|
91
99
|
|
|
92
100
|
## Step 3 — Update staging.md
|
|
93
101
|
|
|
@@ -118,7 +126,8 @@ For each substantive answer:
|
|
|
118
126
|
- Do NOT write to \`memory.md\` or \`rules.md\`. Only the promoter job can.
|
|
119
127
|
- Do NOT use \`add_memory\` or \`add_rule\` MCP tools. Edit staging.md directly.
|
|
120
128
|
- Do NOT message the user.
|
|
121
|
-
-
|
|
129
|
+
- If nothing qualifies, do nothing. But don't be so conservative that the
|
|
130
|
+
pipeline starves — if you're skipping every session, your bar is too high.
|
|
122
131
|
|
|
123
132
|
Report a one-line summary of what you did: "staged N new / reinforced M /
|
|
124
133
|
skipped (trivial session)". No preamble.`;
|
|
@@ -0,0 +1,98 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Guard against stopping/restarting while active engines are running.
|
|
3
|
+
*
|
|
4
|
+
* Default: warn and refuse.
|
|
5
|
+
* --wait <minutes>: poll until engines clear, then proceed. Times out with error.
|
|
6
|
+
* --force: skip the check entirely.
|
|
7
|
+
*/
|
|
8
|
+
|
|
9
|
+
import { ActiveEngine } from "../db/models";
|
|
10
|
+
import { withDb } from "../db/with-db";
|
|
11
|
+
import { DIM, RESET, ICON_WARN } from "../utils/cli";
|
|
12
|
+
|
|
13
|
+
export interface GuardOptions {
|
|
14
|
+
/** Wait up to this many minutes for engines to clear. 0 = don't wait (default). */
|
|
15
|
+
waitMinutes: number;
|
|
16
|
+
/** Skip the guard entirely. */
|
|
17
|
+
force: boolean;
|
|
18
|
+
}
|
|
19
|
+
|
|
20
|
+
export function parseGuardFlags(args: string[]): GuardOptions {
|
|
21
|
+
const force = args.includes("--force") || args.includes("-f");
|
|
22
|
+
|
|
23
|
+
let waitMinutes = 0;
|
|
24
|
+
const waitIdx = args.indexOf("--wait");
|
|
25
|
+
if (waitIdx !== -1 && args[waitIdx + 1]) {
|
|
26
|
+
const parsed = parseInt(args[waitIdx + 1], 10);
|
|
27
|
+
if (!isNaN(parsed) && parsed > 0) waitMinutes = parsed;
|
|
28
|
+
}
|
|
29
|
+
|
|
30
|
+
return { waitMinutes, force };
|
|
31
|
+
}
|
|
32
|
+
|
|
33
|
+
interface ActiveSummary {
|
|
34
|
+
count: number;
|
|
35
|
+
rooms: string[];
|
|
36
|
+
}
|
|
37
|
+
|
|
38
|
+
async function getActiveEngines(): Promise<ActiveSummary> {
|
|
39
|
+
let count = 0;
|
|
40
|
+
let rooms: string[] = [];
|
|
41
|
+
try {
|
|
42
|
+
await withDb(async () => {
|
|
43
|
+
const engines = await ActiveEngine.list();
|
|
44
|
+
count = engines.length;
|
|
45
|
+
rooms = engines.map((e) => `${e.room} (${e.channel})`);
|
|
46
|
+
});
|
|
47
|
+
} catch {
|
|
48
|
+
// DB unreachable — no engines to worry about
|
|
49
|
+
}
|
|
50
|
+
return { count, rooms };
|
|
51
|
+
}
|
|
52
|
+
|
|
53
|
+
/**
|
|
54
|
+
* Check for active engines before a destructive operation.
|
|
55
|
+
* Returns true if safe to proceed, false if blocked.
|
|
56
|
+
*/
|
|
57
|
+
export async function guardActiveEngines(action: string, opts: GuardOptions): Promise<boolean> {
|
|
58
|
+
if (opts.force) return true;
|
|
59
|
+
|
|
60
|
+
const { count, rooms } = await getActiveEngines();
|
|
61
|
+
if (count === 0) return true;
|
|
62
|
+
|
|
63
|
+
// Active engines found
|
|
64
|
+
console.log(`\n${ICON_WARN} ${count} active engine${count > 1 ? "s" : ""} running:`);
|
|
65
|
+
for (const room of rooms) {
|
|
66
|
+
console.log(` ${DIM}${room}${RESET}`);
|
|
67
|
+
}
|
|
68
|
+
|
|
69
|
+
if (opts.waitMinutes === 0) {
|
|
70
|
+
// Default: refuse
|
|
71
|
+
console.log(`\nCannot ${action} while engines are active.`);
|
|
72
|
+
console.log(`${DIM}Options:${RESET}`);
|
|
73
|
+
console.log(` --wait <minutes> Wait for engines to finish (checks every 5s)`);
|
|
74
|
+
console.log(` --force ${action} immediately, killing active sessions`);
|
|
75
|
+
return false;
|
|
76
|
+
}
|
|
77
|
+
|
|
78
|
+
// --wait: poll until clear or timeout
|
|
79
|
+
const deadlineMs = opts.waitMinutes * 60 * 1000;
|
|
80
|
+
const deadline = Date.now() + deadlineMs;
|
|
81
|
+
console.log(`\nWaiting up to ${opts.waitMinutes}m for engines to finish...`);
|
|
82
|
+
|
|
83
|
+
while (Date.now() < deadline) {
|
|
84
|
+
await new Promise((r) => setTimeout(r, 5_000));
|
|
85
|
+
const { count: remaining } = await getActiveEngines();
|
|
86
|
+
if (remaining === 0) {
|
|
87
|
+
console.log("All engines finished.");
|
|
88
|
+
return true;
|
|
89
|
+
}
|
|
90
|
+
const left = Math.ceil((deadline - Date.now()) / 1000);
|
|
91
|
+
process.stdout.write(`\r${DIM} ${remaining} active, ${left}s remaining${RESET}`);
|
|
92
|
+
}
|
|
93
|
+
|
|
94
|
+
process.stdout.write("\n");
|
|
95
|
+
console.log(`\nTimed out — ${count} engine${count > 1 ? "s" : ""} still active.`);
|
|
96
|
+
console.log(`Use --force to ${action} anyway.`);
|
|
97
|
+
return false;
|
|
98
|
+
}
|
package/src/core/scheduler.ts
CHANGED
|
@@ -72,7 +72,7 @@ async function tick(): Promise<void> {
|
|
|
72
72
|
nextRun = computeNextRun(job.scheduleType, job.schedule, config.timezone, new Date());
|
|
73
73
|
} catch (err) {
|
|
74
74
|
log.error({ err, job: job.name, schedule: job.schedule }, "scheduler: invalid schedule, disabling job");
|
|
75
|
-
await Job.update(job.name, {
|
|
75
|
+
await Job.update(job.name, { status: "disabled" }).catch(() => {});
|
|
76
76
|
continue;
|
|
77
77
|
}
|
|
78
78
|
await Job.markRun(job.name, nextRun).catch((err) => {
|
|
@@ -81,7 +81,7 @@ async function tick(): Promise<void> {
|
|
|
81
81
|
|
|
82
82
|
// Auto-disable one-shot jobs after execution
|
|
83
83
|
if (job.scheduleType === "once") {
|
|
84
|
-
await Job.update(job.name, {
|
|
84
|
+
await Job.update(job.name, { status: "disabled" }).catch(() => {});
|
|
85
85
|
log.info({ job: job.name }, "scheduler: one-shot job completed, auto-disabled");
|
|
86
86
|
}
|
|
87
87
|
}
|
|
@@ -0,0 +1,10 @@
|
|
|
1
|
+
import type postgres from "postgres";
|
|
2
|
+
|
|
3
|
+
export const name = "016_jobs_status";
|
|
4
|
+
|
|
5
|
+
export async function up(sql: postgres.Sql): Promise<void> {
|
|
6
|
+
// Add status column: active | disabled | archived
|
|
7
|
+
await sql`ALTER TABLE jobs ADD COLUMN IF NOT EXISTS status TEXT NOT NULL DEFAULT 'active'`;
|
|
8
|
+
// Backfill from enabled boolean
|
|
9
|
+
await sql`UPDATE jobs SET status = CASE WHEN enabled THEN 'active' ELSE 'disabled' END WHERE status = 'active'`;
|
|
10
|
+
}
|
package/src/db/models/job.ts
CHANGED
|
@@ -3,7 +3,7 @@ import { CronExpressionParser } from "cron-parser";
|
|
|
3
3
|
import { parseDuration } from "../../utils/duration";
|
|
4
4
|
import { computeInitialNextRun } from "../../utils/schedule";
|
|
5
5
|
import { getConfig } from "../../utils/config";
|
|
6
|
-
import type { ScheduleType } from "../../types";
|
|
6
|
+
import type { ScheduleType, JobLifecycle } from "../../types";
|
|
7
7
|
|
|
8
8
|
/** Validate that a schedule string matches its declared type. Throws on mismatch. */
|
|
9
9
|
function validateSchedule(schedule: string, scheduleType: ScheduleType): void {
|
|
@@ -36,7 +36,7 @@ export interface Job {
|
|
|
36
36
|
name: string;
|
|
37
37
|
schedule: string;
|
|
38
38
|
prompt: string;
|
|
39
|
-
|
|
39
|
+
status: JobLifecycle;
|
|
40
40
|
always: boolean;
|
|
41
41
|
scheduleType: ScheduleType;
|
|
42
42
|
agent: string | null;
|
|
@@ -49,12 +49,23 @@ export interface Job {
|
|
|
49
49
|
updatedAt: string;
|
|
50
50
|
}
|
|
51
51
|
|
|
52
|
+
const COLS =
|
|
53
|
+
"name, schedule, prompt, status, always, schedule_type, agent, employee, model, stateless, next_run_at, last_run_at, created_at, updated_at";
|
|
54
|
+
|
|
52
55
|
function toJob(r: Record<string, any>): Job {
|
|
56
|
+
// Support both old (enabled boolean) and new (status text) schema
|
|
57
|
+
let status: JobLifecycle;
|
|
58
|
+
if (typeof r.status === "string" && ["active", "disabled", "archived"].includes(r.status)) {
|
|
59
|
+
status = r.status as JobLifecycle;
|
|
60
|
+
} else {
|
|
61
|
+
status = r.enabled ? "active" : "disabled";
|
|
62
|
+
}
|
|
63
|
+
|
|
53
64
|
return {
|
|
54
65
|
name: r.name,
|
|
55
66
|
schedule: r.schedule,
|
|
56
67
|
prompt: r.prompt,
|
|
57
|
-
|
|
68
|
+
status,
|
|
58
69
|
always: r.always ?? false,
|
|
59
70
|
scheduleType: r.schedule_type || "cron",
|
|
60
71
|
agent: r.agent || null,
|
|
@@ -92,23 +103,21 @@ export async function create(
|
|
|
92
103
|
}
|
|
93
104
|
const sql = getSql();
|
|
94
105
|
await sql`
|
|
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})
|
|
106
|
+
INSERT INTO jobs (name, schedule, prompt, always, schedule_type, next_run_at, agent, stateless, model, employee, status)
|
|
107
|
+
VALUES (${name}, ${schedule}, ${prompt}, ${always}, ${scheduleType}, ${nextRunAt ?? null}, ${agent ?? null}, ${stateless}, ${model ?? null}, ${employee ?? null}, 'active')
|
|
97
108
|
`;
|
|
98
109
|
await notifyChange();
|
|
99
110
|
}
|
|
100
111
|
|
|
101
112
|
export async function list(): Promise<Job[]> {
|
|
102
113
|
const sql = getSql();
|
|
103
|
-
const rows =
|
|
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`;
|
|
114
|
+
const rows = await sql`SELECT ${sql.unsafe(COLS)} FROM jobs ORDER BY name`;
|
|
105
115
|
return rows.map(toJob);
|
|
106
116
|
}
|
|
107
117
|
|
|
108
118
|
export async function get(name: string): Promise<Job | null> {
|
|
109
119
|
const sql = getSql();
|
|
110
|
-
const rows =
|
|
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}`;
|
|
120
|
+
const rows = await sql`SELECT ${sql.unsafe(COLS)} FROM jobs WHERE name = ${name}`;
|
|
112
121
|
return rows.length > 0 ? toJob(rows[0]) : null;
|
|
113
122
|
}
|
|
114
123
|
|
|
@@ -117,7 +126,7 @@ export async function update(
|
|
|
117
126
|
fields: Partial<{
|
|
118
127
|
schedule: string;
|
|
119
128
|
prompt: string;
|
|
120
|
-
|
|
129
|
+
status: JobLifecycle;
|
|
121
130
|
always: boolean;
|
|
122
131
|
agent: string | null;
|
|
123
132
|
employee: string | null;
|
|
@@ -133,7 +142,7 @@ export async function update(
|
|
|
133
142
|
const schedule = fields.schedule ?? existing.schedule;
|
|
134
143
|
const scheduleType = fields.scheduleType ?? existing.scheduleType;
|
|
135
144
|
const prompt = fields.prompt ?? existing.prompt;
|
|
136
|
-
const
|
|
145
|
+
const status = fields.status ?? existing.status;
|
|
137
146
|
const always = fields.always ?? existing.always;
|
|
138
147
|
const agent = fields.agent !== undefined ? fields.agent : existing.agent;
|
|
139
148
|
const employee = fields.employee !== undefined ? fields.employee : existing.employee;
|
|
@@ -149,13 +158,13 @@ export async function update(
|
|
|
149
158
|
const nextRun = computeInitialNextRun(scheduleType, schedule, getConfig().timezone);
|
|
150
159
|
await sql`
|
|
151
160
|
UPDATE jobs
|
|
152
|
-
SET schedule = ${schedule}, schedule_type = ${scheduleType}, prompt = ${prompt},
|
|
161
|
+
SET schedule = ${schedule}, schedule_type = ${scheduleType}, prompt = ${prompt}, status = ${status}, always = ${always}, agent = ${agent}, employee = ${employee}, model = ${model}, stateless = ${stateless}, next_run_at = ${nextRun}, updated_at = NOW()
|
|
153
162
|
WHERE name = ${name}
|
|
154
163
|
`;
|
|
155
164
|
} else {
|
|
156
165
|
await sql`
|
|
157
166
|
UPDATE jobs
|
|
158
|
-
SET schedule = ${schedule}, schedule_type = ${scheduleType}, prompt = ${prompt},
|
|
167
|
+
SET schedule = ${schedule}, schedule_type = ${scheduleType}, prompt = ${prompt}, status = ${status}, always = ${always}, agent = ${agent}, employee = ${employee}, model = ${model}, stateless = ${stateless}, updated_at = NOW()
|
|
159
168
|
WHERE name = ${name}
|
|
160
169
|
`;
|
|
161
170
|
}
|
|
@@ -172,17 +181,16 @@ export async function remove(name: string): Promise<boolean> {
|
|
|
172
181
|
|
|
173
182
|
export async function listEnabled(): Promise<Job[]> {
|
|
174
183
|
const sql = getSql();
|
|
175
|
-
const rows =
|
|
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`;
|
|
184
|
+
const rows = await sql`SELECT ${sql.unsafe(COLS)} FROM jobs WHERE status = 'active' ORDER BY name`;
|
|
177
185
|
return rows.map(toJob);
|
|
178
186
|
}
|
|
179
187
|
|
|
180
188
|
export async function listDue(): Promise<Job[]> {
|
|
181
189
|
const sql = getSql();
|
|
182
190
|
const rows = await sql`
|
|
183
|
-
SELECT
|
|
191
|
+
SELECT ${sql.unsafe(COLS)}
|
|
184
192
|
FROM jobs
|
|
185
|
-
WHERE
|
|
193
|
+
WHERE status = 'active' AND next_run_at <= NOW()
|
|
186
194
|
ORDER BY next_run_at
|
|
187
195
|
`;
|
|
188
196
|
return rows.map(toJob);
|
|
@@ -193,7 +201,7 @@ export async function markRun(name: string, nextRunAt: Date | null): Promise<voi
|
|
|
193
201
|
if (nextRunAt) {
|
|
194
202
|
await sql`UPDATE jobs SET last_run_at = NOW(), next_run_at = ${nextRunAt}, updated_at = NOW() WHERE name = ${name}`;
|
|
195
203
|
} else {
|
|
196
|
-
await sql`UPDATE jobs SET last_run_at = NOW(),
|
|
204
|
+
await sql`UPDATE jobs SET last_run_at = NOW(), status = 'disabled', updated_at = NOW() WHERE name = ${name}`;
|
|
197
205
|
}
|
|
198
206
|
await notifyChange();
|
|
199
207
|
}
|
package/src/mcp/server.ts
CHANGED
|
@@ -106,6 +106,22 @@ export function createNiaMcpServer() {
|
|
|
106
106
|
],
|
|
107
107
|
}),
|
|
108
108
|
),
|
|
109
|
+
tool(
|
|
110
|
+
"archive_job",
|
|
111
|
+
"Archive a job (out of sight, won't run). Use unarchive_job to bring it back.",
|
|
112
|
+
{ name: z.string().describe("Job name to archive") },
|
|
113
|
+
async (args) => ({
|
|
114
|
+
content: [{ type: "text" as const, text: await handlers.archiveJob(args.name) }],
|
|
115
|
+
}),
|
|
116
|
+
),
|
|
117
|
+
tool(
|
|
118
|
+
"unarchive_job",
|
|
119
|
+
"Unarchive a job back to disabled state. Use enable_job after to start running it.",
|
|
120
|
+
{ name: z.string().describe("Job name to unarchive") },
|
|
121
|
+
async (args) => ({
|
|
122
|
+
content: [{ type: "text" as const, text: await handlers.unarchiveJob(args.name) }],
|
|
123
|
+
}),
|
|
124
|
+
),
|
|
109
125
|
tool(
|
|
110
126
|
"run_job",
|
|
111
127
|
"Trigger a job to run immediately on the next scheduler tick",
|
package/src/mcp/tools.ts
CHANGED
|
@@ -96,7 +96,7 @@ export async function removeJob(name: string): Promise<string> {
|
|
|
96
96
|
}
|
|
97
97
|
|
|
98
98
|
export async function enableJob(name: string): Promise<string> {
|
|
99
|
-
const updated = await Job.update(name, {
|
|
99
|
+
const updated = await Job.update(name, { status: "active" });
|
|
100
100
|
if (!updated) return `Job "${name}" not found.`;
|
|
101
101
|
|
|
102
102
|
const job = await Job.get(name);
|
|
@@ -110,10 +110,20 @@ export async function enableJob(name: string): Promise<string> {
|
|
|
110
110
|
}
|
|
111
111
|
|
|
112
112
|
export async function disableJob(name: string): Promise<string> {
|
|
113
|
-
const updated = await Job.update(name, {
|
|
113
|
+
const updated = await Job.update(name, { status: "disabled" });
|
|
114
114
|
return updated ? `Job "${name}" disabled.` : `Job "${name}" not found.`;
|
|
115
115
|
}
|
|
116
116
|
|
|
117
|
+
export async function archiveJob(name: string): Promise<string> {
|
|
118
|
+
const updated = await Job.update(name, { status: "archived" });
|
|
119
|
+
return updated ? `Job "${name}" archived.` : `Job "${name}" not found.`;
|
|
120
|
+
}
|
|
121
|
+
|
|
122
|
+
export async function unarchiveJob(name: string): Promise<string> {
|
|
123
|
+
const updated = await Job.update(name, { status: "disabled" });
|
|
124
|
+
return updated ? `Job "${name}" unarchived (disabled). Enable with enable_job.` : `Job "${name}" not found.`;
|
|
125
|
+
}
|
|
126
|
+
|
|
117
127
|
export async function runJobNow(name: string): Promise<string> {
|
|
118
128
|
const job = await Job.get(name);
|
|
119
129
|
if (!job) return `Job "${name}" not found.`;
|
|
@@ -20,7 +20,7 @@ Prefer MCP tools for job/message management (faster, no subprocess overhead), bu
|
|
|
20
20
|
|
|
21
21
|
You have MCP tools for managing jobs directly (preferred over CLI for speed):
|
|
22
22
|
|
|
23
|
-
- **list_jobs** — see all scheduled jobs with status and next run time
|
|
23
|
+
- **list_jobs** — see all scheduled jobs with status and next run time. Jobs have three statuses: `active` (running on schedule), `disabled` (paused but visible), `archived` (hidden from list, won't run — use `nia job archive/unarchive` or MCP tools)
|
|
24
24
|
- **add_job** — create a new job. Supports three schedule types:
|
|
25
25
|
- `cron`: standard cron expression (e.g. `0 9 * * *` = daily at 9am, `*/5 * * * *` = every 5 min)
|
|
26
26
|
- `interval`: duration string (e.g., "5m", "2h", "1d" = every 5 min/2 hours/1 day)
|
|
@@ -28,10 +28,14 @@ You have MCP tools for managing jobs directly (preferred over CLI for speed):
|
|
|
28
28
|
- Set `always: true` to run 24/7 (ignores active hours)
|
|
29
29
|
- Set `stateless: true` to disable working memory (no state.md or workspace)
|
|
30
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
|
-
-
|
|
31
|
+
- Set `employee` to assign the job to an employee (employee identity takes precedence over agent)
|
|
32
|
+
- **update_job** — update an existing job's schedule, prompt, always, stateless, agent, model, or employee
|
|
32
33
|
- **remove_job** — delete a job by name
|
|
33
34
|
- **enable_job** / **disable_job** — toggle a job on or off
|
|
35
|
+
- **archive_job** — archive a job (hidden from list, won't run)
|
|
36
|
+
- **unarchive_job** — unarchive a job back to disabled state
|
|
34
37
|
- **run_job** — trigger a job to run immediately
|
|
38
|
+
- **list_employees** — list all employees with role, project, status
|
|
35
39
|
- **send_message** — send a message to the user (via telegram, slack, or default channel). Supports `media_path` to send images/files.
|
|
36
40
|
- **list_messages** — read recent chat history
|
|
37
41
|
- **list_sessions** — browse past conversation sessions with previews and message counts. Returns session IDs.
|
|
@@ -89,6 +93,18 @@ Config reference:
|
|
|
89
93
|
- `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
94
|
{{slackWatch}}
|
|
91
95
|
|
|
96
|
+
## Employees
|
|
97
|
+
|
|
98
|
+
Employees are persistent co-founders scoped to projects — not just role prompts like agents, but full identities with their own memory, goals, decisions, and org chart position.
|
|
99
|
+
|
|
100
|
+
Each employee lives in `~/.niahere/employees/<name>/` and has an `EMPLOYEE.md` identity file plus working memory. Employee identity takes precedence over agent identity when both are present.
|
|
101
|
+
|
|
102
|
+
CLI: `nia employee add|list|show|pause|resume|remove|approvals`
|
|
103
|
+
Chat: `nia chat --employee <name>` or `nia employee <name>`
|
|
104
|
+
Jobs: assign via `--employee` flag or `employee` parameter in MCP tools
|
|
105
|
+
|
|
106
|
+
Use `list_employees` to see all employees with their role, project, and status.
|
|
107
|
+
|
|
92
108
|
## Conversation History
|
|
93
109
|
|
|
94
110
|
You have access to all prior conversations stored in the database:
|
package/src/types/enums.ts
CHANGED
|
@@ -4,6 +4,9 @@ export type JobStatus = "ok" | "error";
|
|
|
4
4
|
/** Status of a job in the cron state (includes running). */
|
|
5
5
|
export type JobStateStatus = "ok" | "error" | "running";
|
|
6
6
|
|
|
7
|
+
/** Lifecycle state of a job. */
|
|
8
|
+
export type JobLifecycle = "active" | "disabled" | "archived";
|
|
9
|
+
|
|
7
10
|
/** Schedule type for jobs. */
|
|
8
11
|
export type ScheduleType = "cron" | "interval" | "once";
|
|
9
12
|
|
package/src/types/index.ts
CHANGED
|
@@ -1,5 +1,5 @@
|
|
|
1
1
|
export type { Attachment } from "./attachment";
|
|
2
|
-
export type { JobStatus, JobStateStatus, ScheduleType, Mode, AttachmentType, ChannelName } from "./enums";
|
|
2
|
+
export type { JobStatus, JobStateStatus, JobLifecycle, ScheduleType, Mode, AttachmentType, ChannelName } from "./enums";
|
|
3
3
|
export type { JobInput, JobResult } from "./job";
|
|
4
4
|
export type { SendResult, StreamCallback, ActivityCallback, SendCallbacks, ChatEngine, EngineOptions } from "./engine";
|
|
5
5
|
export type { AuditEntry, JobState, CronState } from "./audit";
|