@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.
Files changed (90) hide show
  1. package/dist/agent/agent.d.ts +12 -0
  2. package/dist/agent/agent.js +3 -0
  3. package/dist/agent/agent.js.map +1 -1
  4. package/dist/agent/memory-backup.d.ts +2 -0
  5. package/dist/agent/memory-backup.js +9 -1
  6. package/dist/agent/memory-backup.js.map +1 -1
  7. package/dist/agent/memory-limits.js +8 -1
  8. package/dist/agent/memory-limits.js.map +1 -1
  9. package/dist/agent/memory-usage.d.ts +1 -1
  10. package/dist/agent/memory-usage.js +2 -3
  11. package/dist/agent/memory-usage.js.map +1 -1
  12. package/dist/agent/memory.d.ts +3 -0
  13. package/dist/agent/memory.js +6 -18
  14. package/dist/agent/memory.js.map +1 -1
  15. package/dist/agent/session.d.ts +9 -0
  16. package/dist/agent/session.js +14 -2
  17. package/dist/agent/session.js.map +1 -1
  18. package/dist/agent/settings.js +3 -2
  19. package/dist/agent/settings.js.map +1 -1
  20. package/dist/agent/store.d.ts +45 -4
  21. package/dist/agent/store.js +198 -25
  22. package/dist/agent/store.js.map +1 -1
  23. package/dist/artifacts/files.d.ts +52 -0
  24. package/dist/artifacts/files.js +169 -0
  25. package/dist/artifacts/files.js.map +1 -0
  26. package/dist/auth/store.js +3 -2
  27. package/dist/auth/store.js.map +1 -1
  28. package/dist/config-files.js +3 -2
  29. package/dist/config-files.js.map +1 -1
  30. package/dist/config.d.ts +6 -1
  31. package/dist/config.js +10 -2
  32. package/dist/config.js.map +1 -1
  33. package/dist/harness.d.ts +62 -1
  34. package/dist/harness.js +174 -12
  35. package/dist/harness.js.map +1 -1
  36. package/dist/paths.d.ts +5 -0
  37. package/dist/paths.js +21 -3
  38. package/dist/paths.js.map +1 -1
  39. package/dist/providers/builtin.d.ts +2 -0
  40. package/dist/providers/builtin.js +38 -24
  41. package/dist/providers/builtin.js.map +1 -1
  42. package/dist/providers/catalog.d.ts +5 -0
  43. package/dist/providers/catalog.js +32 -1
  44. package/dist/providers/catalog.js.map +1 -1
  45. package/dist/providers/catalog.json +241 -123
  46. package/dist/providers/discovery.js +17 -0
  47. package/dist/providers/discovery.js.map +1 -1
  48. package/dist/providers/models-dev.d.ts +89 -0
  49. package/dist/providers/models-dev.js +160 -0
  50. package/dist/providers/models-dev.js.map +1 -0
  51. package/dist/providers/models-dev.json +821 -0
  52. package/dist/providers/registry.d.ts +12 -0
  53. package/dist/providers/registry.js +33 -1
  54. package/dist/providers/registry.js.map +1 -1
  55. package/dist/providers/types.d.ts +21 -0
  56. package/dist/providers/types.js +16 -0
  57. package/dist/providers/types.js.map +1 -1
  58. package/dist/runtime-plugins/builtin/memory-migrate.d.ts +5 -21
  59. package/dist/runtime-plugins/builtin/memory-migrate.js +5 -125
  60. package/dist/runtime-plugins/builtin/memory-migrate.js.map +1 -1
  61. package/dist/runtime-plugins/builtin/memory.d.ts +3 -0
  62. package/dist/runtime-plugins/builtin/memory.js +12 -10
  63. package/dist/runtime-plugins/builtin/memory.js.map +1 -1
  64. package/dist/schedule/clock.d.ts +76 -0
  65. package/dist/schedule/clock.js +147 -0
  66. package/dist/schedule/clock.js.map +1 -0
  67. package/dist/schedule/cron.d.ts +9 -0
  68. package/dist/schedule/cron.js +18 -0
  69. package/dist/schedule/cron.js.map +1 -1
  70. package/dist/schedule/lock.d.ts +38 -0
  71. package/dist/schedule/lock.js +100 -0
  72. package/dist/schedule/lock.js.map +1 -0
  73. package/dist/schedule/runner.d.ts +67 -0
  74. package/dist/schedule/runner.js +100 -0
  75. package/dist/schedule/runner.js.map +1 -0
  76. package/dist/schedule/scheduler.d.ts +56 -15
  77. package/dist/schedule/scheduler.js +80 -107
  78. package/dist/schedule/scheduler.js.map +1 -1
  79. package/dist/storage/kv.js +3 -2
  80. package/dist/storage/kv.js.map +1 -1
  81. package/dist/storage/project.d.ts +26 -0
  82. package/dist/storage/project.js +74 -15
  83. package/dist/storage/project.js.map +1 -1
  84. package/dist/tools/builtin/routine.d.ts +31 -0
  85. package/dist/tools/builtin/routine.js +70 -0
  86. package/dist/tools/builtin/routine.js.map +1 -0
  87. package/dist/util/fs.d.ts +6 -0
  88. package/dist/util/fs.js +22 -0
  89. package/dist/util/fs.js.map +1 -0
  90. 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
- * Runs prompts on a cron schedule.
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 firing gets its own session rather than appending to a long-lived one:
44
- * a nightly job that shares context with every previous night would compact
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
- * Start firing on schedule.
108
+ * Run a definition in this harness, whether or not this scheduler holds it.
73
109
  *
74
- * Wakes every 30s and fires anything whose minute has arrived, rather than
75
- * sleeping until the exact next occurrence: a laptop that suspends would
76
- * otherwise wake with a timer scheduled for a moment that has passed.
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
- start(): void;
79
- stop(): void;
80
- /** Resolves when every in-flight run has finished. */
81
- drain(): Promise<void>;
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[]>;