arisa 3.1.2 → 3.1.6

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.
@@ -1,34 +1,16 @@
1
1
  import crypto from "node:crypto";
2
- import { spawn } from "node:child_process";
3
- import { openSync } from "node:fs";
4
- import { mkdir, readFile, readdir, rename, rm, unlink, writeFile } from "node:fs/promises";
2
+ import { mkdir, readdir, rename, unlink } from "node:fs/promises";
5
3
  import path from "node:path";
6
- import { stateDir } from "../../runtime/paths.js";
7
-
8
- export function daemonPaths(toolName) {
9
- const root = path.join(stateDir, toolName);
10
- return {
11
- root,
12
- commandsDir: path.join(root, "commands"),
13
- pidFile: path.join(root, "daemon.pid"),
14
- statusFile: path.join(root, "status.json"),
15
- logFile: path.join(root, "daemon.log")
16
- };
17
- }
18
-
19
- export async function readJson(file, fallback = {}) {
20
- try { return JSON.parse(await readFile(file, "utf8")); } catch { return fallback; }
21
- }
22
-
23
- export async function writeJson(file, value) {
24
- await mkdir(path.dirname(file), { recursive: true });
25
- await writeFile(file, `${JSON.stringify(value, null, 2)}\n`, "utf8");
26
- }
27
-
28
- export function isProcessAlive(pid) {
29
- if (!pid) return false;
30
- try { process.kill(pid, 0); return true; } catch { return false; }
31
- }
4
+ import {
5
+ OWNER_HEARTBEAT_INTERVAL_MS,
6
+ OWNER_HEARTBEAT_TTL_MS,
7
+ daemonPaths,
8
+ isProcessAlive,
9
+ readJson,
10
+ startManagedDaemon,
11
+ stopManagedDaemon,
12
+ writeJson
13
+ } from "./daemon-processes.js";
32
14
 
33
15
  export function createDaemonRuntime({ toolName, entryPath, beforeStart = null }) {
34
16
  const paths = daemonPaths(toolName);
@@ -54,33 +36,54 @@ export function createDaemonRuntime({ toolName, entryPath, beforeStart = null })
54
36
  await writeJson(paths.statusFile, { ...current, ...patch, updatedAt: new Date().toISOString() });
55
37
  }
56
38
 
