@useorgx/openclaw-plugin 0.4.1 → 0.4.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/README.md +4 -0
- package/dist/byok-store.js +181 -54
- package/dist/gateway-watchdog-runner.d.ts +1 -0
- package/dist/gateway-watchdog-runner.js +6 -0
- package/dist/gateway-watchdog.d.ts +11 -0
- package/dist/gateway-watchdog.js +221 -0
- package/dist/http-handler.js +705 -78
- package/dist/index.js +7 -0
- package/dist/openclaw-settings.d.ts +17 -0
- package/dist/openclaw-settings.js +118 -0
- package/package.json +1 -1
package/README.md
CHANGED
|
@@ -142,9 +142,13 @@ npm run job:dispatch -- \
|
|
|
142
142
|
|
|
143
143
|
Key behavior:
|
|
144
144
|
- Pulls tasks from OrgX for selected workstreams
|
|
145
|
+
- Runs `orgx_spawn_check` preflight per task before dispatch
|
|
146
|
+
- Injects required OrgX skill context (for example `orgx-engineering-agent`) into worker prompts
|
|
147
|
+
- Applies the same spawn-guard + skill-policy enforcement to manual launch, restart, and Next Up fallback dispatch paths
|
|
145
148
|
- Spawns parallel Codex workers per task
|
|
146
149
|
- Retries failures with backoff up to `--max_attempts`
|
|
147
150
|
- Emits activity and task status transitions into OrgX DB
|
|
151
|
+
- Auto-creates a blocking decision when a task exhausts retries (disable with `--decision_on_block=false`)
|
|
148
152
|
- Persists resumable state to `.orgx-codex-jobs/<job-id>/job-state.json`
|
|
149
153
|
|
|
150
154
|
Notes:
|
package/dist/byok-store.js
CHANGED
|
@@ -1,14 +1,76 @@
|
|
|
1
|
-
import { chmodSync, existsSync, mkdirSync, readFileSync,
|
|
2
|
-
import {
|
|
1
|
+
import { chmodSync, existsSync, mkdirSync, readFileSync, statSync } from "node:fs";
|
|
2
|
+
import { join } from "node:path";
|
|
3
|
+
import { getOpenClawDir } from "./paths.js";
|
|
3
4
|
import { backupCorruptFileSync, writeJsonFileAtomicSync } from "./fs-utils.js";
|
|
4
|
-
|
|
5
|
-
|
|
5
|
+
const PROVIDER_PROFILE_MAP = {
|
|
6
|
+
openaiApiKey: { profileId: "openai-codex", provider: "openai-codex" },
|
|
7
|
+
anthropicApiKey: { profileId: "anthropic", provider: "anthropic" },
|
|
8
|
+
openrouterApiKey: { profileId: "openrouter", provider: "openrouter" },
|
|
9
|
+
};
|
|
10
|
+
function isSafePathSegment(value) {
|
|
11
|
+
const normalized = value.trim();
|
|
12
|
+
if (!normalized || normalized === "." || normalized === "..")
|
|
13
|
+
return false;
|
|
14
|
+
if (normalized.includes("/") || normalized.includes("\\") || normalized.includes("\0")) {
|
|
15
|
+
return false;
|
|
16
|
+
}
|
|
17
|
+
if (normalized.includes(".."))
|
|
18
|
+
return false;
|
|
19
|
+
return true;
|
|
20
|
+
}
|
|
21
|
+
function parseJson(value) {
|
|
22
|
+
try {
|
|
23
|
+
return JSON.parse(value);
|
|
24
|
+
}
|
|
25
|
+
catch {
|
|
26
|
+
return null;
|
|
27
|
+
}
|
|
6
28
|
}
|
|
7
|
-
function
|
|
8
|
-
return
|
|
29
|
+
function readObject(value) {
|
|
30
|
+
return value && typeof value === "object" && !Array.isArray(value)
|
|
31
|
+
? value
|
|
32
|
+
: {};
|
|
9
33
|
}
|
|
10
|
-
function
|
|
11
|
-
|
|
34
|
+
function resolveDefaultAgentId() {
|
|
35
|
+
try {
|
|
36
|
+
const configPath = join(getOpenClawDir(), "openclaw.json");
|
|
37
|
+
if (!existsSync(configPath))
|
|
38
|
+
return "main";
|
|
39
|
+
const raw = parseJson(readFileSync(configPath, "utf8"));
|
|
40
|
+
const agents = readObject(raw?.agents);
|
|
41
|
+
const list = Array.isArray(agents.list) ? agents.list : [];
|
|
42
|
+
for (const entry of list) {
|
|
43
|
+
if (!entry || typeof entry !== "object")
|
|
44
|
+
continue;
|
|
45
|
+
const row = entry;
|
|
46
|
+
if (row.default !== true)
|
|
47
|
+
continue;
|
|
48
|
+
const id = typeof row.id === "string" ? row.id.trim() : "";
|
|
49
|
+
if (id && isSafePathSegment(id))
|
|
50
|
+
return id;
|
|
51
|
+
}
|
|
52
|
+
for (const entry of list) {
|
|
53
|
+
if (!entry || typeof entry !== "object")
|
|
54
|
+
continue;
|
|
55
|
+
const row = entry;
|
|
56
|
+
const id = typeof row.id === "string" ? row.id.trim() : "";
|
|
57
|
+
if (id && isSafePathSegment(id))
|
|
58
|
+
return id;
|
|
59
|
+
}
|
|
60
|
+
}
|
|
61
|
+
catch {
|
|
62
|
+
// fall through
|
|
63
|
+
}
|
|
64
|
+
return "main";
|
|
65
|
+
}
|
|
66
|
+
function authProfilesDir() {
|
|
67
|
+
return join(getOpenClawDir(), "agents", resolveDefaultAgentId(), "agent");
|
|
68
|
+
}
|
|
69
|
+
function authProfilesFile() {
|
|
70
|
+
return join(authProfilesDir(), "auth-profiles.json");
|
|
71
|
+
}
|
|
72
|
+
function ensureAuthProfilesDir() {
|
|
73
|
+
const dir = authProfilesDir();
|
|
12
74
|
mkdirSync(dir, { recursive: true, mode: 0o700 });
|
|
13
75
|
try {
|
|
14
76
|
chmodSync(dir, 0o700);
|
|
@@ -17,12 +79,56 @@ function ensureConfigDir() {
|
|
|
17
79
|
// best effort
|
|
18
80
|
}
|
|
19
81
|
}
|
|
20
|
-
function
|
|
82
|
+
function normalizeAuthProfileEntry(value) {
|
|
83
|
+
if (!value || typeof value !== "object")
|
|
84
|
+
return null;
|
|
85
|
+
const row = value;
|
|
86
|
+
const type = typeof row.type === "string" ? row.type.trim() : "";
|
|
87
|
+
const provider = typeof row.provider === "string" ? row.provider.trim() : "";
|
|
88
|
+
const key = typeof row.key === "string" ? row.key.trim() : "";
|
|
89
|
+
if (!type || !provider || !key)
|
|
90
|
+
return null;
|
|
91
|
+
return { type, provider, key };
|
|
92
|
+
}
|
|
93
|
+
function readAuthProfiles() {
|
|
94
|
+
const file = authProfilesFile();
|
|
21
95
|
try {
|
|
22
|
-
|
|
96
|
+
if (!existsSync(file))
|
|
97
|
+
return { file, parsed: null };
|
|
98
|
+
const raw = readFileSync(file, "utf8");
|
|
99
|
+
const parsed = parseJson(raw);
|
|
100
|
+
if (!parsed || typeof parsed !== "object") {
|
|
101
|
+
backupCorruptFileSync(file);
|
|
102
|
+
return { file, parsed: null };
|
|
103
|
+
}
|
|
104
|
+
const profilesRaw = parsed.profiles && typeof parsed.profiles === "object"
|
|
105
|
+
? parsed.profiles
|
|
106
|
+
: {};
|
|
107
|
+
const profiles = {};
|
|
108
|
+
for (const [profileId, entry] of Object.entries(profilesRaw)) {
|
|
109
|
+
const normalized = normalizeAuthProfileEntry(entry);
|
|
110
|
+
if (!normalized)
|
|
111
|
+
continue;
|
|
112
|
+
profiles[profileId] = normalized;
|
|
113
|
+
}
|
|
114
|
+
return {
|
|
115
|
+
file,
|
|
116
|
+
parsed: {
|
|
117
|
+
version: typeof parsed.version === "number" && Number.isFinite(parsed.version)
|
|
118
|
+
? Math.floor(parsed.version)
|
|
119
|
+
: 1,
|
|
120
|
+
profiles,
|
|
121
|
+
lastGood: parsed.lastGood && typeof parsed.lastGood === "object"
|
|
122
|
+
? parsed.lastGood
|
|
123
|
+
: undefined,
|
|
124
|
+
usageStats: parsed.usageStats && typeof parsed.usageStats === "object"
|
|
125
|
+
? parsed.usageStats
|
|
126
|
+
: undefined,
|
|
127
|
+
},
|
|
128
|
+
};
|
|
23
129
|
}
|
|
24
130
|
catch {
|
|
25
|
-
return null;
|
|
131
|
+
return { file, parsed: null };
|
|
26
132
|
}
|
|
27
133
|
}
|
|
28
134
|
function normalizeKey(value) {
|
|
@@ -33,29 +139,29 @@ function normalizeKey(value) {
|
|
|
33
139
|
return null;
|
|
34
140
|
return trimmed;
|
|
35
141
|
}
|
|
142
|
+
function findProfileKey(profiles, provider) {
|
|
143
|
+
const entries = Object.entries(profiles);
|
|
144
|
+
if (provider === "openai") {
|
|
145
|
+
const codex = entries.find(([, entry]) => entry.provider === "openai-codex");
|
|
146
|
+
if (codex)
|
|
147
|
+
return codex[1].key;
|
|
148
|
+
const openai = entries.find(([, entry]) => entry.provider === "openai");
|
|
149
|
+
return openai?.[1].key ?? null;
|
|
150
|
+
}
|
|
151
|
+
return entries.find(([, entry]) => entry.provider === provider)?.[1].key ?? null;
|
|
152
|
+
}
|
|
36
153
|
export function readByokKeys() {
|
|
37
|
-
const file =
|
|
154
|
+
const { file, parsed } = readAuthProfiles();
|
|
38
155
|
try {
|
|
39
|
-
if (!
|
|
40
|
-
return null;
|
|
41
|
-
const raw = readFileSync(file, "utf8");
|
|
42
|
-
const parsed = parseJson(raw);
|
|
43
|
-
if (!parsed) {
|
|
44
|
-
backupCorruptFileSync(file);
|
|
45
|
-
return null;
|
|
46
|
-
}
|
|
47
|
-
if (!parsed || typeof parsed !== "object")
|
|
156
|
+
if (!parsed)
|
|
48
157
|
return null;
|
|
49
|
-
const
|
|
50
|
-
|
|
51
|
-
|
|
52
|
-
const updatedAt = typeof parsed.updatedAt === "string" && parsed.updatedAt.trim().length > 0
|
|
53
|
-
? parsed.updatedAt
|
|
54
|
-
: createdAt;
|
|
158
|
+
const stats = statSync(file);
|
|
159
|
+
const createdAt = stats.birthtime.toISOString();
|
|
160
|
+
const updatedAt = stats.mtime.toISOString();
|
|
55
161
|
return {
|
|
56
|
-
openaiApiKey: normalizeKey(parsed.
|
|
57
|
-
anthropicApiKey: normalizeKey(parsed.
|
|
58
|
-
openrouterApiKey: normalizeKey(parsed.
|
|
162
|
+
openaiApiKey: normalizeKey(findProfileKey(parsed.profiles, "openai")) ?? null,
|
|
163
|
+
anthropicApiKey: normalizeKey(findProfileKey(parsed.profiles, "anthropic")) ?? null,
|
|
164
|
+
openrouterApiKey: normalizeKey(findProfileKey(parsed.profiles, "openrouter")) ?? null,
|
|
59
165
|
createdAt,
|
|
60
166
|
updatedAt,
|
|
61
167
|
};
|
|
@@ -65,33 +171,54 @@ export function readByokKeys() {
|
|
|
65
171
|
}
|
|
66
172
|
}
|
|
67
173
|
export function writeByokKeys(input) {
|
|
68
|
-
|
|
69
|
-
const
|
|
70
|
-
const
|
|
174
|
+
ensureAuthProfilesDir();
|
|
175
|
+
const existingParsed = readAuthProfiles().parsed;
|
|
176
|
+
const next = existingParsed ?? {
|
|
177
|
+
version: 1,
|
|
178
|
+
profiles: {},
|
|
179
|
+
};
|
|
71
180
|
const has = (key) => Object.prototype.hasOwnProperty.call(input, key);
|
|
72
|
-
const
|
|
73
|
-
|
|
74
|
-
|
|
75
|
-
|
|
76
|
-
|
|
77
|
-
|
|
78
|
-
|
|
79
|
-
|
|
80
|
-
|
|
81
|
-
|
|
82
|
-
|
|
83
|
-
|
|
181
|
+
const applyKey = (field, value) => {
|
|
182
|
+
const mapped = PROVIDER_PROFILE_MAP[field];
|
|
183
|
+
const normalized = normalizeKey(value);
|
|
184
|
+
if (normalized) {
|
|
185
|
+
next.profiles[mapped.profileId] = {
|
|
186
|
+
type: "api_key",
|
|
187
|
+
provider: mapped.provider,
|
|
188
|
+
key: normalized,
|
|
189
|
+
};
|
|
190
|
+
return;
|
|
191
|
+
}
|
|
192
|
+
delete next.profiles[mapped.profileId];
|
|
193
|
+
if (next.lastGood)
|
|
194
|
+
delete next.lastGood[mapped.profileId];
|
|
195
|
+
if (next.usageStats)
|
|
196
|
+
delete next.usageStats[mapped.profileId];
|
|
84
197
|
};
|
|
85
|
-
|
|
198
|
+
if (has("openaiApiKey"))
|
|
199
|
+
applyKey("openaiApiKey", input.openaiApiKey);
|
|
200
|
+
if (has("anthropicApiKey"))
|
|
201
|
+
applyKey("anthropicApiKey", input.anthropicApiKey);
|
|
202
|
+
if (has("openrouterApiKey"))
|
|
203
|
+
applyKey("openrouterApiKey", input.openrouterApiKey);
|
|
204
|
+
const file = authProfilesFile();
|
|
86
205
|
writeJsonFileAtomicSync(file, next, 0o600);
|
|
87
|
-
|
|
206
|
+
const updated = readByokKeys();
|
|
207
|
+
if (updated)
|
|
208
|
+
return updated;
|
|
209
|
+
const now = new Date().toISOString();
|
|
210
|
+
return {
|
|
211
|
+
openaiApiKey: null,
|
|
212
|
+
anthropicApiKey: null,
|
|
213
|
+
openrouterApiKey: null,
|
|
214
|
+
createdAt: now,
|
|
215
|
+
updatedAt: now,
|
|
216
|
+
};
|
|
88
217
|
}
|
|
89
218
|
export function clearByokKeys() {
|
|
90
|
-
|
|
91
|
-
|
|
92
|
-
|
|
93
|
-
|
|
94
|
-
|
|
95
|
-
// best effort
|
|
96
|
-
}
|
|
219
|
+
writeByokKeys({
|
|
220
|
+
openaiApiKey: null,
|
|
221
|
+
anthropicApiKey: null,
|
|
222
|
+
openrouterApiKey: null,
|
|
223
|
+
});
|
|
97
224
|
}
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
export {};
|
|
@@ -0,0 +1,6 @@
|
|
|
1
|
+
import { runGatewayWatchdogDaemon } from "./gateway-watchdog.js";
|
|
2
|
+
void runGatewayWatchdogDaemon().catch((err) => {
|
|
3
|
+
const message = err instanceof Error ? err.message : String(err);
|
|
4
|
+
console.error(`[orgx] gateway-watchdog crashed: ${message}`);
|
|
5
|
+
process.exit(1);
|
|
6
|
+
});
|
|
@@ -0,0 +1,11 @@
|
|
|
1
|
+
type Logger = {
|
|
2
|
+
info?: (message: string, meta?: Record<string, unknown>) => void;
|
|
3
|
+
warn?: (message: string, meta?: Record<string, unknown>) => void;
|
|
4
|
+
debug?: (message: string, meta?: Record<string, unknown>) => void;
|
|
5
|
+
};
|
|
6
|
+
export declare function runGatewayWatchdogDaemon(logger?: Logger): Promise<void>;
|
|
7
|
+
export declare function ensureGatewayWatchdog(logger: Logger): {
|
|
8
|
+
started: boolean;
|
|
9
|
+
pid: number | null;
|
|
10
|
+
};
|
|
11
|
+
export {};
|
|
@@ -0,0 +1,221 @@
|
|
|
1
|
+
import { existsSync, mkdirSync, readFileSync, rmSync, writeFileSync } from "node:fs";
|
|
2
|
+
import { spawn } from "node:child_process";
|
|
3
|
+
import { join } from "node:path";
|
|
4
|
+
import { fileURLToPath } from "node:url";
|
|
5
|
+
import { getOpenClawDir } from "./paths.js";
|
|
6
|
+
import { readOpenClawGatewayPort, readOpenClawSettingsSnapshot } from "./openclaw-settings.js";
|
|
7
|
+
const DEFAULT_MONITOR_INTERVAL_MS = 30_000;
|
|
8
|
+
const DEFAULT_FAILURES_BEFORE_RESTART = 2;
|
|
9
|
+
const DEFAULT_PROBE_TIMEOUT_MS = 2_500;
|
|
10
|
+
const WATCHDOG_PID_FILE = join(getOpenClawDir(), "orgx-gateway-watchdog.pid");
|
|
11
|
+
function readEnvNumber(name, fallback, min) {
|
|
12
|
+
const raw = (process.env[name] ?? "").trim();
|
|
13
|
+
if (!raw)
|
|
14
|
+
return fallback;
|
|
15
|
+
const parsed = Number.parseInt(raw, 10);
|
|
16
|
+
if (!Number.isFinite(parsed) || parsed < min)
|
|
17
|
+
return fallback;
|
|
18
|
+
return parsed;
|
|
19
|
+
}
|
|
20
|
+
function isPidAlive(pid) {
|
|
21
|
+
if (!Number.isFinite(pid) || pid <= 0)
|
|
22
|
+
return false;
|
|
23
|
+
try {
|
|
24
|
+
process.kill(pid, 0);
|
|
25
|
+
return true;
|
|
26
|
+
}
|
|
27
|
+
catch {
|
|
28
|
+
return false;
|
|
29
|
+
}
|
|
30
|
+
}
|
|
31
|
+
function readWatchdogPid() {
|
|
32
|
+
try {
|
|
33
|
+
if (!existsSync(WATCHDOG_PID_FILE))
|
|
34
|
+
return null;
|
|
35
|
+
const raw = readFileSync(WATCHDOG_PID_FILE, "utf8").trim();
|
|
36
|
+
const parsed = Number.parseInt(raw, 10);
|
|
37
|
+
if (!Number.isFinite(parsed) || parsed <= 0)
|
|
38
|
+
return null;
|
|
39
|
+
return parsed;
|
|
40
|
+
}
|
|
41
|
+
catch {
|
|
42
|
+
return null;
|
|
43
|
+
}
|
|
44
|
+
}
|
|
45
|
+
function writeWatchdogPid(pid) {
|
|
46
|
+
const dir = getOpenClawDir();
|
|
47
|
+
mkdirSync(dir, { recursive: true, mode: 0o700 });
|
|
48
|
+
writeFileSync(WATCHDOG_PID_FILE, `${pid}\n`, { mode: 0o600 });
|
|
49
|
+
}
|
|
50
|
+
function clearWatchdogPid() {
|
|
51
|
+
try {
|
|
52
|
+
rmSync(WATCHDOG_PID_FILE, { force: true });
|
|
53
|
+
}
|
|
54
|
+
catch {
|
|
55
|
+
// best effort
|
|
56
|
+
}
|
|
57
|
+
}
|
|
58
|
+
async function runCommandCollect(input) {
|
|
59
|
+
const timeoutMs = input.timeoutMs ?? 10_000;
|
|
60
|
+
return await new Promise((resolve, reject) => {
|
|
61
|
+
const child = spawn(input.command, input.args, {
|
|
62
|
+
env: process.env,
|
|
63
|
+
stdio: ["ignore", "pipe", "pipe"],
|
|
64
|
+
});
|
|
65
|
+
let stdout = "";
|
|
66
|
+
let stderr = "";
|
|
67
|
+
const timer = timeoutMs
|
|
68
|
+
? setTimeout(() => {
|
|
69
|
+
try {
|
|
70
|
+
child.kill("SIGKILL");
|
|
71
|
+
}
|
|
72
|
+
catch {
|
|
73
|
+
// best effort
|
|
74
|
+
}
|
|
75
|
+
reject(new Error(`Command timed out after ${timeoutMs}ms`));
|
|
76
|
+
}, timeoutMs)
|
|
77
|
+
: null;
|
|
78
|
+
child.stdout?.on("data", (chunk) => {
|
|
79
|
+
stdout += chunk.toString("utf8");
|
|
80
|
+
});
|
|
81
|
+
child.stderr?.on("data", (chunk) => {
|
|
82
|
+
stderr += chunk.toString("utf8");
|
|
83
|
+
});
|
|
84
|
+
child.on("error", (err) => {
|
|
85
|
+
if (timer)
|
|
86
|
+
clearTimeout(timer);
|
|
87
|
+
reject(err);
|
|
88
|
+
});
|
|
89
|
+
child.on("close", (code) => {
|
|
90
|
+
if (timer)
|
|
91
|
+
clearTimeout(timer);
|
|
92
|
+
resolve({ stdout, stderr, exitCode: typeof code === "number" ? code : null });
|
|
93
|
+
});
|
|
94
|
+
});
|
|
95
|
+
}
|
|
96
|
+
async function probeGateway(port, timeoutMs) {
|
|
97
|
+
const controller = new AbortController();
|
|
98
|
+
const timeout = setTimeout(() => controller.abort(), timeoutMs);
|
|
99
|
+
try {
|
|
100
|
+
// Any HTTP response (including 404) means the gateway port is reachable.
|
|
101
|
+
await fetch(`http://127.0.0.1:${port}/`, {
|
|
102
|
+
method: "GET",
|
|
103
|
+
signal: controller.signal,
|
|
104
|
+
headers: {
|
|
105
|
+
"cache-control": "no-cache",
|
|
106
|
+
},
|
|
107
|
+
});
|
|
108
|
+
return true;
|
|
109
|
+
}
|
|
110
|
+
catch {
|
|
111
|
+
return false;
|
|
112
|
+
}
|
|
113
|
+
finally {
|
|
114
|
+
clearTimeout(timeout);
|
|
115
|
+
}
|
|
116
|
+
}
|
|
117
|
+
async function restartGateway(logger) {
|
|
118
|
+
const restart = await runCommandCollect({
|
|
119
|
+
command: "openclaw",
|
|
120
|
+
args: ["gateway", "restart", "--json"],
|
|
121
|
+
timeoutMs: 30_000,
|
|
122
|
+
});
|
|
123
|
+
if (restart.exitCode === 0) {
|
|
124
|
+
logger.warn?.("[orgx] Gateway watchdog restarted OpenClaw gateway service");
|
|
125
|
+
return;
|
|
126
|
+
}
|
|
127
|
+
const start = await runCommandCollect({
|
|
128
|
+
command: "openclaw",
|
|
129
|
+
args: ["gateway", "start", "--json"],
|
|
130
|
+
timeoutMs: 30_000,
|
|
131
|
+
});
|
|
132
|
+
if (start.exitCode !== 0) {
|
|
133
|
+
throw new Error(start.stderr.trim() || restart.stderr.trim() || "Failed to restart gateway");
|
|
134
|
+
}
|
|
135
|
+
logger.warn?.("[orgx] Gateway watchdog started OpenClaw gateway service");
|
|
136
|
+
}
|
|
137
|
+
export async function runGatewayWatchdogDaemon(logger = console) {
|
|
138
|
+
const monitorIntervalMs = readEnvNumber("ORGX_GATEWAY_WATCHDOG_INTERVAL_MS", DEFAULT_MONITOR_INTERVAL_MS, 5_000);
|
|
139
|
+
const failuresBeforeRestart = readEnvNumber("ORGX_GATEWAY_WATCHDOG_FAILURES", DEFAULT_FAILURES_BEFORE_RESTART, 1);
|
|
140
|
+
const probeTimeoutMs = readEnvNumber("ORGX_GATEWAY_WATCHDOG_TIMEOUT_MS", DEFAULT_PROBE_TIMEOUT_MS, 500);
|
|
141
|
+
let consecutiveFailures = 0;
|
|
142
|
+
let restartInFlight = false;
|
|
143
|
+
const cleanup = () => {
|
|
144
|
+
const pid = readWatchdogPid();
|
|
145
|
+
if (pid === process.pid) {
|
|
146
|
+
clearWatchdogPid();
|
|
147
|
+
}
|
|
148
|
+
};
|
|
149
|
+
process.on("SIGTERM", () => {
|
|
150
|
+
cleanup();
|
|
151
|
+
process.exit(0);
|
|
152
|
+
});
|
|
153
|
+
process.on("SIGINT", () => {
|
|
154
|
+
cleanup();
|
|
155
|
+
process.exit(0);
|
|
156
|
+
});
|
|
157
|
+
process.on("exit", cleanup);
|
|
158
|
+
writeWatchdogPid(process.pid);
|
|
159
|
+
logger.info?.("[orgx] Gateway watchdog daemon started", {
|
|
160
|
+
intervalMs: monitorIntervalMs,
|
|
161
|
+
failuresBeforeRestart,
|
|
162
|
+
});
|
|
163
|
+
const tick = async () => {
|
|
164
|
+
if (restartInFlight)
|
|
165
|
+
return;
|
|
166
|
+
const snapshot = readOpenClawSettingsSnapshot();
|
|
167
|
+
const port = readOpenClawGatewayPort(snapshot.raw);
|
|
168
|
+
const healthy = await probeGateway(port, probeTimeoutMs);
|
|
169
|
+
if (healthy) {
|
|
170
|
+
consecutiveFailures = 0;
|
|
171
|
+
return;
|
|
172
|
+
}
|
|
173
|
+
consecutiveFailures += 1;
|
|
174
|
+
logger.warn?.("[orgx] Gateway watchdog probe failed", {
|
|
175
|
+
port,
|
|
176
|
+
consecutiveFailures,
|
|
177
|
+
threshold: failuresBeforeRestart,
|
|
178
|
+
});
|
|
179
|
+
if (consecutiveFailures < failuresBeforeRestart) {
|
|
180
|
+
return;
|
|
181
|
+
}
|
|
182
|
+
restartInFlight = true;
|
|
183
|
+
try {
|
|
184
|
+
await restartGateway(logger);
|
|
185
|
+
consecutiveFailures = 0;
|
|
186
|
+
}
|
|
187
|
+
catch (err) {
|
|
188
|
+
logger.warn?.("[orgx] Gateway watchdog failed to restart gateway", {
|
|
189
|
+
error: err instanceof Error ? err.message : String(err),
|
|
190
|
+
});
|
|
191
|
+
}
|
|
192
|
+
finally {
|
|
193
|
+
restartInFlight = false;
|
|
194
|
+
}
|
|
195
|
+
};
|
|
196
|
+
await tick();
|
|
197
|
+
setInterval(() => {
|
|
198
|
+
void tick();
|
|
199
|
+
}, monitorIntervalMs);
|
|
200
|
+
}
|
|
201
|
+
export function ensureGatewayWatchdog(logger) {
|
|
202
|
+
if (process.env.ORGX_DISABLE_GATEWAY_WATCHDOG === "1") {
|
|
203
|
+
logger.debug?.("[orgx] Gateway watchdog disabled via ORGX_DISABLE_GATEWAY_WATCHDOG=1");
|
|
204
|
+
return { started: false, pid: null };
|
|
205
|
+
}
|
|
206
|
+
const existing = readWatchdogPid();
|
|
207
|
+
if (existing && isPidAlive(existing)) {
|
|
208
|
+
return { started: false, pid: existing };
|
|
209
|
+
}
|
|
210
|
+
if (existing && !isPidAlive(existing)) {
|
|
211
|
+
clearWatchdogPid();
|
|
212
|
+
}
|
|
213
|
+
const runnerPath = fileURLToPath(new URL("./gateway-watchdog-runner.js", import.meta.url));
|
|
214
|
+
const child = spawn(process.execPath, [runnerPath], {
|
|
215
|
+
env: process.env,
|
|
216
|
+
stdio: "ignore",
|
|
217
|
+
detached: true,
|
|
218
|
+
});
|
|
219
|
+
child.unref();
|
|
220
|
+
return { started: true, pid: child.pid ?? null };
|
|
221
|
+
}
|