agentel 0.2.0 → 0.2.3

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/src/supervisor.js CHANGED
@@ -6,18 +6,21 @@ const { spawn } = require("child_process");
6
6
  const { ensureBaseDirs, paths, readJson, writeJson } = require("./paths");
7
7
  const { effectiveImportSources, loadConfig } = require("./config");
8
8
  const { startCollector } = require("./collector");
9
- const { reindexIfNeeded } = require("./search");
10
- const { hasRemoteTarget, syncArchiveIfConfigured } = require("./sync");
9
+ const { hasRemoteTarget } = require("./sync");
11
10
 
12
11
  let tickRunning = false;
12
+ const CHILD_MAX_OUTPUT_BYTES = 10 * 1024 * 1024;
13
+ const CHILD_TIMEOUT_MS = 10 * 60 * 1000;
14
+ const CHILD_KILL_GRACE_MS = 2000;
13
15
 
14
- function startSupervisorDetached() {
15
- const p = paths();
16
+ function startSupervisorDetached(env = process.env) {
17
+ const p = paths(env);
16
18
  ensureBaseDirs(p);
17
19
  const out = fs.openSync(path.join(p.logs, "supervisor.log"), "a");
18
20
  const err = fs.openSync(path.join(p.logs, "supervisor.err.log"), "a");
19
21
  const child = spawn(process.execPath, [path.resolve(__dirname, "..", "bin", "agentlog.js"), "start", "--foreground"], {
20
22
  detached: true,
23
+ env,
21
24
  stdio: ["ignore", out, err]
22
25
  });
23
26
  child.unref();
@@ -39,11 +42,7 @@ async function runSupervisorForeground(env = process.env) {
39
42
  if (stopping) return;
40
43
  stopping = true;
41
44
  log("supervisor stopping", env);
42
- try {
43
- fs.unlinkSync(p.pid);
44
- } catch {
45
- // Already gone.
46
- }
45
+ removeSupervisorPidIfOwned(process.pid, env);
47
46
  process.exit(0);
48
47
  };
49
48
  process.on("SIGTERM", stop);
@@ -64,49 +63,200 @@ async function runSupervisorForeground(env = process.env) {
64
63
  async function tick(env) {
65
64
  tickRunning = true;
66
65
  try {
67
- const cfg = loadConfig(env);
68
- const sources = effectiveImportSources(cfg);
69
- const importSources = Array.isArray(sources) && sources.length ? sources : ["all"];
70
- for (const source of importSources) {
71
- try {
72
- const importCliHistory = loadImportCliHistory();
73
- const imports = importCliHistory(supervisorImportOptionsForSource(source, cfg), env);
74
- for (const result of imports) {
75
- const summary = supervisorImportResultLog(result, source);
76
- if (summary) log(summary, env);
77
- if (result.errors?.length) log(`${result.provider} import errors from ${source}: ${result.errors.length}`, env);
66
+ try {
67
+ const cfg = loadConfig(env);
68
+ const sources = effectiveImportSources(cfg);
69
+ const importSources = Array.isArray(sources) && sources.length ? sources : ["all"];
70
+ for (const source of importSources) {
71
+ try {
72
+ const imports = await importSourceInChild(source, cfg, env);
73
+ for (const result of imports) {
74
+ const summary = supervisorImportResultLog(result, source);
75
+ if (summary) log(summary, env);
76
+ if (result.errors?.length) log(`${result.provider} import errors from ${source}: ${result.errors.length}`, env);
77
+ }
78
+ } catch (error) {
79
+ log(`${source} history import skipped: ${error.message}`, env);
78
80
  }
79
- } catch (error) {
80
- log(`${source} history import skipped: ${error.message}`, env);
81
81
  }
82
+ } catch (error) {
83
+ log(`history import skipped: ${error.message}`, env);
82
84
  }
83
- } catch (error) {
84
- log(`history import skipped: ${error.message}`, env);
85
- }
86
85
 
87
- try {
88
- const result = reindexIfNeeded(env);
89
- if (result.index?.docCount != null) log(`index ready (${result.index.docCount} chunk(s))`, env);
90
- } catch (error) {
91
- log(`index skipped: ${error.message}`, env);
92
- }
86
+ try {
87
+ const result = await reindexInChild(env);
88
+ if (result.index?.docCount != null) log(`index ready (${result.index.docCount} chunk(s))`, env);
89
+ } catch (error) {
90
+ log(`index skipped: ${error.message}`, env);
91
+ }
93
92
 
94
- try {
95
- const cfg = loadConfig(env);
96
- if (hasRemoteTarget(cfg, env) && shouldRunScheduledSync(cfg, env)) {
97
- markScheduledSyncAttempt(env);
98
- const result = await syncArchiveIfConfigured(env);
99
- if (result.configured !== false && (result.uploaded || result.errors?.length)) {
100
- log(`remote sync uploaded=${result.uploaded} current=${result.skipped} retried=${result.retried || 0} errors=${result.errors.length}`, env);
93
+ try {
94
+ const cfg = loadConfig(env);
95
+ if (hasRemoteTarget(cfg, env) && shouldRunScheduledSync(cfg, env)) {
96
+ markScheduledSyncAttempt(env);
97
+ const result = await syncInChild(env);
98
+ if (result.configured !== false && (result.uploaded || result.errors?.length)) {
99
+ log(`remote sync uploaded=${result.uploaded} current=${result.skipped} retried=${result.retried || 0} errors=${result.errors.length}`, env);
100
+ }
101
101
  }
102
+ } catch (error) {
103
+ log(`remote sync skipped: ${error.message}`, env);
102
104
  }
103
- } catch (error) {
104
- log(`remote sync skipped: ${error.message}`, env);
105
105
  } finally {
106
106
  tickRunning = false;
107
107
  }
108
108
  }
109
109
 
110
+ function importSourceInChild(source, config, env = process.env) {
111
+ const options = supervisorImportOptionsForSource(source, config);
112
+ const title = childProcessTitle("import", source);
113
+ const script = `
114
+ process.title = ${JSON.stringify(title)};
115
+ try {
116
+ const { importCliHistory } = require(${JSON.stringify(path.join(__dirname, "importers"))});
117
+ const results = importCliHistory(${JSON.stringify(options)}, process.env);
118
+ process.stdout.write(JSON.stringify(results));
119
+ } catch (error) {
120
+ console.error(error && error.message ? error.message : String(error));
121
+ process.exit(1);
122
+ }
123
+ `;
124
+ return runJsonChild(script, env, `${source} import`, title);
125
+ }
126
+
127
+ function reindexInChild(env = process.env) {
128
+ const title = childProcessTitle("index");
129
+ const script = `
130
+ process.title = ${JSON.stringify(title)};
131
+ try {
132
+ const { reindexIfNeeded } = require(${JSON.stringify(path.join(__dirname, "search"))});
133
+ const result = reindexIfNeeded(process.env, { rebuildInProcess: true });
134
+ process.stdout.write(JSON.stringify({
135
+ paused: Boolean(result.paused),
136
+ rebuilt: Boolean(result.rebuilt),
137
+ index: result.index
138
+ ? {
139
+ version: result.index.version,
140
+ builtAt: result.index.builtAt,
141
+ docCount: result.index.docCount,
142
+ avgDocLength: result.index.avgDocLength,
143
+ summaryOnly: true
144
+ }
145
+ : null
146
+ }));
147
+ } catch (error) {
148
+ console.error(error && error.message ? error.message : String(error));
149
+ process.exit(1);
150
+ }
151
+ `;
152
+ return runJsonChild(script, env, "index", title);
153
+ }
154
+
155
+ function syncInChild(env = process.env) {
156
+ const title = childProcessTitle("sync");
157
+ const script = `
158
+ process.title = ${JSON.stringify(title)};
159
+ (async () => {
160
+ const { syncArchiveIfConfigured } = require(${JSON.stringify(path.join(__dirname, "sync"))});
161
+ const result = await syncArchiveIfConfigured(process.env);
162
+ process.stdout.write(JSON.stringify(result));
163
+ })().catch((error) => {
164
+ console.error(error && error.message ? error.message : String(error));
165
+ process.exitCode = 1;
166
+ });
167
+ `;
168
+ return runJsonChild(script, env, "remote sync", title);
169
+ }
170
+
171
+ function runJsonChild(script, env = process.env, label = "child process", title = childProcessTitle("worker"), options = {}) {
172
+ return new Promise((resolve, reject) => {
173
+ const child = spawn(process.execPath, ["-e", script], {
174
+ argv0: title,
175
+ env,
176
+ stdio: ["ignore", "pipe", "pipe"]
177
+ });
178
+ const stdout = [];
179
+ const stderr = [];
180
+ let stdoutBytes = 0;
181
+ let stderrBytes = 0;
182
+ let settled = false;
183
+ let timedOut = false;
184
+ let killTimer = null;
185
+ const timeoutMs = normalizeChildTimeout(options.timeoutMs);
186
+ const timeoutTimer = setTimeout(() => {
187
+ timedOut = true;
188
+ child.kill("SIGTERM");
189
+ killTimer = setTimeout(() => child.kill("SIGKILL"), CHILD_KILL_GRACE_MS);
190
+ if (typeof killTimer.unref === "function") killTimer.unref();
191
+ }, timeoutMs);
192
+ if (typeof timeoutTimer.unref === "function") timeoutTimer.unref();
193
+
194
+ function clearTimers() {
195
+ clearTimeout(timeoutTimer);
196
+ if (killTimer) clearTimeout(killTimer);
197
+ }
198
+
199
+ function fail(error) {
200
+ if (settled) return;
201
+ settled = true;
202
+ clearTimers();
203
+ reject(error);
204
+ }
205
+
206
+ child.stdout.on("data", (chunk) => {
207
+ stdoutBytes += chunk.length;
208
+ if (stdoutBytes > CHILD_MAX_OUTPUT_BYTES) {
209
+ child.kill("SIGTERM");
210
+ fail(new Error(`${label} output exceeded ${CHILD_MAX_OUTPUT_BYTES} bytes`));
211
+ return;
212
+ }
213
+ stdout.push(chunk);
214
+ });
215
+ child.stderr.on("data", (chunk) => {
216
+ stderrBytes += chunk.length;
217
+ if (stderrBytes <= CHILD_MAX_OUTPUT_BYTES) stderr.push(chunk);
218
+ });
219
+ child.on("error", fail);
220
+ child.on("close", (code, signal) => {
221
+ if (settled) return;
222
+ settled = true;
223
+ clearTimers();
224
+ if (timedOut) {
225
+ reject(new Error(`${label} timed out after ${formatDuration(timeoutMs)}`));
226
+ return;
227
+ }
228
+ const output = Buffer.concat(stdout).toString("utf8").trim();
229
+ const errorOutput = Buffer.concat(stderr).toString("utf8").trim();
230
+ if (code !== 0) {
231
+ reject(new Error(errorOutput || `${label} exited with ${signal || `status ${code}`}`));
232
+ return;
233
+ }
234
+ try {
235
+ resolve(output ? JSON.parse(output) : null);
236
+ } catch (error) {
237
+ reject(new Error(`${label} returned invalid JSON: ${error.message}`));
238
+ }
239
+ });
240
+ });
241
+ }
242
+
243
+ function normalizeChildTimeout(value) {
244
+ const timeout = Number(value || CHILD_TIMEOUT_MS);
245
+ return Number.isFinite(timeout) && timeout > 0 ? timeout : CHILD_TIMEOUT_MS;
246
+ }
247
+
248
+ function formatDuration(ms) {
249
+ const seconds = Math.round(ms / 1000);
250
+ if (seconds < 60) return `${seconds}s`;
251
+ const minutes = Math.round(seconds / 60);
252
+ return `${minutes}m`;
253
+ }
254
+
255
+ function childProcessTitle(kind, detail = "") {
256
+ const suffix = detail ? `-${String(detail).replace(/[^A-Za-z0-9._-]+/g, "-").replace(/^-+|-+$/g, "")}` : "";
257
+ return `agentlog-${kind}${suffix}`.slice(0, 64);
258
+ }
259
+
110
260
  function shouldRunScheduledSync(config, env = process.env, now = new Date()) {
111
261
  const intervalMinutes = Number(config?.sync?.intervalMinutes ?? 30);
112
262
  if (!Number.isFinite(intervalMinutes) || intervalMinutes <= 0) return false;
@@ -143,7 +293,6 @@ function supervisorImportResultLog(result, source) {
143
293
  }
144
294
 
145
295
  function stopSupervisor(env = process.env) {
146
- const p = paths(env);
147
296
  const pid = readPid(env);
148
297
  if (!pid) return false;
149
298
  try {
@@ -151,11 +300,7 @@ function stopSupervisor(env = process.env) {
151
300
  return true;
152
301
  } catch (error) {
153
302
  if (error?.code !== "EPERM") {
154
- try {
155
- fs.unlinkSync(p.pid);
156
- } catch {
157
- // No-op.
158
- }
303
+ removeSupervisorPidIfOwned(pid, env);
159
304
  }
160
305
  return false;
161
306
  }
@@ -178,6 +323,16 @@ function readPid(env = process.env) {
178
323
  }
179
324
  }
180
325
 
326
+ function removeSupervisorPidIfOwned(pid, env = process.env) {
327
+ if (!pid || readPid(env) !== Number(pid)) return false;
328
+ try {
329
+ fs.unlinkSync(paths(env).pid);
330
+ return true;
331
+ } catch {
332
+ return false;
333
+ }
334
+ }
335
+
181
336
  function isAlive(pid) {
182
337
  try {
183
338
  process.kill(pid, 0);
@@ -188,17 +343,6 @@ function isAlive(pid) {
188
343
  }
189
344
  }
190
345
 
191
- function loadImportCliHistory() {
192
- const modulePath = require.resolve("./importers");
193
- const loaded = require(modulePath);
194
- if (typeof loaded.importCliHistory === "function") return loaded.importCliHistory;
195
-
196
- delete require.cache[modulePath];
197
- const reloaded = require(modulePath);
198
- if (typeof reloaded.importCliHistory === "function") return reloaded.importCliHistory;
199
- throw new Error("importer module did not expose importCliHistory; restart agentlog or reinstall");
200
- }
201
-
202
346
  function log(message, env = process.env) {
203
347
  const p = paths(env);
204
348
  ensureBaseDirs(p);
@@ -212,6 +356,11 @@ module.exports = {
212
356
  supervisorImportResultLog,
213
357
  supervisorImportOptionsForSource,
214
358
  supervisorStatus,
359
+ removeSupervisorPidIfOwned,
215
360
  shouldRunScheduledSync,
361
+ importSourceInChild,
362
+ reindexInChild,
363
+ runJsonChild,
364
+ syncInChild,
216
365
  tick
217
366
  };