doer-agent 0.4.8 → 0.4.9

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.
@@ -105,7 +105,13 @@ function normalizeLogEvent(value) {
105
105
  }
106
106
  const row = value;
107
107
  const ts = typeof row.ts === "string" ? row.ts.trim() : "";
108
- const type = row.type === "start" || row.type === "stdout" || row.type === "stderr" || row.type === "exit" || row.type === "signal" || row.type === "error"
108
+ const type = row.type === "start" ||
109
+ row.type === "stdout" ||
110
+ row.type === "stderr" ||
111
+ row.type === "heartbeat" ||
112
+ row.type === "exit" ||
113
+ row.type === "signal" ||
114
+ row.type === "error"
109
115
  ? row.type
110
116
  : null;
111
117
  if (!ts || !type) {
@@ -1,5 +1,6 @@
1
1
  import { spawn } from "node:child_process";
2
2
  import { appendFile, readFile, writeFile } from "node:fs/promises";
3
+ const DEFAULT_HEARTBEAT_MS = 15_000;
3
4
  function readRequiredEnv(name) {
4
5
  const value = process.env[name]?.trim() || "";
5
6
  if (!value) {
@@ -7,6 +8,17 @@ function readRequiredEnv(name) {
7
8
  }
8
9
  return value;
9
10
  }
11
+ function readHeartbeatIntervalMs() {
12
+ const raw = process.env.DOER_DAEMON_HEARTBEAT_MS?.trim();
13
+ if (!raw) {
14
+ return DEFAULT_HEARTBEAT_MS;
15
+ }
16
+ const numeric = Number(raw);
17
+ if (!Number.isFinite(numeric) || numeric < 1_000) {
18
+ return DEFAULT_HEARTBEAT_MS;
19
+ }
20
+ return Math.floor(numeric);
21
+ }
10
22
  async function readState(statePath) {
11
23
  const raw = await readFile(statePath, "utf8");
12
24
  return JSON.parse(raw);
@@ -25,13 +37,14 @@ async function appendEvent(eventsPath, event) {
25
37
  };
26
38
  await appendFile(eventsPath, `${JSON.stringify(row)}\n`, "utf8");
27
39
  }
28
- function attachLineLogger(stream, type, eventsPath, pid) {
40
+ function attachLineLogger(stream, type, eventsPath, pid, onActivity) {
29
41
  if (!stream) {
30
42
  return;
31
43
  }
32
44
  stream.setEncoding("utf8");
33
45
  let pending = "";
34
46
  stream.on("data", (chunk) => {
47
+ onActivity?.();
35
48
  pending += chunk;
36
49
  const lines = pending.split(/\r\n|\n|\r/);
37
50
  pending = lines.pop() ?? "";
@@ -61,6 +74,7 @@ async function main() {
61
74
  const command = readRequiredEnv("DOER_DAEMON_COMMAND");
62
75
  const cwd = readRequiredEnv("DOER_DAEMON_CWD");
63
76
  const shellPath = readRequiredEnv("DOER_DAEMON_SHELL_PATH");
77
+ const heartbeatIntervalMs = readHeartbeatIntervalMs();
64
78
  const childEnv = { ...process.env };
65
79
  delete childEnv.DOER_DAEMON_STATE_PATH;
66
80
  delete childEnv.DOER_DAEMON_EVENTS_PATH;
@@ -94,8 +108,28 @@ async function main() {
94
108
  type: "start",
95
109
  pid: child.pid,
96
110
  });
97
- attachLineLogger(child.stdout, "stdout", eventsPath, child.pid);
98
- attachLineLogger(child.stderr, "stderr", eventsPath, child.pid);
111
+ let lastActivityAt = Date.now();
112
+ const markActivity = () => {
113
+ lastActivityAt = Date.now();
114
+ };
115
+ attachLineLogger(child.stdout, "stdout", eventsPath, child.pid, markActivity);
116
+ attachLineLogger(child.stderr, "stderr", eventsPath, child.pid, markActivity);
117
+ const heartbeatTimer = setInterval(() => {
118
+ if (child.exitCode !== null || child.killed) {
119
+ return;
120
+ }
121
+ const idleMs = Date.now() - lastActivityAt;
122
+ if (idleMs < heartbeatIntervalMs) {
123
+ return;
124
+ }
125
+ lastActivityAt = Date.now();
126
+ void appendEvent(eventsPath, {
127
+ type: "heartbeat",
128
+ pid: child.pid,
129
+ text: `[doer-daemon] heartbeat: process still running without new output for ${Math.max(1, Math.round(idleMs / 1000))}s`,
130
+ });
131
+ }, heartbeatIntervalMs);
132
+ heartbeatTimer.unref?.();
99
133
  const forwardSignal = (signal) => {
100
134
  if (child.exitCode !== null || child.killed) {
101
135
  return;
@@ -122,6 +156,7 @@ async function main() {
122
156
  child.once("error", reject);
123
157
  child.once("exit", async (code, signal) => {
124
158
  try {
159
+ clearInterval(heartbeatTimer);
125
160
  const latest = await readState(statePath);
126
161
  await writeState(statePath, {
127
162
  ...latest,
@@ -140,6 +175,7 @@ async function main() {
140
175
  resolve();
141
176
  }
142
177
  catch (error) {
178
+ clearInterval(heartbeatTimer);
143
179
  reject(error);
144
180
  }
145
181
  });
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "doer-agent",
3
- "version": "0.4.8",
3
+ "version": "0.4.9",
4
4
  "description": "Reverse-polling agent runtime for doer",
5
5
  "type": "module",
6
6
  "main": "dist/agent.js",