57
- async function clearJobs() {
58
- await rm(paths.commandsDir, { recursive: true, force: true });
59
- await mkdir(paths.commandsDir, { recursive: true });
60
- }
61
-
62
39
  async function start() {
63
- await ensure();
64
- const pid = await getPid();
65
- if (isProcessAlive(pid)) return pid;
66
-
67
- await clearJobs();
68
- if (beforeStart) await beforeStart();
69
- const out = openSync(paths.logFile, "a");
70
- const child = spawn(process.execPath, [entryPath, "daemon"], {
71
- detached: true,
72
- stdio: ["ignore", out, out],
73
- env: process.env
40
+ return startManagedDaemon({
41
+ toolName,
42
+ entryPath,
43
+ beforeStart,
44
+ ownerEnv: {
45
+ ARISA_OWNER_PID: process.env.ARISA_OWNER_PID,
46
+ ARISA_TOOL_OWNER_FILE: process.env.ARISA_TOOL_OWNER_FILE,
47
+ ARISA_TOOL_OWNER_TOKEN: process.env.ARISA_TOOL_OWNER_TOKEN
48
+ }
74
49
  });
75
- child.unref();
76
- await writeJson(paths.pidFile, { pid: child.pid, startedAt: new Date().toISOString() });
77
- return child.pid;
78
50
  }
79
51
 
80
52
  async function stop() {
81
- const pid = await getPid();
82
- if (isProcessAlive(pid)) process.kill(pid, "SIGTERM");
83
- await rm(paths.pidFile, { force: true });
53
+ await stopManagedDaemon(toolName);
54
+ }
55
+
56
+ function installOwnerWatch() {
57
+ const ownerFile = process.env.ARISA_TOOL_OWNER_FILE;
58
+ const ownerToken = process.env.ARISA_TOOL_OWNER_TOKEN;
59
+ if (!ownerFile || !ownerToken) return;
60
+
61
+ let exiting = false;
62
+ async function exitIfOrphaned(message) {
63
+ if (exiting) return;
64
+ exiting = true;
65
+ await writeStatus({ state: "stopped", message });
66
+ process.exit(0);
67
+ }
68
+
69
+ const timer = setInterval(async () => {
70
+ const owner = await readJson(ownerFile, null);
71
+ if (!owner || owner.token !== ownerToken) {
72
+ await exitIfOrphaned("Arisa owner stopped");
73
+ return;
74
+ }
75
+
76
+ const heartbeatAt = Date.parse(owner.heartbeatAt || "");
77
+ if (!Number.isFinite(heartbeatAt) || Date.now() - heartbeatAt > OWNER_HEARTBEAT_TTL_MS) {
78
+ await exitIfOrphaned("Arisa owner heartbeat expired");
79
+ return;
80
+ }
81
+
82
+ if (!isProcessAlive(owner.pid)) {
83
+ await exitIfOrphaned("Arisa owner process exited");
84
+ }
85
+ }, OWNER_HEARTBEAT_INTERVAL_MS);
86
+ timer.unref();
84
87
  }
85
88
 
86
89
  async function waitReady({ timeoutMs = 120000, readyStates = ["ready"] } = {}) {
@@ -140,6 +143,7 @@ export function createDaemonRuntime({ toolName, entryPath, beforeStart = null })
140
143
  }
141
144
 
142
145
  async function workLoop({ processJob, idleTimeoutMs = 0, intervalMs = 250 }) {
146
+ installOwnerWatch();
143
147
  let lastActivity = Date.now();
144
148
  setInterval(async () => {
145
149
  try {
@@ -1,10 +1,11 @@
1
- import { mkdir, readdir, readFile, unlink, writeFile } from "node:fs/promises";
1
+ import { mkdir, readdir, readFile, rmdir, unlink, writeFile } from "node:fs/promises";
2
2
  import path from "node:path";
3
3
  import { spawn } from "node:child_process";
4
4
  import { fileURLToPath } from "node:url";
5
5
  import { getToolConfigPath, getToolTmpDir, getChatToolTmpDir, toolsDir as userToolsRoot } from "../../runtime/paths.js";
6
6
  import { loadToolConfig, parseConfigModule, writeToolConfig } from "./tool-config.js";
7
7
  import { normalizeToolResult } from "./tool-result.js";
8
+ import { SkillRegistry } from "../skills/skill-registry.js";
8
9
 
9
10
  const bundledToolsRoot = fileURLToPath(new URL("../../../tools", import.meta.url));
10
11
  const toolRoots = [
@@ -24,9 +25,11 @@ function runProcess(command, args, options = {}) {
24
25
  }
25
26
 
26
27
  export class ToolRegistry {
27
- constructor({ logger } = {}) {
28
+ constructor({ logger, processOwnerEnv = {} } = {}) {
28
29
  this.logger = logger;
30
+ this.processOwnerEnv = processOwnerEnv;
29
31
  this.tools = new Map();
32
+ this.skillRegistry = new SkillRegistry();
30
33
  }
31
34
 
32
35
  async load() {
@@ -52,8 +55,10 @@ export class ToolRegistry {
52
55
  const configSource = await readFile(configPath, "utf8");
53
56
  const defaults = parseConfigModule(configSource);
54
57
  const config = await loadToolConfig(manifest.name, defaults);
58
+ const skillHints = this.skillRegistry.normalizeHints(manifest);
55
59
  this.tools.set(manifest.name, {
56
60
  ...manifest,
61
+ skillHints,
57
62
  dir: toolDir,
58
63
  entry: path.join(toolDir, manifest.entry || "index.js"),
59
64
  localConfigPath: configPath,
@@ -77,7 +82,8 @@ export class ToolRegistry {
77
82
  description: tool.description,
78
83
  input: tool.input,
79
84
  output: tool.output,
80
- configSchema: tool.configSchema || {}
85
+ configSchema: tool.configSchema || {},
86
+ skillHints: tool.skillHints || []
81
87
  }));
82
88
  }
83
89
 
@@ -89,7 +95,29 @@ export class ToolRegistry {
89
95
  const tool = this.get(name);
90
96
  if (!tool) throw new Error(`Tool not found: ${name}`);
91
97
  const result = await runProcess("node", [tool.entry, "--help"], { cwd: tool.dir, env: process.env });
92
- return result.stdout || result.stderr;
98
+ const help = result.stdout || result.stderr;
99
+ const skills = await this.resolveSkills(name);
100
+ if (!skills.length) return help;
101
+ const skillHelp = skills.map((item) => [
102
+ `- ${item.name}${item.when ? ` (${item.when})` : ""}`,
103
+ item.description ? ` ${item.description}` : null,
104
+ item.found ? ` path: ${item.path}` : " warning: skill not found"
105
+ ].filter(Boolean).join("\n")).join("\n");
106
+ return `${help}\n\nAssigned skills:\n${skillHelp}\n`;
107
+ }
108
+
109
+ async resolveSkills(name) {
110
+ const tool = this.get(name);
111
+ if (!tool) throw new Error(`Tool not found: ${name}`);
112
+ const hints = await this.skillRegistry.resolveHints(tool.skillHints || []);
113
+ return hints.map((hint) => ({
114
+ name: hint.name,
115
+ when: hint.when,
116
+ found: hint.found,
117
+ description: hint.skill?.description || "",
118
+ path: hint.skill?.path || "",
119
+ content: hint.skill?.content || ""
120
+ }));
93
121
  }
94
122
 
95
123
  async resolveConfigForChat(name, chatId) {
@@ -121,12 +149,19 @@ export class ToolRegistry {
121
149
  const tmpDir = chatId != null ? getChatToolTmpDir(chatId, name) : getToolTmpDir(name);
122
150
  await mkdir(tmpDir, { recursive: true });
123
151
  const requestFile = path.join(tmpDir, `.request-${Date.now()}.json`);
124
- await writeFile(requestFile, `${JSON.stringify(request, null, 2)}\n`, "utf8");
152
+ const skills = await this.resolveSkills(name);
153
+ const enrichedRequest = { ...request, chatId, skills };
154
+ await writeFile(requestFile, `${JSON.stringify(enrichedRequest, null, 2)}\n`, "utf8");
125
155
  const result = await runProcess("node", [tool.entry, "run", "--request-file", requestFile], {
126
156
  cwd: tool.dir,
127
- env: process.env
157
+ env: { ...process.env, ...this.processOwnerEnv }
128
158
  });
129
159
  await unlink(requestFile).catch(() => {});
160
+ await rmdir(tmpDir).catch(() => {});
161
+ if (chatId != null) {
162
+ await rmdir(path.dirname(tmpDir)).catch(() => {});
163
+ await rmdir(path.dirname(path.dirname(tmpDir))).catch(() => {});
164
+ }
130
165
  try {
131
166
  const parsed = JSON.parse(result.stdout || result.stderr);
132
167
  const normalized = normalizeToolResult(name, parsed);
package/src/index.js CHANGED
@@ -4,7 +4,7 @@ import { createServer } from "node:http";
4
4
  import { bootstrapIfNeeded } from "./runtime/bootstrap.js";
5
5
  import { createApp } from "./runtime/create-app.js";
6
6
  import { createLogger } from "./runtime/logger.js";
7
- import { getServiceStatus, registerServiceProcess, startService, stopService } from "./runtime/service-manager.js";
7
+ import { getServiceStatus, registerServiceProcess, startService, stopService, unregisterServiceProcess } from "./runtime/service-manager.js";
8
8
  import { flushArisaHome } from "./runtime/flush.js";
9
9
  import { installPiPackage, removePiPackage } from "./runtime/pi-package-manager.js";
10
10
 
@@ -17,6 +17,8 @@ const serviceRunner = Boolean(cli.flags["service-runner"]);
17
17
  const bootstrapOverrides = toBootstrapOverrides(cli.nestedFlags);
18
18
  const runtimeOverrides = toRuntimeOverrides(cli.nestedFlags);
19
19
  const logger = createLogger({ verbose });
20
+ let activeApp = null;
21
+ let shuttingDown = false;
20
22
 
21
23
  const httpPort = Number(process.env.PORT);
22
24
  let httpRequestHandler = null;
@@ -97,6 +99,35 @@ const bootstrapHttpOptions = httpPort ? { httpPort, setHttpRequestHandler } : {}
97
99
  const webhookUrl = bootstrapOverrides.webhook?.url || "";
98
100
  const appHttpOptions = httpPort ? { webhookUrl, setHttpRequestHandler } : {};
99
101
 
102
+ async function shutdown(exitCode = 0) {
103
+ if (shuttingDown) return;
104
+ shuttingDown = true;
105
+ try {
106
+ await activeApp?.stop?.();
107
+ } catch (error) {
108
+ logger.error("app", `shutdown failed: ${error instanceof Error ? error.message : String(error)}`);
109
+ exitCode = exitCode || 1;
110
+ }
111
+ if (serviceRunner) {
112
+ await unregisterServiceProcess();
113
+ }
114
+ process.exit(exitCode);
115
+ }
116
+
117
+ process.once("SIGTERM", () => {
118
+ shutdown(0);
119
+ });
120
+
121
+ process.once("SIGINT", () => {
122
+ shutdown(0);
123
+ });
124
+
125
+ async function startRuntimeApp() {
126
+ const app = await createApp({ logger, runtimeOverrides, ...appHttpOptions });
127
+ activeApp = app;
128
+ await app.start();
129
+ }
130
+
100
131
  async function runForeground() {
101
132
  const hasRuntimePiOverrides = Boolean(
102
133
  runtimeOverrides?.pi?.model
@@ -106,11 +137,12 @@ async function runForeground() {
106
137
  logger.log("app", `starting${verbose ? " in verbose mode" : ""}`);
107
138
  await bootstrapIfNeeded({ force: forceBootstrap, cliConfigOverrides: bootstrapOverrides, ...bootstrapHttpOptions });
108
139
  try {
109
- const app = await createApp({ logger, runtimeOverrides, ...appHttpOptions });
110
- await app.start();
140
+ await startRuntimeApp();
111
141
  } catch (error) {
112
142
  const message = error instanceof Error ? error.message : String(error);
113
143
  if (message.includes("No auth found")) {
144
+ await activeApp?.stop?.();
145
+ activeApp = null;
114
146
  console.log(`\n${message}\n`);
115
147
  if (hasRuntimePiOverrides) {
116
148
  console.log("Skipping automatic bootstrap because Pi runtime overrides were provided.");
@@ -119,8 +151,7 @@ async function runForeground() {
119
151
  }
120
152
  console.log("Reopening bootstrap so you can provide a Pi API key or switch to a provider you already authenticated with.\n");
121
153
  await bootstrapIfNeeded({ force: true, cliConfigOverrides: bootstrapOverrides, ...bootstrapHttpOptions });
122
- const app = await createApp({ logger, runtimeOverrides, ...appHttpOptions });
123
- await app.start();
154
+ await startRuntimeApp();
124
155
  return;
125
156
  }
126
157
  throw error;
@@ -202,4 +233,9 @@ async function main() {
202
233
  await runForeground();
203
234
  }
204
235
 
205
- await main();
236
+ try {
237
+ await main();
238
+ } catch (error) {
239
+ logger.error("app", error instanceof Error ? error.message : String(error));
240
+ await shutdown(1);
241
+ }
@@ -90,7 +90,7 @@ function sortBootstrapProviders(providers) {
90
90
 
91
91
  function sortBootstrapModels(provider, models) {
92
92
  const preferred = {
93
- "openai-codex": ["gpt-5.4"]
93
+ "openai-codex": ["gpt-5.5"]
94
94
  };
95
95
 
96
96
  const priority = preferred[provider] || [];
@@ -4,6 +4,7 @@ import { ToolRegistry } from "../core/tools/tool-registry.js";
4
4
  import { TaskStore } from "../core/tasks/task-store.js";
5
5
  import { AgentManager } from "../core/agent/agent-manager.js";
6
6
  import { createTelegramBot } from "../transport/telegram/bot.js";
7
+ import { createToolProcessSupervisor } from "./tool-process-supervisor.js";
7
8
 
8
9
  function normalizeString(value) {
9
10
  const text = String(value ?? "").trim();
@@ -51,7 +52,8 @@ export async function createApp({ logger, runtimeOverrides, webhookUrl, setHttpR
51
52
  }
52
53
 
53
54
  const artifactStore = new ArtifactStore();
54
- const toolRegistry = new ToolRegistry({ logger });
55
+ const toolProcessSupervisor = createToolProcessSupervisor({ logger });
56
+ const toolRegistry = new ToolRegistry({ logger, processOwnerEnv: toolProcessSupervisor.env() });
55
57
  const taskStore = new TaskStore();
56
58
  await toolRegistry.load();
57
59
  logger?.log("app", `loaded ${toolRegistry.list().length} tools`);
@@ -63,8 +65,19 @@ export async function createApp({ logger, runtimeOverrides, webhookUrl, setHttpR
63
65
  async start() {
64
66
  logger?.log("app", `validating Pi model ${config.pi.provider}/${config.pi.model}`);
65
67
  await agentManager.validatePiAgent();
68
+ await toolProcessSupervisor.start();
66
69
  logger?.log("app", "starting Telegram bot");
67
- await bot.start();
70
+ try {
71
+ await bot.start();
72
+ } catch (error) {
73
+ await toolProcessSupervisor.stop();
74
+ throw error;
75
+ }
76
+ },
77
+
78
+ async stop() {
79
+ await bot.stop?.();
80
+ await toolProcessSupervisor.stop();
68
81
  }
69
82
  };
70
83
  }
@@ -7,9 +7,11 @@ export const stateDir = path.join(arisaHomeDir, "state");
7
7
  export const configFile = path.join(stateDir, "config.json");
8
8
  export const servicePidFile = path.join(stateDir, "arisa.pid");
9
9
  export const serviceLogFile = path.join(stateDir, "arisa.log");
10
+ export const toolDaemonOwnerFile = path.join(stateDir, "tool-daemon-owner.json");
10
11
  export const tasksFile = path.join(stateDir, "tasks.json");
11
12
  export const toolsDir = path.join(arisaHomeDir, "tools");
12
13
  export const chatsDir = path.join(arisaHomeDir, "chats");
14
+ export const toolStateDir = path.join(stateDir, "tools");
13
15
 
14
16
  export function getChatDir(chatId) {
15
17
  return path.join(chatsDir, String(chatId));
@@ -23,6 +25,10 @@ export function getChatArtifactsIndexFile(chatId) {
23
25
  return path.join(getChatDir(chatId), "state", "artifacts.json");
24
26
  }
25
27
 
28
+ export function getChatToolStateDir(chatId, toolName) {
29
+ return path.join(getChatDir(chatId), "state", "tools", toolName);
30
+ }
31
+
26
32
  export function getChatPiSessionsDir(chatId) {
27
33
  return path.join(getChatDir(chatId), "state", "pi-sessions");
28
34
  }
@@ -35,24 +41,28 @@ export function getToolConfigPath(toolName) {
35
41
  return path.join(getToolDir(toolName), "config.js");
36
42
  }
37
43
 
38
- export function getChatToolConfigPath(chatId, toolName) {
39
- return path.join(getChatDir(chatId), "tools", toolName, "config.js");
44
+ export function getChatConfigDir(chatId) {
45
+ return path.join(getChatDir(chatId), "config");
40
46
  }
41
47
 
42
- export function getToolRuntimeDir(toolName) {
43
- return getToolDir(toolName);
48
+ export function getChatTmpDir(chatId) {
49
+ return path.join(getChatDir(chatId), "tmp");
50
+ }
51
+
52
+ export function getChatToolConfigPath(chatId, toolName) {
53
+ return path.join(getChatConfigDir(chatId), "tools", toolName, "config.js");
44
54
  }
45
55
 
46
- export function getToolOutDir(toolName) {
47
- return path.join(getToolRuntimeDir(toolName), "out");
56
+ export function getToolStateDir(toolName) {
57
+ return path.join(toolStateDir, toolName);
48
58
  }
49
59
 
50
60
  export function getToolTmpDir(toolName) {
51
- return path.join(getToolRuntimeDir(toolName), "tmp");
61
+ return path.join(getToolStateDir(toolName), "tmp");
52
62
  }
53
63
 
54
64
  export function getChatToolTmpDir(chatId, toolName) {
55
- return path.join(getChatDir(chatId), "tools", toolName, "tmp");
65
+ return path.join(getChatTmpDir(chatId), "tools", toolName);
56
66
  }
57
67
 
58
68
  export async function ensureArisaHome() {
@@ -74,24 +74,14 @@ export async function stopService() {
74
74
  return { ok: true, pid: status.pid };
75
75
  }
76
76
 
77
+ export async function unregisterServiceProcess() {
78
+ await rm(servicePidFile, { force: true }).catch(() => {});
79
+ }
80
+
77
81
  export async function registerServiceProcess() {
78
82
  await ensureArisaHome();
79
83
  await writeFile(servicePidFile, `${process.pid}\n`, "utf8");
80
84
 
81
- const cleanup = async () => {
82
- await rm(servicePidFile, { force: true }).catch(() => {});
83
- };
84
-
85
- process.on("SIGTERM", async () => {
86
- await cleanup();
87
- process.exit(0);
88
- });
89
-
90
- process.on("SIGINT", async () => {
91
- await cleanup();
92
- process.exit(0);
93
- });
94
-
95
85
  process.on("exit", () => {
96
86
  rm(servicePidFile, { force: true }).catch(() => {});
97
87
  });
@@ -0,0 +1,110 @@
1
+ import crypto from "node:crypto";
2
+ import { access, rm } from "node:fs/promises";
3
+ import {
4
+ daemonPaths,
5
+ listRegisteredDaemons,
6
+ readJson,
7
+ startManagedDaemon,
8
+ stopManagedDaemon,
9
+ writeJson
10
+ } from "../core/tools/daemon-processes.js";
11
+ import { ensureArisaHome, toolDaemonOwnerFile } from "./paths.js";
12
+
13
+ async function fileExists(file) {
14
+ try {
15
+ await access(file);
16
+ return true;
17
+ } catch {
18
+ return false;
19
+ }
20
+ }
21
+
22
+ export function createToolProcessSupervisor({ logger } = {}) {
23
+ const token = crypto.randomUUID();
24
+ let heartbeatTimer = null;
25
+ let running = false;
26
+
27
+ const ownerEnv = {
28
+ ARISA_OWNER_PID: String(process.pid),
29
+ ARISA_TOOL_OWNER_FILE: toolDaemonOwnerFile,
30
+ ARISA_TOOL_OWNER_TOKEN: token
31
+ };
32
+
33
+ async function writeHeartbeat() {
34
+ await writeJson(toolDaemonOwnerFile, {
35
+ pid: process.pid,
36
+ token,
37
+ heartbeatAt: new Date().toISOString()
38
+ });
39
+ }
40
+
41
+ async function startHeartbeat() {
42
+ await ensureArisaHome();
43
+ await writeHeartbeat();
44
+ heartbeatTimer = setInterval(() => {
45
+ writeHeartbeat().catch((error) => {
46
+ logger?.error("tools", `tool daemon heartbeat failed: ${error instanceof Error ? error.message : String(error)}`);
47
+ });
48
+ }, 2000);
49
+ heartbeatTimer.unref();
50
+ }
51
+
52
+ async function restartRegisteredDaemons() {
53
+ for (const record of await listRegisteredDaemons()) {
54
+ if (!record.autoStart) continue;
55
+ if (!(await fileExists(record.entryPath))) {
56
+ logger?.log("tools", `skipping daemon ${record.toolName}: missing entry ${record.entryPath}`);
57
+ continue;
58
+ }
59
+
60
+ const paths = daemonPaths(record.toolName);
61
+ const { pid } = await readJson(paths.pidFile, {});
62
+ if (pid) {
63
+ await stopManagedDaemon(record.toolName);
64
+ }
65
+
66
+ logger?.log("tools", `starting managed daemon ${record.toolName}`);
67
+ await startManagedDaemon({
68
+ toolName: record.toolName,
69
+ entryPath: record.entryPath,
70
+ ownerEnv
71
+ });
72
+ }
73
+ }
74
+
75
+ async function stopRegisteredDaemons() {
76
+ for (const record of await listRegisteredDaemons()) {
77
+ logger?.log("tools", `stopping managed daemon ${record.toolName}`);
78
+ await stopManagedDaemon(record.toolName);
79
+ }
80
+ }
81
+
82
+ async function removeOwnerFile() {
83
+ const current = await readJson(toolDaemonOwnerFile, null);
84
+ if (current?.token === token) {
85
+ await rm(toolDaemonOwnerFile, { force: true });
86
+ }
87
+ }
88
+
89
+ return {
90
+ env() {
91
+ return ownerEnv;
92
+ },
93
+
94
+ async start() {
95
+ if (running) return;
96
+ running = true;
97
+ await startHeartbeat();
98
+ await restartRegisteredDaemons();
99
+ },
100
+
101
+ async stop() {
102
+ if (!running) return;
103
+ running = false;
104
+ if (heartbeatTimer) clearInterval(heartbeatTimer);
105
+ heartbeatTimer = null;
106
+ await stopRegisteredDaemons();
107
+ await removeOwnerFile();
108
+ }
109
+ };
110
+ }