@oharness/engine 0.5.1 → 0.7.0
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/dist/agent/agent.d.ts +12 -0
- package/dist/agent/agent.js +3 -0
- package/dist/agent/agent.js.map +1 -1
- package/dist/agent/memory-backup.d.ts +2 -0
- package/dist/agent/memory-backup.js +9 -1
- package/dist/agent/memory-backup.js.map +1 -1
- package/dist/agent/memory-limits.js +8 -1
- package/dist/agent/memory-limits.js.map +1 -1
- package/dist/agent/memory-usage.d.ts +1 -1
- package/dist/agent/memory-usage.js +2 -3
- package/dist/agent/memory-usage.js.map +1 -1
- package/dist/agent/memory.d.ts +3 -0
- package/dist/agent/memory.js +6 -18
- package/dist/agent/memory.js.map +1 -1
- package/dist/agent/session.d.ts +9 -0
- package/dist/agent/session.js +14 -2
- package/dist/agent/session.js.map +1 -1
- package/dist/agent/settings.js +3 -2
- package/dist/agent/settings.js.map +1 -1
- package/dist/agent/store.d.ts +45 -4
- package/dist/agent/store.js +198 -25
- package/dist/agent/store.js.map +1 -1
- package/dist/artifacts/files.d.ts +52 -0
- package/dist/artifacts/files.js +169 -0
- package/dist/artifacts/files.js.map +1 -0
- package/dist/auth/store.js +3 -2
- package/dist/auth/store.js.map +1 -1
- package/dist/config-files.js +3 -2
- package/dist/config-files.js.map +1 -1
- package/dist/config.d.ts +6 -1
- package/dist/config.js +10 -2
- package/dist/config.js.map +1 -1
- package/dist/harness.d.ts +62 -1
- package/dist/harness.js +174 -12
- package/dist/harness.js.map +1 -1
- package/dist/paths.d.ts +5 -0
- package/dist/paths.js +21 -3
- package/dist/paths.js.map +1 -1
- package/dist/providers/builtin.d.ts +2 -0
- package/dist/providers/builtin.js +38 -24
- package/dist/providers/builtin.js.map +1 -1
- package/dist/providers/catalog.d.ts +5 -0
- package/dist/providers/catalog.js +32 -1
- package/dist/providers/catalog.js.map +1 -1
- package/dist/providers/catalog.json +241 -123
- package/dist/providers/discovery.js +17 -0
- package/dist/providers/discovery.js.map +1 -1
- package/dist/providers/models-dev.d.ts +89 -0
- package/dist/providers/models-dev.js +160 -0
- package/dist/providers/models-dev.js.map +1 -0
- package/dist/providers/models-dev.json +821 -0
- package/dist/providers/registry.d.ts +12 -0
- package/dist/providers/registry.js +33 -1
- package/dist/providers/registry.js.map +1 -1
- package/dist/providers/types.d.ts +21 -0
- package/dist/providers/types.js +16 -0
- package/dist/providers/types.js.map +1 -1
- package/dist/runtime-plugins/builtin/memory-migrate.d.ts +5 -21
- package/dist/runtime-plugins/builtin/memory-migrate.js +5 -125
- package/dist/runtime-plugins/builtin/memory-migrate.js.map +1 -1
- package/dist/runtime-plugins/builtin/memory.d.ts +3 -0
- package/dist/runtime-plugins/builtin/memory.js +12 -10
- package/dist/runtime-plugins/builtin/memory.js.map +1 -1
- package/dist/schedule/clock.d.ts +76 -0
- package/dist/schedule/clock.js +147 -0
- package/dist/schedule/clock.js.map +1 -0
- package/dist/schedule/cron.d.ts +9 -0
- package/dist/schedule/cron.js +18 -0
- package/dist/schedule/cron.js.map +1 -1
- package/dist/schedule/lock.d.ts +38 -0
- package/dist/schedule/lock.js +100 -0
- package/dist/schedule/lock.js.map +1 -0
- package/dist/schedule/runner.d.ts +67 -0
- package/dist/schedule/runner.js +100 -0
- package/dist/schedule/runner.js.map +1 -0
- package/dist/schedule/scheduler.d.ts +56 -15
- package/dist/schedule/scheduler.js +80 -107
- package/dist/schedule/scheduler.js.map +1 -1
- package/dist/storage/kv.js +3 -2
- package/dist/storage/kv.js.map +1 -1
- package/dist/storage/project.d.ts +26 -0
- package/dist/storage/project.js +74 -15
- package/dist/storage/project.js.map +1 -1
- package/dist/tools/builtin/routine.d.ts +31 -0
- package/dist/tools/builtin/routine.js +70 -0
- package/dist/tools/builtin/routine.js.map +1 -0
- package/dist/util/fs.d.ts +6 -0
- package/dist/util/fs.js +22 -0
- package/dist/util/fs.js.map +1 -0
- package/package.json +3 -3
|
@@ -0,0 +1,38 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Who on this machine fires routines.
|
|
3
|
+
*
|
|
4
|
+
* Every copy of the harness that watches the clock reads the same user config,
|
|
5
|
+
* so without this a routine fires once per copy: the desktop app and a
|
|
6
|
+
* `schedule daemon` left running in a terminal would both summarise yesterday's
|
|
7
|
+
* commits at nine. One holder at a time; the others keep asking, so when the
|
|
8
|
+
* holder quits the next one takes over within a tick instead of routines going
|
|
9
|
+
* quiet until a restart.
|
|
10
|
+
*
|
|
11
|
+
* A file with a pid rather than an OS lock: node has no portable advisory
|
|
12
|
+
* lock, and a pid that is no longer alive is a lock nobody holds — which is
|
|
13
|
+
* what a crashed holder leaves behind.
|
|
14
|
+
*/
|
|
15
|
+
export declare class RoutineLock {
|
|
16
|
+
private readonly file;
|
|
17
|
+
private held;
|
|
18
|
+
constructor(file?: string);
|
|
19
|
+
/**
|
|
20
|
+
* Claim a minute: true when this process holds the lock and nobody has
|
|
21
|
+
* handled that minute yet, taking the lock if it is free.
|
|
22
|
+
*
|
|
23
|
+
* The minute is written into the lock, not kept in memory, because the
|
|
24
|
+
* dangerous moment is a takeover: a holder that quits after firing at 9:00
|
|
25
|
+
* leaves the file behind, and the next process to look — still inside 9:00
|
|
26
|
+
* — would otherwise fire everything a second time.
|
|
27
|
+
*/
|
|
28
|
+
claim(minute: number): Promise<boolean>;
|
|
29
|
+
/**
|
|
30
|
+
* Give the lock up, if this process has it.
|
|
31
|
+
*
|
|
32
|
+
* The file stays, owned by nobody, because the minute in it is still true:
|
|
33
|
+
* quitting just after firing at 9:00 must not let whoever takes over next
|
|
34
|
+
* fire 9:00 again.
|
|
35
|
+
*/
|
|
36
|
+
release(): Promise<void>;
|
|
37
|
+
private read;
|
|
38
|
+
}
|
|
@@ -0,0 +1,100 @@
|
|
|
1
|
+
import { mkdir, readFile, writeFile } from "node:fs/promises";
|
|
2
|
+
import path from "node:path";
|
|
3
|
+
import { userPath } from "../paths.js";
|
|
4
|
+
import { parseJson } from "../util/json.js";
|
|
5
|
+
/**
|
|
6
|
+
* Who on this machine fires routines.
|
|
7
|
+
*
|
|
8
|
+
* Every copy of the harness that watches the clock reads the same user config,
|
|
9
|
+
* so without this a routine fires once per copy: the desktop app and a
|
|
10
|
+
* `schedule daemon` left running in a terminal would both summarise yesterday's
|
|
11
|
+
* commits at nine. One holder at a time; the others keep asking, so when the
|
|
12
|
+
* holder quits the next one takes over within a tick instead of routines going
|
|
13
|
+
* quiet until a restart.
|
|
14
|
+
*
|
|
15
|
+
* A file with a pid rather than an OS lock: node has no portable advisory
|
|
16
|
+
* lock, and a pid that is no longer alive is a lock nobody holds — which is
|
|
17
|
+
* what a crashed holder leaves behind.
|
|
18
|
+
*/
|
|
19
|
+
export class RoutineLock {
|
|
20
|
+
file;
|
|
21
|
+
held = false;
|
|
22
|
+
constructor(file = userPath("schedules", "clock.lock")) {
|
|
23
|
+
this.file = file;
|
|
24
|
+
}
|
|
25
|
+
/**
|
|
26
|
+
* Claim a minute: true when this process holds the lock and nobody has
|
|
27
|
+
* handled that minute yet, taking the lock if it is free.
|
|
28
|
+
*
|
|
29
|
+
* The minute is written into the lock, not kept in memory, because the
|
|
30
|
+
* dangerous moment is a takeover: a holder that quits after firing at 9:00
|
|
31
|
+
* leaves the file behind, and the next process to look — still inside 9:00
|
|
32
|
+
* — would otherwise fire everything a second time.
|
|
33
|
+
*/
|
|
34
|
+
async claim(minute) {
|
|
35
|
+
const current = await this.read();
|
|
36
|
+
const mine = current?.pid === process.pid;
|
|
37
|
+
if (!mine && current && alive(current.pid))
|
|
38
|
+
return (this.held = false);
|
|
39
|
+
const done = current?.minute ?? -1;
|
|
40
|
+
await mkdir(path.dirname(this.file), { recursive: true });
|
|
41
|
+
try {
|
|
42
|
+
// `wx` when nobody has ever held it, so two processes starting at once
|
|
43
|
+
// cannot both create it; a stale file, or our own, is replaced.
|
|
44
|
+
await writeFile(this.file, JSON.stringify({ pid: process.pid, since: mine ? current.since : Date.now(), minute: Math.max(done, minute) }), { flag: current ? "w" : "wx" });
|
|
45
|
+
}
|
|
46
|
+
catch {
|
|
47
|
+
return (this.held = false);
|
|
48
|
+
}
|
|
49
|
+
// Read back: two processes replacing the same stale file both succeed, and
|
|
50
|
+
// the one whose pid is in it now is the one that won.
|
|
51
|
+
this.held = (await this.read())?.pid === process.pid;
|
|
52
|
+
return this.held && minute > done;
|
|
53
|
+
}
|
|
54
|
+
/**
|
|
55
|
+
* Give the lock up, if this process has it.
|
|
56
|
+
*
|
|
57
|
+
* The file stays, owned by nobody, because the minute in it is still true:
|
|
58
|
+
* quitting just after firing at 9:00 must not let whoever takes over next
|
|
59
|
+
* fire 9:00 again.
|
|
60
|
+
*/
|
|
61
|
+
async release() {
|
|
62
|
+
if (!this.held)
|
|
63
|
+
return;
|
|
64
|
+
this.held = false;
|
|
65
|
+
const current = await this.read();
|
|
66
|
+
if (current?.pid !== process.pid)
|
|
67
|
+
return;
|
|
68
|
+
await writeFile(this.file, JSON.stringify({ pid: 0, since: 0, minute: current.minute ?? -1 }));
|
|
69
|
+
}
|
|
70
|
+
async read() {
|
|
71
|
+
let raw;
|
|
72
|
+
try {
|
|
73
|
+
raw = await readFile(this.file, "utf8");
|
|
74
|
+
}
|
|
75
|
+
catch {
|
|
76
|
+
return undefined;
|
|
77
|
+
}
|
|
78
|
+
const parsed = parseJson(raw);
|
|
79
|
+
const value = parsed.ok && parsed.value && typeof parsed.value === "object" ? parsed.value : {};
|
|
80
|
+
// An unreadable file is held by nobody; 0 is never a live pid.
|
|
81
|
+
return {
|
|
82
|
+
pid: typeof value.pid === "number" ? value.pid : 0,
|
|
83
|
+
since: typeof value.since === "number" ? value.since : 0,
|
|
84
|
+
...(typeof value.minute === "number" ? { minute: value.minute } : {}),
|
|
85
|
+
};
|
|
86
|
+
}
|
|
87
|
+
}
|
|
88
|
+
function alive(pid) {
|
|
89
|
+
if (pid <= 0)
|
|
90
|
+
return false;
|
|
91
|
+
try {
|
|
92
|
+
process.kill(pid, 0);
|
|
93
|
+
return true;
|
|
94
|
+
}
|
|
95
|
+
catch (err) {
|
|
96
|
+
// EPERM: it exists and belongs to someone else. Still alive.
|
|
97
|
+
return err.code === "EPERM";
|
|
98
|
+
}
|
|
99
|
+
}
|
|
100
|
+
//# sourceMappingURL=lock.js.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"lock.js","sourceRoot":"","sources":["../../src/schedule/lock.ts"],"names":[],"mappings":"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"}
|
|
@@ -0,0 +1,67 @@
|
|
|
1
|
+
import type { Harness } from "../harness.ts";
|
|
2
|
+
import type { Logger } from "../util/logger.ts";
|
|
3
|
+
import type { ClockRoutine } from "./clock.ts";
|
|
4
|
+
import type { RoutineLock } from "./lock.ts";
|
|
5
|
+
import type { ScheduleRun } from "./scheduler.ts";
|
|
6
|
+
export interface RoutineRunnerOptions {
|
|
7
|
+
/** Where a routine written by hand without a directory runs. */
|
|
8
|
+
defaultCwd: string;
|
|
9
|
+
/** The projects open in this process, whose harnesses a run can reuse. */
|
|
10
|
+
openHarnesses(): Iterable<Harness>;
|
|
11
|
+
/** Make a harness for a project that is not open. Closed with the runner. */
|
|
12
|
+
createHarness(cwd: string): Promise<Harness>;
|
|
13
|
+
onStart?(routine: ClockRoutine, meta: {
|
|
14
|
+
scheduledFor: number;
|
|
15
|
+
catchUp: boolean;
|
|
16
|
+
}): void;
|
|
17
|
+
onRun?(run: ScheduleRun, routine: ClockRoutine): void;
|
|
18
|
+
onError?(routine: ClockRoutine, error: string): void;
|
|
19
|
+
logger?: Logger;
|
|
20
|
+
lock?: RoutineLock;
|
|
21
|
+
}
|
|
22
|
+
/**
|
|
23
|
+
* Routines for one process: which exist, where each runs, and the clock that
|
|
24
|
+
* fires them.
|
|
25
|
+
*
|
|
26
|
+
* Shared by the desktop app's server, the TUI and `schedule daemon`, so all
|
|
27
|
+
* three agree on the two things that were each getting wrong on their own —
|
|
28
|
+
* that a routine runs in the project it names, not the one the process
|
|
29
|
+
* happened to start in, and that only one process on the machine fires them.
|
|
30
|
+
*/
|
|
31
|
+
export declare class RoutineRunner {
|
|
32
|
+
private readonly opts;
|
|
33
|
+
private readonly own;
|
|
34
|
+
private clock?;
|
|
35
|
+
constructor(opts: RoutineRunnerOptions);
|
|
36
|
+
/**
|
|
37
|
+
* Every routine that could fire, and the directory each runs in.
|
|
38
|
+
*
|
|
39
|
+
* The user's own name their directory. A project's oharness.config.json
|
|
40
|
+
* contributes its routines while that project is open in this process, and
|
|
41
|
+
* wins a name it shares with the user's in the same directory, as it does
|
|
42
|
+
* when the config is loaded.
|
|
43
|
+
*/
|
|
44
|
+
list(): Promise<ClockRoutine[]>;
|
|
45
|
+
/** A routine by name, preferring the one in `cwd` when two share it. */
|
|
46
|
+
find(name: string, cwd?: string): Promise<ClockRoutine | undefined>;
|
|
47
|
+
/**
|
|
48
|
+
* Run one in its own project. Undefined when that project is already
|
|
49
|
+
* running it. With no meta it is a run by hand, which answers no scheduled
|
|
50
|
+
* time.
|
|
51
|
+
*/
|
|
52
|
+
run(routine: ClockRoutine, meta?: {
|
|
53
|
+
scheduledFor?: number;
|
|
54
|
+
catchUp?: boolean;
|
|
55
|
+
}): Promise<ScheduleRun | undefined>;
|
|
56
|
+
/** Start firing on schedule. */
|
|
57
|
+
start(): void;
|
|
58
|
+
close(): Promise<void>;
|
|
59
|
+
/**
|
|
60
|
+
* An open project's harness, or one of the runner's own.
|
|
61
|
+
*
|
|
62
|
+
* Kept apart from the ones the user switches between: a routine firing is
|
|
63
|
+
* not the user opening a project, and must not add it to their list or make
|
|
64
|
+
* it what the next launch opens.
|
|
65
|
+
*/
|
|
66
|
+
private harnessFor;
|
|
67
|
+
}
|
|
@@ -0,0 +1,100 @@
|
|
|
1
|
+
import { loadProjectConfig, loadUserConfig } from "../config.js";
|
|
2
|
+
import { RoutineClock } from "./clock.js";
|
|
3
|
+
/**
|
|
4
|
+
* Routines for one process: which exist, where each runs, and the clock that
|
|
5
|
+
* fires them.
|
|
6
|
+
*
|
|
7
|
+
* Shared by the desktop app's server, the TUI and `schedule daemon`, so all
|
|
8
|
+
* three agree on the two things that were each getting wrong on their own —
|
|
9
|
+
* that a routine runs in the project it names, not the one the process
|
|
10
|
+
* happened to start in, and that only one process on the machine fires them.
|
|
11
|
+
*/
|
|
12
|
+
export class RoutineRunner {
|
|
13
|
+
opts;
|
|
14
|
+
own = new Map();
|
|
15
|
+
clock;
|
|
16
|
+
constructor(opts) {
|
|
17
|
+
this.opts = opts;
|
|
18
|
+
}
|
|
19
|
+
/**
|
|
20
|
+
* Every routine that could fire, and the directory each runs in.
|
|
21
|
+
*
|
|
22
|
+
* The user's own name their directory. A project's oharness.config.json
|
|
23
|
+
* contributes its routines while that project is open in this process, and
|
|
24
|
+
* wins a name it shares with the user's in the same directory, as it does
|
|
25
|
+
* when the config is loaded.
|
|
26
|
+
*/
|
|
27
|
+
async list() {
|
|
28
|
+
const found = new Map();
|
|
29
|
+
const user = await loadUserConfig().catch(() => ({ schedules: [] }));
|
|
30
|
+
for (const def of user.schedules ?? []) {
|
|
31
|
+
const cwd = def.cwd ?? this.opts.defaultCwd;
|
|
32
|
+
found.set(def.name + "\0" + cwd, { def, cwd });
|
|
33
|
+
}
|
|
34
|
+
for (const harness of this.opts.openHarnesses()) {
|
|
35
|
+
const project = await loadProjectConfig(harness.cwd).catch(() => undefined);
|
|
36
|
+
for (const def of project?.schedules ?? []) {
|
|
37
|
+
found.set(def.name + "\0" + harness.cwd, { def, cwd: harness.cwd });
|
|
38
|
+
}
|
|
39
|
+
}
|
|
40
|
+
return [...found.values()];
|
|
41
|
+
}
|
|
42
|
+
/** A routine by name, preferring the one in `cwd` when two share it. */
|
|
43
|
+
async find(name, cwd) {
|
|
44
|
+
const candidates = (await this.list()).filter((one) => one.def.name === name);
|
|
45
|
+
return candidates.find((one) => one.cwd === cwd) ?? candidates[0];
|
|
46
|
+
}
|
|
47
|
+
/**
|
|
48
|
+
* Run one in its own project. Undefined when that project is already
|
|
49
|
+
* running it. With no meta it is a run by hand, which answers no scheduled
|
|
50
|
+
* time.
|
|
51
|
+
*/
|
|
52
|
+
async run(routine, meta = {}) {
|
|
53
|
+
const harness = await this.harnessFor(routine.cwd);
|
|
54
|
+
return harness.scheduler.runDefinition(routine.def, meta);
|
|
55
|
+
}
|
|
56
|
+
/** Start firing on schedule. */
|
|
57
|
+
start() {
|
|
58
|
+
if (this.clock)
|
|
59
|
+
return;
|
|
60
|
+
this.clock = new RoutineClock({
|
|
61
|
+
routines: () => this.list(),
|
|
62
|
+
run: (routine, meta) => this.run(routine, meta),
|
|
63
|
+
...(this.opts.onStart ? { onStart: this.opts.onStart } : {}),
|
|
64
|
+
...(this.opts.onRun ? { onRun: this.opts.onRun } : {}),
|
|
65
|
+
...(this.opts.onError ? { onError: this.opts.onError } : {}),
|
|
66
|
+
...(this.opts.logger ? { logger: this.opts.logger } : {}),
|
|
67
|
+
...(this.opts.lock ? { lock: this.opts.lock } : {}),
|
|
68
|
+
});
|
|
69
|
+
this.clock.start();
|
|
70
|
+
}
|
|
71
|
+
async close() {
|
|
72
|
+
await this.clock?.stop();
|
|
73
|
+
this.clock = undefined;
|
|
74
|
+
for (const pending of this.own.values()) {
|
|
75
|
+
await pending.then((harness) => harness.close()).catch(() => { });
|
|
76
|
+
}
|
|
77
|
+
this.own.clear();
|
|
78
|
+
}
|
|
79
|
+
/**
|
|
80
|
+
* An open project's harness, or one of the runner's own.
|
|
81
|
+
*
|
|
82
|
+
* Kept apart from the ones the user switches between: a routine firing is
|
|
83
|
+
* not the user opening a project, and must not add it to their list or make
|
|
84
|
+
* it what the next launch opens.
|
|
85
|
+
*/
|
|
86
|
+
harnessFor(cwd) {
|
|
87
|
+
for (const harness of this.opts.openHarnesses()) {
|
|
88
|
+
if (harness.cwd === cwd)
|
|
89
|
+
return Promise.resolve(harness);
|
|
90
|
+
}
|
|
91
|
+
let pending = this.own.get(cwd);
|
|
92
|
+
if (!pending) {
|
|
93
|
+
pending = this.opts.createHarness(cwd);
|
|
94
|
+
this.own.set(cwd, pending);
|
|
95
|
+
pending.catch(() => this.own.delete(cwd));
|
|
96
|
+
}
|
|
97
|
+
return pending;
|
|
98
|
+
}
|
|
99
|
+
}
|
|
100
|
+
//# sourceMappingURL=runner.js.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"runner.js","sourceRoot":"","sources":["../../src/schedule/runner.ts"],"names":[],"mappings":"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"}
|
|
@@ -1,3 +1,4 @@
|
|
|
1
|
+
import type { CronFields } from "./cron.ts";
|
|
1
2
|
import type { Harness } from "../harness.ts";
|
|
2
3
|
import type { PermissionMode } from "../agent/permissions.ts";
|
|
3
4
|
export interface ScheduleDefinition {
|
|
@@ -14,10 +15,21 @@ export interface ScheduleDefinition {
|
|
|
14
15
|
*/
|
|
15
16
|
permissionMode?: PermissionMode;
|
|
16
17
|
maxTurns?: number;
|
|
18
|
+
/** False pauses it: kept, listed, never fired. */
|
|
17
19
|
enabled?: boolean;
|
|
18
20
|
/** Working directory for the run; defaults to the harness cwd. */
|
|
19
21
|
cwd?: string;
|
|
22
|
+
/**
|
|
23
|
+
* When it was made. A routine is never caught up for a time before it
|
|
24
|
+
* existed; one written by hand without this is caught up only from the
|
|
25
|
+
* first time a clock saw it.
|
|
26
|
+
*/
|
|
27
|
+
createdAt?: number;
|
|
20
28
|
}
|
|
29
|
+
/**
|
|
30
|
+
* One line of a routine's history: a run, or a scheduled time that was
|
|
31
|
+
* deliberately not run.
|
|
32
|
+
*/
|
|
21
33
|
export interface ScheduleRun {
|
|
22
34
|
schedule: string;
|
|
23
35
|
startedAt: number;
|
|
@@ -29,19 +41,45 @@ export interface ScheduleRun {
|
|
|
29
41
|
/** Final assistant text, truncated. */
|
|
30
42
|
output: string;
|
|
31
43
|
error?: string;
|
|
44
|
+
/**
|
|
45
|
+
* The scheduled time this answered, before any stagger. Absent on a run
|
|
46
|
+
* started by hand — which is why a manual run never stands in for the
|
|
47
|
+
* scheduled one after it.
|
|
48
|
+
*/
|
|
49
|
+
scheduledFor?: number;
|
|
50
|
+
/** Started late, because the scheduled time passed while nothing was watching. */
|
|
51
|
+
catchUp?: boolean;
|
|
52
|
+
/** Not run at all; `reason` says why. */
|
|
53
|
+
skipped?: boolean;
|
|
54
|
+
reason?: "still-running";
|
|
32
55
|
}
|
|
33
56
|
export interface ScheduleStatus {
|
|
34
57
|
name: string;
|
|
35
58
|
cron: string;
|
|
36
59
|
enabled: boolean;
|
|
37
60
|
nextRunAt?: number;
|
|
61
|
+
/** The last run that actually ran — a skipped time is history, not a result. */
|
|
38
62
|
lastRun?: ScheduleRun;
|
|
39
63
|
}
|
|
40
64
|
/**
|
|
41
|
-
*
|
|
65
|
+
* Minutes a routine starts after its scheduled time, the same every time for
|
|
66
|
+
* the same routine.
|
|
67
|
+
*
|
|
68
|
+
* "Every day at 9" is what most people write, and every copy firing at 9:00:00
|
|
69
|
+
* is a queue at the gateway at 9:00:00. A few minutes, fixed by the name so a
|
|
70
|
+
* routine keeps its own slot rather than wandering, and none at all for one
|
|
71
|
+
* that fires more than once an hour, where a few minutes is most of its
|
|
72
|
+
* interval.
|
|
73
|
+
*/
|
|
74
|
+
export declare function staggerMinutes(def: Pick<ScheduleDefinition, "name">, fields: CronFields): number;
|
|
75
|
+
/**
|
|
76
|
+
* Holds routines, runs them, and keeps their history.
|
|
77
|
+
*
|
|
78
|
+
* It does not watch the clock: RoutineClock does, for every project at once,
|
|
79
|
+
* and asks the scheduler of each routine's own project to run it.
|
|
42
80
|
*
|
|
43
|
-
* Each
|
|
44
|
-
*
|
|
81
|
+
* Each run gets its own session rather than appending to a long-lived one: a
|
|
82
|
+
* nightly job that shares context with every previous night would compact
|
|
45
83
|
* itself into uselessness within a week, and a failure would poison every
|
|
46
84
|
* subsequent run.
|
|
47
85
|
*/
|
|
@@ -50,8 +88,6 @@ export declare class Scheduler {
|
|
|
50
88
|
private readonly logger;
|
|
51
89
|
private readonly schedules;
|
|
52
90
|
private readonly runDir;
|
|
53
|
-
private timer?;
|
|
54
|
-
private running;
|
|
55
91
|
/** Names currently executing, so a slow job cannot overlap itself. */
|
|
56
92
|
private readonly inFlight;
|
|
57
93
|
constructor(harness: Harness, definitions?: ScheduleDefinition[], opts?: {
|
|
@@ -69,19 +105,24 @@ export declare class Scheduler {
|
|
|
69
105
|
*/
|
|
70
106
|
runNow(name: string): Promise<ScheduleRun>;
|
|
71
107
|
/**
|
|
72
|
-
*
|
|
108
|
+
* Run a definition in this harness, whether or not this scheduler holds it.
|
|
73
109
|
*
|
|
74
|
-
*
|
|
75
|
-
*
|
|
76
|
-
*
|
|
110
|
+
* For the clock, which watches routines across projects: it decides what is
|
|
111
|
+
* due, and each routine runs in the harness of the directory it names — the
|
|
112
|
+
* project's instructions, MCP servers and permission rules — rather than in
|
|
113
|
+
* whichever project happened to notice the minute arrive.
|
|
77
114
|
*/
|
|
78
|
-
|
|
79
|
-
|
|
80
|
-
|
|
81
|
-
|
|
115
|
+
runDefinition(def: ScheduleDefinition, meta?: {
|
|
116
|
+
scheduledFor?: number;
|
|
117
|
+
catchUp?: boolean;
|
|
118
|
+
}): Promise<ScheduleRun | undefined>;
|
|
119
|
+
isRunning(name: string): boolean;
|
|
82
120
|
private execute;
|
|
83
|
-
private recordRun;
|
|
84
121
|
private lastRun;
|
|
85
|
-
/** Past runs, newest first. */
|
|
122
|
+
/** Past runs and skipped times, newest first. */
|
|
86
123
|
history(name: string, limit?: number): Promise<ScheduleRun[]>;
|
|
87
124
|
}
|
|
125
|
+
/** Add one line to a routine's history. */
|
|
126
|
+
export declare function appendRun(runDir: string, run: ScheduleRun): Promise<void>;
|
|
127
|
+
/** A routine's history, oldest first. Lines that do not parse are passed over. */
|
|
128
|
+
export declare function readRuns(runDir: string, name: string): Promise<ScheduleRun[]>;
|