@keyou007/agent-bot 0.1.2 → 0.1.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/.env.example +3 -1
- package/CHANGELOG.md +9 -0
- package/README.md +20 -7
- package/README.zh.md +20 -7
- package/config.example.yaml +1 -0
- package/dist/cli/FeishuAppRegistration.js +12 -2
- package/dist/cli/FeishuAppRegistration.js.map +1 -1
- package/dist/cli/Initializer.js +10 -4
- package/dist/cli/Initializer.js.map +1 -1
- package/dist/cli/ServerStarter.js +69 -0
- package/dist/cli/ServerStarter.js.map +1 -0
- package/dist/cli/help.js +6 -2
- package/dist/cli/help.js.map +1 -1
- package/dist/cli/profile.js +40 -0
- package/dist/cli/profile.js.map +1 -0
- package/dist/cli.js +40 -56
- package/dist/cli.js.map +1 -1
- package/dist/config/loadConfig.js +7 -2
- package/dist/config/loadConfig.js.map +1 -1
- package/dist/config/paths.js +1 -0
- package/dist/config/paths.js.map +1 -1
- package/dist/config/schema.js +1 -0
- package/dist/config/schema.js.map +1 -1
- package/dist/feishu/CardRenderer.js +21 -3
- package/dist/feishu/CardRenderer.js.map +1 -1
- package/dist/feishu/FeishuConnector.js +7 -76
- package/dist/feishu/FeishuConnector.js.map +1 -1
- package/dist/index.js +13 -5
- package/dist/index.js.map +1 -1
- package/dist/proxy/ProxySessionController.js +18 -3
- package/dist/proxy/ProxySessionController.js.map +1 -1
- package/dist/startup/StartupNotifier.js +13 -2
- package/dist/startup/StartupNotifier.js.map +1 -1
- package/dist/startup/startFeishu.js +1 -1
- package/dist/startup/startFeishu.js.map +1 -1
- package/dist/supervision/SafeRestartNotifier.js +23 -7
- package/dist/supervision/SafeRestartNotifier.js.map +1 -1
- package/dist/supervision/SafeRestartScheduler.js +20 -2
- package/dist/supervision/SafeRestartScheduler.js.map +1 -1
- package/dist/supervision/SupervisorDiagnostics.js +209 -0
- package/dist/supervision/SupervisorDiagnostics.js.map +1 -0
- package/dist/supervisor.js +47 -12
- package/dist/supervisor.js.map +1 -1
- package/docs/technical-reference.md +23 -5
- package/docs/technical-reference.zh.md +23 -5
- package/npm-shrinkwrap.json +2 -2
- package/package.json +1 -1
- package/skills/agent-bot/SKILL.md +23 -3
- package/src/cli/FeishuAppRegistration.ts +14 -1
- package/src/cli/Initializer.ts +10 -3
- package/src/cli/ServerStarter.ts +106 -0
- package/src/cli/help.ts +6 -2
- package/src/cli/profile.ts +59 -0
- package/src/cli.ts +51 -52
- package/src/config/loadConfig.ts +12 -2
- package/src/config/paths.ts +1 -0
- package/src/config/schema.ts +1 -0
- package/src/feishu/CardRenderer.ts +25 -3
- package/src/feishu/FeishuConnector.ts +6 -87
- package/src/index.ts +17 -5
- package/src/proxy/ProxySessionController.ts +18 -3
- package/src/startup/StartupNotifier.ts +21 -4
- package/src/startup/startFeishu.ts +1 -1
- package/src/supervision/SafeRestartNotifier.ts +22 -7
- package/src/supervision/SafeRestartScheduler.ts +27 -3
- package/src/supervision/SupervisorDiagnostics.ts +268 -0
- package/src/supervisor.ts +46 -11
|
@@ -4,7 +4,12 @@ export interface ServerActivityState {
|
|
|
4
4
|
latestInboundAt?: string;
|
|
5
5
|
}
|
|
6
6
|
|
|
7
|
-
export type SafeRestartPhase =
|
|
7
|
+
export type SafeRestartPhase =
|
|
8
|
+
| "waiting_tasks"
|
|
9
|
+
| "waiting_delivery"
|
|
10
|
+
| "countdown"
|
|
11
|
+
| "restarting"
|
|
12
|
+
| "cancelled";
|
|
8
13
|
|
|
9
14
|
export interface SafeRestartStatus {
|
|
10
15
|
scheduleId: number;
|
|
@@ -56,6 +61,24 @@ export class SafeRestartScheduler {
|
|
|
56
61
|
}
|
|
57
62
|
|
|
58
63
|
cancel(): void {
|
|
64
|
+
this.clear();
|
|
65
|
+
}
|
|
66
|
+
|
|
67
|
+
async cancelScheduled(expectedScheduleId: number): Promise<boolean> {
|
|
68
|
+
const reason = this.reason;
|
|
69
|
+
if (!reason || this.scheduleId !== expectedScheduleId) return false;
|
|
70
|
+
const activity = this.options.readActivity();
|
|
71
|
+
this.clear();
|
|
72
|
+
await this.emitStatus({
|
|
73
|
+
scheduleId: expectedScheduleId,
|
|
74
|
+
reason,
|
|
75
|
+
phase: "cancelled",
|
|
76
|
+
activity,
|
|
77
|
+
});
|
|
78
|
+
return true;
|
|
79
|
+
}
|
|
80
|
+
|
|
81
|
+
private clear(): void {
|
|
59
82
|
if (this.timer) clearInterval(this.timer);
|
|
60
83
|
this.timer = undefined;
|
|
61
84
|
this.reason = undefined;
|
|
@@ -117,8 +140,9 @@ export class SafeRestartScheduler {
|
|
|
117
140
|
}
|
|
118
141
|
return;
|
|
119
142
|
}
|
|
120
|
-
|
|
121
|
-
this.
|
|
143
|
+
const scheduleId = this.scheduleId;
|
|
144
|
+
this.clear();
|
|
145
|
+
await this.emitStatus({ scheduleId, reason, phase: "restarting", activity: confirmed, remainingMs: 0 });
|
|
122
146
|
await this.options.onReady(reason);
|
|
123
147
|
}
|
|
124
148
|
|
|
@@ -0,0 +1,268 @@
|
|
|
1
|
+
import fs from "node:fs";
|
|
2
|
+
import path from "node:path";
|
|
3
|
+
import type { AppConfig } from "../config/schema.js";
|
|
4
|
+
|
|
5
|
+
const MAX_LOG_BYTES = 10 * 1024 * 1024;
|
|
6
|
+
const LOG_BACKUP_COUNT = 3;
|
|
7
|
+
|
|
8
|
+
export interface SupervisorDiagnosticsPaths {
|
|
9
|
+
supervisorLogPath: string;
|
|
10
|
+
workerStderrPath: string;
|
|
11
|
+
crashReportDirectory: string;
|
|
12
|
+
lastCrashPath: string;
|
|
13
|
+
}
|
|
14
|
+
|
|
15
|
+
export interface WorkerCrashContext {
|
|
16
|
+
workerPid?: number;
|
|
17
|
+
exitCode: number | null;
|
|
18
|
+
signal: NodeJS.Signals | null;
|
|
19
|
+
startedAt: string;
|
|
20
|
+
exitedAt: string;
|
|
21
|
+
uptimeMs: number;
|
|
22
|
+
consecutiveFailures: number;
|
|
23
|
+
restartDelayMs: number;
|
|
24
|
+
}
|
|
25
|
+
|
|
26
|
+
export interface WorkerCrashRecord extends WorkerCrashContext {
|
|
27
|
+
version: 1;
|
|
28
|
+
supervisorPid: number;
|
|
29
|
+
nodeVersion: string;
|
|
30
|
+
executablePath: string;
|
|
31
|
+
cwd: string;
|
|
32
|
+
supervisorLogPath: string;
|
|
33
|
+
workerStderrPath: string;
|
|
34
|
+
crashReportDirectory: string;
|
|
35
|
+
diagnosticReports: string[];
|
|
36
|
+
}
|
|
37
|
+
|
|
38
|
+
type ExtendedProcessReport = typeof process.report & {
|
|
39
|
+
excludeEnv?: boolean;
|
|
40
|
+
excludeNetwork?: boolean;
|
|
41
|
+
};
|
|
42
|
+
|
|
43
|
+
export class SupervisorDiagnostics {
|
|
44
|
+
readonly paths: SupervisorDiagnosticsPaths;
|
|
45
|
+
|
|
46
|
+
constructor(config: Pick<AppConfig, "logging" | "storage">) {
|
|
47
|
+
this.paths = resolveSupervisorDiagnosticsPaths(config);
|
|
48
|
+
}
|
|
49
|
+
|
|
50
|
+
initialize(): void {
|
|
51
|
+
try {
|
|
52
|
+
ensurePrivateDirectory(path.dirname(this.paths.supervisorLogPath));
|
|
53
|
+
ensurePrivateDirectory(this.paths.crashReportDirectory);
|
|
54
|
+
rotateLogFile(this.paths.supervisorLogPath);
|
|
55
|
+
rotateLogFile(this.paths.workerStderrPath);
|
|
56
|
+
this.configureCurrentProcessReports();
|
|
57
|
+
this.writeEvent("diagnostics_initialized", {
|
|
58
|
+
nodeVersion: process.versions.node,
|
|
59
|
+
supervisorLogPath: this.paths.supervisorLogPath,
|
|
60
|
+
workerStderrPath: this.paths.workerStderrPath,
|
|
61
|
+
crashReportDirectory: this.paths.crashReportDirectory,
|
|
62
|
+
});
|
|
63
|
+
} catch (error) {
|
|
64
|
+
writeFallbackDiagnostic("initialize_failed", error);
|
|
65
|
+
}
|
|
66
|
+
}
|
|
67
|
+
|
|
68
|
+
writeEvent(event: string, data: Record<string, unknown> = {}): void {
|
|
69
|
+
const line = `${JSON.stringify({
|
|
70
|
+
component: "agent-bot-supervisor",
|
|
71
|
+
event,
|
|
72
|
+
time: new Date().toISOString(),
|
|
73
|
+
...data,
|
|
74
|
+
})}\n`;
|
|
75
|
+
try {
|
|
76
|
+
fs.appendFileSync(this.paths.supervisorLogPath, line, { encoding: "utf8", mode: 0o600 });
|
|
77
|
+
restrictFilePermissions(this.paths.supervisorLogPath);
|
|
78
|
+
} catch (error) {
|
|
79
|
+
writeFallbackDiagnostic(event, error);
|
|
80
|
+
}
|
|
81
|
+
}
|
|
82
|
+
|
|
83
|
+
openWorkerStderr(): number | "ignore" {
|
|
84
|
+
try {
|
|
85
|
+
rotateLogFile(this.paths.workerStderrPath);
|
|
86
|
+
fs.appendFileSync(
|
|
87
|
+
this.paths.workerStderrPath,
|
|
88
|
+
`${JSON.stringify({ event: "worker_started", time: new Date().toISOString() })}\n`,
|
|
89
|
+
{ encoding: "utf8", mode: 0o600 },
|
|
90
|
+
);
|
|
91
|
+
restrictFilePermissions(this.paths.workerStderrPath);
|
|
92
|
+
return fs.openSync(this.paths.workerStderrPath, "a", 0o600);
|
|
93
|
+
} catch (error) {
|
|
94
|
+
this.writeEvent("worker_stderr_open_failed", { error: errorMessage(error) });
|
|
95
|
+
return "ignore";
|
|
96
|
+
}
|
|
97
|
+
}
|
|
98
|
+
|
|
99
|
+
closeWorkerStderr(target: number | "ignore"): void {
|
|
100
|
+
if (typeof target !== "number") return;
|
|
101
|
+
try {
|
|
102
|
+
fs.closeSync(target);
|
|
103
|
+
} catch (error) {
|
|
104
|
+
this.writeEvent("worker_stderr_close_failed", { error: errorMessage(error) });
|
|
105
|
+
}
|
|
106
|
+
}
|
|
107
|
+
|
|
108
|
+
recordCrash(context: WorkerCrashContext): WorkerCrashRecord | undefined {
|
|
109
|
+
try {
|
|
110
|
+
const record: WorkerCrashRecord = {
|
|
111
|
+
version: 1,
|
|
112
|
+
...context,
|
|
113
|
+
supervisorPid: process.pid,
|
|
114
|
+
nodeVersion: process.versions.node,
|
|
115
|
+
executablePath: process.execPath,
|
|
116
|
+
cwd: process.cwd(),
|
|
117
|
+
supervisorLogPath: this.paths.supervisorLogPath,
|
|
118
|
+
workerStderrPath: this.paths.workerStderrPath,
|
|
119
|
+
crashReportDirectory: this.paths.crashReportDirectory,
|
|
120
|
+
diagnosticReports: findDiagnosticReports(
|
|
121
|
+
this.paths.crashReportDirectory,
|
|
122
|
+
context.workerPid,
|
|
123
|
+
Date.parse(context.startedAt),
|
|
124
|
+
),
|
|
125
|
+
};
|
|
126
|
+
const crashPath = path.join(
|
|
127
|
+
this.paths.crashReportDirectory,
|
|
128
|
+
crashRecordFilename(context.exitedAt, context.workerPid),
|
|
129
|
+
);
|
|
130
|
+
writePrivateJson(crashPath, record);
|
|
131
|
+
writePrivateJson(this.paths.lastCrashPath, record);
|
|
132
|
+
this.writeEvent("crash_context_saved", {
|
|
133
|
+
crashPath,
|
|
134
|
+
lastCrashPath: this.paths.lastCrashPath,
|
|
135
|
+
diagnosticReports: record.diagnosticReports,
|
|
136
|
+
});
|
|
137
|
+
return record;
|
|
138
|
+
} catch (error) {
|
|
139
|
+
this.writeEvent("crash_context_save_failed", { error: errorMessage(error) });
|
|
140
|
+
return undefined;
|
|
141
|
+
}
|
|
142
|
+
}
|
|
143
|
+
|
|
144
|
+
private configureCurrentProcessReports(): void {
|
|
145
|
+
const report = process.report as ExtendedProcessReport;
|
|
146
|
+
report.directory = this.paths.crashReportDirectory;
|
|
147
|
+
report.reportOnFatalError = true;
|
|
148
|
+
report.reportOnUncaughtException = true;
|
|
149
|
+
if ("excludeEnv" in report) report.excludeEnv = true;
|
|
150
|
+
if ("excludeNetwork" in report) report.excludeNetwork = true;
|
|
151
|
+
}
|
|
152
|
+
}
|
|
153
|
+
|
|
154
|
+
export function resolveSupervisorDiagnosticsPaths(
|
|
155
|
+
config: Pick<AppConfig, "logging" | "storage">,
|
|
156
|
+
): SupervisorDiagnosticsPaths {
|
|
157
|
+
const logDirectory = path.dirname(config.logging.path);
|
|
158
|
+
const dataDirectory = path.dirname(config.storage.sqlitePath);
|
|
159
|
+
return {
|
|
160
|
+
supervisorLogPath: path.join(logDirectory, "supervisor.log"),
|
|
161
|
+
workerStderrPath: path.join(logDirectory, "worker.stderr.log"),
|
|
162
|
+
crashReportDirectory: path.join(dataDirectory, "crash-reports"),
|
|
163
|
+
lastCrashPath: path.join(dataDirectory, "last-crash.json"),
|
|
164
|
+
};
|
|
165
|
+
}
|
|
166
|
+
|
|
167
|
+
export function nodeDiagnosticReportArguments(
|
|
168
|
+
reportDirectory: string,
|
|
169
|
+
allowedFlags: ReadonlySet<string> = process.allowedNodeEnvironmentFlags,
|
|
170
|
+
): string[] {
|
|
171
|
+
const args = [
|
|
172
|
+
"--report-on-fatalerror",
|
|
173
|
+
"--report-uncaught-exception",
|
|
174
|
+
`--report-directory=${reportDirectory}`,
|
|
175
|
+
];
|
|
176
|
+
if (allowedFlags.has("--report-exclude-env")) args.push("--report-exclude-env");
|
|
177
|
+
if (allowedFlags.has("--report-exclude-network")) args.push("--report-exclude-network");
|
|
178
|
+
return args;
|
|
179
|
+
}
|
|
180
|
+
|
|
181
|
+
export function prepareCrashReportDirectory(reportDirectory: string): boolean {
|
|
182
|
+
try {
|
|
183
|
+
ensurePrivateDirectory(reportDirectory);
|
|
184
|
+
return true;
|
|
185
|
+
} catch {
|
|
186
|
+
return false;
|
|
187
|
+
}
|
|
188
|
+
}
|
|
189
|
+
|
|
190
|
+
function ensurePrivateDirectory(directoryPath: string): void {
|
|
191
|
+
fs.mkdirSync(directoryPath, { recursive: true, mode: 0o700 });
|
|
192
|
+
try {
|
|
193
|
+
fs.chmodSync(directoryPath, 0o700);
|
|
194
|
+
} catch {
|
|
195
|
+
// Windows and some mounted filesystems do not implement POSIX modes.
|
|
196
|
+
}
|
|
197
|
+
}
|
|
198
|
+
|
|
199
|
+
function rotateLogFile(filePath: string): void {
|
|
200
|
+
const size = fs.statSync(filePath, { throwIfNoEntry: false })?.size ?? 0;
|
|
201
|
+
if (size < MAX_LOG_BYTES) return;
|
|
202
|
+
for (let index = LOG_BACKUP_COUNT; index >= 1; index -= 1) {
|
|
203
|
+
const source = index === 1 ? filePath : `${filePath}.${index - 1}`;
|
|
204
|
+
const target = `${filePath}.${index}`;
|
|
205
|
+
if (!fs.existsSync(source)) continue;
|
|
206
|
+
fs.rmSync(target, { force: true });
|
|
207
|
+
fs.renameSync(source, target);
|
|
208
|
+
}
|
|
209
|
+
}
|
|
210
|
+
|
|
211
|
+
function findDiagnosticReports(
|
|
212
|
+
reportDirectory: string,
|
|
213
|
+
workerPid: number | undefined,
|
|
214
|
+
startedAtMs: number,
|
|
215
|
+
): string[] {
|
|
216
|
+
if (!fs.existsSync(reportDirectory)) return [];
|
|
217
|
+
const pidToken = workerPid === undefined ? undefined : `.${workerPid}.`;
|
|
218
|
+
return fs.readdirSync(reportDirectory, { withFileTypes: true })
|
|
219
|
+
.filter((entry) => entry.isFile() && /^report\..+\.json$/i.test(entry.name))
|
|
220
|
+
.filter((entry) => {
|
|
221
|
+
if (pidToken && entry.name.includes(pidToken)) return true;
|
|
222
|
+
const modifiedAt = fs.statSync(path.join(reportDirectory, entry.name)).mtimeMs;
|
|
223
|
+
return workerPid === undefined && modifiedAt >= startedAtMs - 1_000;
|
|
224
|
+
})
|
|
225
|
+
.map((entry) => path.join(reportDirectory, entry.name))
|
|
226
|
+
.sort();
|
|
227
|
+
}
|
|
228
|
+
|
|
229
|
+
function crashRecordFilename(exitedAt: string, workerPid: number | undefined): string {
|
|
230
|
+
const timestamp = exitedAt.replace(/\D/g, "");
|
|
231
|
+
return `crash-${timestamp}-${workerPid ?? "unknown"}.json`;
|
|
232
|
+
}
|
|
233
|
+
|
|
234
|
+
function writePrivateJson(filePath: string, value: unknown): void {
|
|
235
|
+
fs.mkdirSync(path.dirname(filePath), { recursive: true, mode: 0o700 });
|
|
236
|
+
fs.writeFileSync(filePath, `${JSON.stringify(value, null, 2)}\n`, {
|
|
237
|
+
encoding: "utf8",
|
|
238
|
+
mode: 0o600,
|
|
239
|
+
});
|
|
240
|
+
restrictFilePermissions(filePath);
|
|
241
|
+
}
|
|
242
|
+
|
|
243
|
+
function restrictFilePermissions(filePath: string): void {
|
|
244
|
+
try {
|
|
245
|
+
fs.chmodSync(filePath, 0o600);
|
|
246
|
+
} catch {
|
|
247
|
+
// Windows and some mounted filesystems do not implement POSIX modes.
|
|
248
|
+
}
|
|
249
|
+
}
|
|
250
|
+
|
|
251
|
+
function writeFallbackDiagnostic(event: string, error: unknown): void {
|
|
252
|
+
try {
|
|
253
|
+
process.stderr.write(
|
|
254
|
+
`${JSON.stringify({
|
|
255
|
+
component: "agent-bot-supervisor",
|
|
256
|
+
event,
|
|
257
|
+
time: new Date().toISOString(),
|
|
258
|
+
error: errorMessage(error),
|
|
259
|
+
})}\n`,
|
|
260
|
+
);
|
|
261
|
+
} catch {
|
|
262
|
+
// Diagnostics must never prevent the supervisor from keeping the worker alive.
|
|
263
|
+
}
|
|
264
|
+
}
|
|
265
|
+
|
|
266
|
+
function errorMessage(error: unknown): string {
|
|
267
|
+
return error instanceof Error ? error.message : String(error);
|
|
268
|
+
}
|
package/src/supervisor.ts
CHANGED
|
@@ -12,12 +12,15 @@ import {
|
|
|
12
12
|
STABLE_UPTIME_MS,
|
|
13
13
|
STOP_EXIT_CODE,
|
|
14
14
|
} from "./supervision/restartPolicy.js";
|
|
15
|
+
import { SupervisorDiagnostics, nodeDiagnosticReportArguments } from "./supervision/SupervisorDiagnostics.js";
|
|
15
16
|
|
|
16
17
|
const childEntry = fileURLToPath(new URL("./index.js", import.meta.url));
|
|
17
18
|
const config = loadConfig();
|
|
18
19
|
requireServerFeishuTransport(config.feishu);
|
|
19
20
|
const sqlitePath = config.storage.sqlitePath;
|
|
20
21
|
const serverEndpoint = controlEndpoint(sqlitePath);
|
|
22
|
+
const diagnostics = new SupervisorDiagnostics(config);
|
|
23
|
+
diagnostics.initialize();
|
|
21
24
|
let child: ChildProcess | undefined;
|
|
22
25
|
let restartTimer: NodeJS.Timeout | undefined;
|
|
23
26
|
let stopping = false;
|
|
@@ -34,19 +37,33 @@ async function startChild(): Promise<void> {
|
|
|
34
37
|
const startedAt = Date.now();
|
|
35
38
|
const restartReason = nextStartReason;
|
|
36
39
|
nextStartReason = "Supervisor 重新拉起进程";
|
|
37
|
-
|
|
38
|
-
|
|
39
|
-
|
|
40
|
-
|
|
41
|
-
|
|
40
|
+
const workerStderr = diagnostics.openWorkerStderr();
|
|
41
|
+
try {
|
|
42
|
+
child = spawn(process.execPath, [
|
|
43
|
+
...nodeDiagnosticReportArguments(diagnostics.paths.crashReportDirectory),
|
|
44
|
+
childEntry,
|
|
45
|
+
], {
|
|
46
|
+
cwd: process.cwd(),
|
|
47
|
+
env: { ...process.env, AGENT_BOT_SUPERVISED: "1", AGENT_BOT_RESTART_REASON: restartReason },
|
|
48
|
+
stdio: ["ignore", "ignore", workerStderr],
|
|
49
|
+
windowsHide: true,
|
|
50
|
+
});
|
|
51
|
+
} finally {
|
|
52
|
+
diagnostics.closeWorkerStderr(workerStderr);
|
|
53
|
+
}
|
|
54
|
+
const workerPid = child.pid;
|
|
55
|
+
writeSupervisorLog("started", {
|
|
56
|
+
pid: child.pid,
|
|
57
|
+
restartReason,
|
|
58
|
+
workerStderrPath: diagnostics.paths.workerStderrPath,
|
|
59
|
+
crashReportDirectory: diagnostics.paths.crashReportDirectory,
|
|
42
60
|
});
|
|
43
|
-
writeSupervisorLog("started", { pid: child.pid, restartReason });
|
|
44
61
|
|
|
45
62
|
child.once("error", (error) => {
|
|
46
|
-
writeSupervisorLog("spawn_error", { error: error.message });
|
|
63
|
+
writeSupervisorLog("spawn_error", { pid: workerPid, error: error.message });
|
|
47
64
|
});
|
|
48
65
|
child.once("exit", (code, signal) => {
|
|
49
|
-
void handleChildExit(code, signal, startedAt);
|
|
66
|
+
void handleChildExit(code, signal, startedAt, workerPid);
|
|
50
67
|
});
|
|
51
68
|
}
|
|
52
69
|
|
|
@@ -54,10 +71,11 @@ async function handleChildExit(
|
|
|
54
71
|
code: number | null,
|
|
55
72
|
signal: NodeJS.Signals | null,
|
|
56
73
|
startedAt: number,
|
|
74
|
+
workerPid: number | undefined,
|
|
57
75
|
): Promise<void> {
|
|
58
76
|
child = undefined;
|
|
59
77
|
const uptimeMs = Date.now() - startedAt;
|
|
60
|
-
writeSupervisorLog("exited", { code, signal, uptimeMs });
|
|
78
|
+
writeSupervisorLog("exited", { pid: workerPid, code, signal, uptimeMs });
|
|
61
79
|
if (stopping) {
|
|
62
80
|
process.exit(0);
|
|
63
81
|
return;
|
|
@@ -80,7 +98,24 @@ async function handleChildExit(
|
|
|
80
98
|
const delayMs = intentional
|
|
81
99
|
? INTENTIONAL_RESTART_DELAY_MS
|
|
82
100
|
: crashRestartDelayMs(consecutiveFailures);
|
|
83
|
-
|
|
101
|
+
if (!intentional) {
|
|
102
|
+
diagnostics.recordCrash({
|
|
103
|
+
workerPid,
|
|
104
|
+
exitCode: code,
|
|
105
|
+
signal,
|
|
106
|
+
startedAt: new Date(startedAt).toISOString(),
|
|
107
|
+
exitedAt: new Date().toISOString(),
|
|
108
|
+
uptimeMs,
|
|
109
|
+
consecutiveFailures,
|
|
110
|
+
restartDelayMs: delayMs,
|
|
111
|
+
});
|
|
112
|
+
}
|
|
113
|
+
writeSupervisorLog("restarting", {
|
|
114
|
+
previousPid: workerPid,
|
|
115
|
+
delayMs,
|
|
116
|
+
consecutiveFailures,
|
|
117
|
+
intentional,
|
|
118
|
+
});
|
|
84
119
|
restartTimer = setTimeout(() => void startChild(), delayMs);
|
|
85
120
|
}
|
|
86
121
|
|
|
@@ -96,7 +131,7 @@ function stop(signal: NodeJS.Signals): void {
|
|
|
96
131
|
}
|
|
97
132
|
|
|
98
133
|
function writeSupervisorLog(event: string, data: Record<string, unknown>): void {
|
|
99
|
-
|
|
134
|
+
diagnostics.writeEvent(event, data);
|
|
100
135
|
}
|
|
101
136
|
|
|
102
137
|
process.on("SIGINT", () => stop("SIGINT"));
|