@syengup/friday-channel-next 0.0.46 → 0.1.2
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/LICENSE +21 -0
- package/dist/index.js +10 -0
- package/dist/src/agent/run-usage-accumulator.d.ts +13 -0
- package/dist/src/agent/run-usage-accumulator.js +58 -0
- package/dist/src/friday-session.js +84 -15
- package/dist/src/health/self-health.d.ts +39 -0
- package/dist/src/health/self-health.js +174 -0
- package/dist/src/http/handlers/health.d.ts +23 -0
- package/dist/src/http/handlers/health.js +225 -0
- package/dist/src/http/server.js +5 -0
- package/dist/src/run-metadata.d.ts +6 -0
- package/dist/src/run-metadata.js +24 -1
- package/index.ts +16 -0
- package/install.js +4 -0
- package/package.json +12 -10
- package/src/agent/run-usage-accumulator.ts +70 -0
- package/src/friday-session.forward-agent.test.ts +100 -33
- package/src/friday-session.ts +78 -16
- package/src/http/handlers/health.test.ts +515 -0
- package/src/http/handlers/health.ts +289 -0
- package/src/http/server.ts +6 -0
- package/src/run-metadata.ts +28 -1
package/LICENSE
ADDED
|
@@ -0,0 +1,21 @@
|
|
|
1
|
+
MIT License
|
|
2
|
+
|
|
3
|
+
Copyright (c) 2025 SyengUp
|
|
4
|
+
|
|
5
|
+
Permission is hereby granted, free of charge, to any person obtaining a copy
|
|
6
|
+
of this software and associated documentation files (the "Software"), to deal
|
|
7
|
+
in the Software without restriction, including without limitation the rights
|
|
8
|
+
to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
|
|
9
|
+
copies of the Software, and to permit persons to whom the Software is
|
|
10
|
+
furnished to do so, subject to the following conditions:
|
|
11
|
+
|
|
12
|
+
The above copyright notice and this permission notice shall be included in all
|
|
13
|
+
copies or substantial portions of the Software.
|
|
14
|
+
|
|
15
|
+
THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
|
|
16
|
+
IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
|
|
17
|
+
FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
|
|
18
|
+
AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
|
|
19
|
+
LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
|
|
20
|
+
OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
|
|
21
|
+
SOFTWARE.
|
package/dist/index.js
CHANGED
|
@@ -9,6 +9,7 @@ import { sseEmitter } from "./src/sse/emitter.js";
|
|
|
9
9
|
import { forwardAgentEventRaw, getLastRegisteredFridayDeviceId, resolveFridayDeviceIdForSessionKey, } from "./src/friday-session.js";
|
|
10
10
|
import { setFridayAgentForwardRuntime } from "./src/agent-forward-runtime.js";
|
|
11
11
|
import { getOpenClawAgentRunContext } from "./src/agent-run-context-bridge.js";
|
|
12
|
+
import { accumulateRunUsage } from "./src/agent/run-usage-accumulator.js";
|
|
12
13
|
export { fridayNextChannelPlugin } from "./src/channel.js";
|
|
13
14
|
export { setFridayNextRuntime } from "./src/runtime.js";
|
|
14
15
|
/** `api.on` returns void — register tool hooks at most once per process. */
|
|
@@ -95,6 +96,15 @@ export default defineChannelPluginEntry({
|
|
|
95
96
|
sessionKey: evt.sessionKey,
|
|
96
97
|
});
|
|
97
98
|
});
|
|
99
|
+
api.on("llm_output", (event) => {
|
|
100
|
+
accumulateRunUsage(event.runId, {
|
|
101
|
+
input: event.usage?.input,
|
|
102
|
+
output: event.usage?.output,
|
|
103
|
+
cacheRead: event.usage?.cacheRead,
|
|
104
|
+
cacheWrite: event.usage?.cacheWrite,
|
|
105
|
+
total: event.usage?.total,
|
|
106
|
+
}, event.model, event.provider);
|
|
107
|
+
});
|
|
98
108
|
if (fridayNextToolHooksRegistered) {
|
|
99
109
|
return;
|
|
100
110
|
}
|
|
@@ -0,0 +1,13 @@
|
|
|
1
|
+
import type { FridaySessionUsagePayload } from "../session-usage-snapshot.js";
|
|
2
|
+
type UsageFields = {
|
|
3
|
+
input?: number;
|
|
4
|
+
output?: number;
|
|
5
|
+
cacheRead?: number;
|
|
6
|
+
cacheWrite?: number;
|
|
7
|
+
total?: number;
|
|
8
|
+
};
|
|
9
|
+
export declare function accumulateRunUsage(runId: string, usage: UsageFields, model?: string, provider?: string): void;
|
|
10
|
+
export declare function consumeRunUsage(runId: string): FridaySessionUsagePayload | undefined;
|
|
11
|
+
/** Vitest-only. */
|
|
12
|
+
export declare function resetRunUsageAccumulatorForTest(): void;
|
|
13
|
+
export {};
|
|
@@ -0,0 +1,58 @@
|
|
|
1
|
+
const usageByRunId = new Map();
|
|
2
|
+
function ensure(runId) {
|
|
3
|
+
let entry = usageByRunId.get(runId);
|
|
4
|
+
if (!entry) {
|
|
5
|
+
entry = { input: 0, output: 0, cacheRead: 0, cacheWrite: 0, total: 0 };
|
|
6
|
+
usageByRunId.set(runId, entry);
|
|
7
|
+
}
|
|
8
|
+
return entry;
|
|
9
|
+
}
|
|
10
|
+
export function accumulateRunUsage(runId, usage, model, provider) {
|
|
11
|
+
if (!runId.trim())
|
|
12
|
+
return;
|
|
13
|
+
const entry = ensure(runId);
|
|
14
|
+
if (typeof usage.input === "number" && usage.input > 0)
|
|
15
|
+
entry.input += usage.input;
|
|
16
|
+
if (typeof usage.output === "number" && usage.output > 0)
|
|
17
|
+
entry.output += usage.output;
|
|
18
|
+
if (typeof usage.cacheRead === "number" && usage.cacheRead > 0)
|
|
19
|
+
entry.cacheRead += usage.cacheRead;
|
|
20
|
+
if (typeof usage.cacheWrite === "number" && usage.cacheWrite > 0)
|
|
21
|
+
entry.cacheWrite += usage.cacheWrite;
|
|
22
|
+
if (typeof usage.total === "number" && usage.total > 0)
|
|
23
|
+
entry.total += usage.total;
|
|
24
|
+
if (model && model.trim())
|
|
25
|
+
entry.model = model.trim();
|
|
26
|
+
if (provider && provider.trim())
|
|
27
|
+
entry.provider = provider.trim();
|
|
28
|
+
}
|
|
29
|
+
export function consumeRunUsage(runId) {
|
|
30
|
+
const entry = usageByRunId.get(runId);
|
|
31
|
+
if (!entry)
|
|
32
|
+
return undefined;
|
|
33
|
+
usageByRunId.delete(runId);
|
|
34
|
+
const tokens = {};
|
|
35
|
+
if (entry.input > 0)
|
|
36
|
+
tokens.input = entry.input;
|
|
37
|
+
if (entry.output > 0)
|
|
38
|
+
tokens.output = entry.output;
|
|
39
|
+
if (entry.cacheRead > 0)
|
|
40
|
+
tokens.cacheRead = entry.cacheRead;
|
|
41
|
+
if (entry.cacheWrite > 0)
|
|
42
|
+
tokens.cacheWrite = entry.cacheWrite;
|
|
43
|
+
if (entry.total > 0)
|
|
44
|
+
tokens.total = entry.total;
|
|
45
|
+
tokens.totalFresh = true;
|
|
46
|
+
if (Object.keys(tokens).length === 1)
|
|
47
|
+
return undefined; // only totalFresh, no actual tokens
|
|
48
|
+
const payload = { tokens };
|
|
49
|
+
if (entry.model)
|
|
50
|
+
payload.modelId = entry.model;
|
|
51
|
+
if (entry.provider)
|
|
52
|
+
payload.modelProvider = entry.provider;
|
|
53
|
+
return payload;
|
|
54
|
+
}
|
|
55
|
+
/** Vitest-only. */
|
|
56
|
+
export function resetRunUsageAccumulatorForTest() {
|
|
57
|
+
usageByRunId.clear();
|
|
58
|
+
}
|
|
@@ -4,6 +4,7 @@ import { toSessionStoreKey } from "./session/session-manager.js";
|
|
|
4
4
|
import { getOpenClawAgentRunContext } from "./agent-run-context-bridge.js";
|
|
5
5
|
import { observeAgentEventForActiveRuns } from "./agent/active-runs.js";
|
|
6
6
|
import { getRunMetadata, ingestAgentEventMetadata } from "./run-metadata.js";
|
|
7
|
+
import { consumeRunUsage } from "./agent/run-usage-accumulator.js";
|
|
7
8
|
import { buildSessionUsageSnapshot } from "./session-usage-snapshot.js";
|
|
8
9
|
import { lookupByRunId, registerSessionKeyForRun, registerSpawnIntent, consumeSpawnIntent, ensureSubagentFromSpawnTool, registerEnded as registerSubagentEnded, } from "./agent/subagent-registry.js";
|
|
9
10
|
/** Last `data.text` per run for `stream: "thinking"` — OpenClaw core may send cumulative `delta`; we rewrite true increments for the app. */
|
|
@@ -162,6 +163,59 @@ function tryReadSessionUsageFromStore(sessionKeyForStore) {
|
|
|
162
163
|
return undefined;
|
|
163
164
|
}
|
|
164
165
|
}
|
|
166
|
+
function buildSessionUsageFromRunMetadata(runId) {
|
|
167
|
+
const meta = getRunMetadata(runId);
|
|
168
|
+
if (!meta)
|
|
169
|
+
return undefined;
|
|
170
|
+
const payload = {};
|
|
171
|
+
if (typeof meta.modelName === "string" && meta.modelName.trim()) {
|
|
172
|
+
payload.modelId = meta.modelName.trim();
|
|
173
|
+
}
|
|
174
|
+
if (typeof meta.modelProvider === "string" && meta.modelProvider.trim()) {
|
|
175
|
+
payload.modelProvider = meta.modelProvider.trim();
|
|
176
|
+
}
|
|
177
|
+
const tokens = {};
|
|
178
|
+
if (typeof meta.inputTokens === "number")
|
|
179
|
+
tokens.input = meta.inputTokens;
|
|
180
|
+
if (typeof meta.outputTokens === "number")
|
|
181
|
+
tokens.output = meta.outputTokens;
|
|
182
|
+
if (typeof meta.cacheReadTokens === "number")
|
|
183
|
+
tokens.cacheRead = meta.cacheReadTokens;
|
|
184
|
+
if (typeof meta.cacheWriteTokens === "number")
|
|
185
|
+
tokens.cacheWrite = meta.cacheWriteTokens;
|
|
186
|
+
if (typeof meta.totalTokens === "number")
|
|
187
|
+
tokens.total = meta.totalTokens;
|
|
188
|
+
if (Object.keys(tokens).length > 0)
|
|
189
|
+
payload.tokens = tokens;
|
|
190
|
+
const context = {};
|
|
191
|
+
if (typeof meta.contextWindowMax === "number")
|
|
192
|
+
context.windowMax = meta.contextWindowMax;
|
|
193
|
+
if (typeof meta.totalTokens === "number")
|
|
194
|
+
context.used = meta.totalTokens;
|
|
195
|
+
if (Object.keys(context).length > 0)
|
|
196
|
+
payload.context = context;
|
|
197
|
+
if (!payload.modelId && !payload.modelProvider && !payload.tokens && !payload.context) {
|
|
198
|
+
return undefined;
|
|
199
|
+
}
|
|
200
|
+
return payload;
|
|
201
|
+
}
|
|
202
|
+
function mergeUsage(llmUsage, memUsage) {
|
|
203
|
+
if (!llmUsage && !memUsage)
|
|
204
|
+
return undefined;
|
|
205
|
+
if (!llmUsage)
|
|
206
|
+
return memUsage;
|
|
207
|
+
if (!memUsage)
|
|
208
|
+
return llmUsage;
|
|
209
|
+
// llm_output tokens are authoritative (per API call, no race);
|
|
210
|
+
// RunMetadata fills context window gaps.
|
|
211
|
+
return {
|
|
212
|
+
modelId: llmUsage.modelId ?? memUsage.modelId,
|
|
213
|
+
modelProvider: llmUsage.modelProvider ?? memUsage.modelProvider,
|
|
214
|
+
tokens: llmUsage.tokens,
|
|
215
|
+
context: memUsage.context ?? llmUsage.context,
|
|
216
|
+
estimatedCostUsd: llmUsage.estimatedCostUsd ?? memUsage.estimatedCostUsd,
|
|
217
|
+
};
|
|
218
|
+
}
|
|
165
219
|
function completeAgentEventForward(params) {
|
|
166
220
|
const { evt, sk, deviceIdRaw, outgoingData, isTerminalLifecycle, subagentMeta } = params;
|
|
167
221
|
observeAgentEventForActiveRuns({ stream: evt.stream, runId: evt.runId, data: outgoingData });
|
|
@@ -366,23 +420,38 @@ export function forwardAgentEventRaw(evt) {
|
|
|
366
420
|
}, ended.deviceId);
|
|
367
421
|
}
|
|
368
422
|
}
|
|
369
|
-
|
|
370
|
-
|
|
371
|
-
|
|
372
|
-
|
|
423
|
+
// Build sessionUsage: llm_output hook (primary, no race) → store read (fallback).
|
|
424
|
+
if (isTerminalLifecycle) {
|
|
425
|
+
const llmUsage = consumeRunUsage(evt.runId);
|
|
426
|
+
const memUsage = buildSessionUsageFromRunMetadata(evt.runId);
|
|
427
|
+
const hasRealTokens = llmUsage?.tokens && Object.keys(llmUsage.tokens).length > 1;
|
|
428
|
+
if (hasRealTokens) {
|
|
429
|
+
const usage = mergeUsage(llmUsage, memUsage);
|
|
373
430
|
if (usage) {
|
|
374
|
-
|
|
431
|
+
outgoingData = { ...outgoingData, sessionUsage: usage };
|
|
375
432
|
}
|
|
376
|
-
|
|
377
|
-
|
|
378
|
-
|
|
379
|
-
|
|
380
|
-
|
|
381
|
-
|
|
382
|
-
|
|
383
|
-
|
|
384
|
-
|
|
385
|
-
|
|
433
|
+
}
|
|
434
|
+
else if (getFridayAgentForwardRuntime()) {
|
|
435
|
+
// llm_output hook fires async ~20ms after lifecycle.end.
|
|
436
|
+
// Wait 100ms then re-check before falling back to store read.
|
|
437
|
+
setTimeout(() => {
|
|
438
|
+
let data = outgoingData;
|
|
439
|
+
const retryLlm = consumeRunUsage(evt.runId);
|
|
440
|
+
const usage = mergeUsage(retryLlm, memUsage) ?? tryReadSessionUsageFromStore(sk);
|
|
441
|
+
if (usage) {
|
|
442
|
+
data = { ...outgoingData, sessionUsage: usage };
|
|
443
|
+
}
|
|
444
|
+
completeAgentEventForward({
|
|
445
|
+
evt,
|
|
446
|
+
sk,
|
|
447
|
+
deviceIdRaw,
|
|
448
|
+
outgoingData: data,
|
|
449
|
+
isTerminalLifecycle: true,
|
|
450
|
+
subagentMeta,
|
|
451
|
+
});
|
|
452
|
+
}, 100);
|
|
453
|
+
return;
|
|
454
|
+
}
|
|
386
455
|
}
|
|
387
456
|
completeAgentEventForward({
|
|
388
457
|
evt,
|
|
@@ -0,0 +1,39 @@
|
|
|
1
|
+
export interface SelfHealthOptions {
|
|
2
|
+
enabled: boolean;
|
|
3
|
+
checkIntervalMs: number;
|
|
4
|
+
selfHeal: boolean;
|
|
5
|
+
}
|
|
6
|
+
interface CheckResult {
|
|
7
|
+
name: string;
|
|
8
|
+
status: "ok" | "degraded" | "failed";
|
|
9
|
+
detail: string;
|
|
10
|
+
}
|
|
11
|
+
interface RepairAction {
|
|
12
|
+
component: string;
|
|
13
|
+
action: string;
|
|
14
|
+
result: "ok" | "failed" | "skipped";
|
|
15
|
+
detail: string;
|
|
16
|
+
}
|
|
17
|
+
export interface SelfHealthReport {
|
|
18
|
+
timestamp: number;
|
|
19
|
+
checks: CheckResult[];
|
|
20
|
+
repairs: RepairAction[];
|
|
21
|
+
overallStatus: "ok" | "degraded" | "failed";
|
|
22
|
+
}
|
|
23
|
+
export declare class HealthCheckRunner {
|
|
24
|
+
private timer;
|
|
25
|
+
private options;
|
|
26
|
+
constructor(options?: Partial<SelfHealthOptions>);
|
|
27
|
+
updateOptions(opts: Partial<SelfHealthOptions>): void;
|
|
28
|
+
start(_api: unknown): void;
|
|
29
|
+
stop(): void;
|
|
30
|
+
runCheck(): Promise<SelfHealthReport>;
|
|
31
|
+
private checkConfig;
|
|
32
|
+
private checkSseEmitter;
|
|
33
|
+
private checkActiveRuns;
|
|
34
|
+
private repairConfig;
|
|
35
|
+
private repairSseEmitter;
|
|
36
|
+
}
|
|
37
|
+
export declare function getHealthCheckRunner(): HealthCheckRunner;
|
|
38
|
+
export declare function resetHealthCheckRunnerForTest(): void;
|
|
39
|
+
export {};
|
|
@@ -0,0 +1,174 @@
|
|
|
1
|
+
import { resolveFridayNextConfig } from "../config.js";
|
|
2
|
+
import { getHostOpenClawConfigSnapshot } from "../host-config.js";
|
|
3
|
+
import { getFridayNextRuntime } from "../runtime.js";
|
|
4
|
+
import { sseEmitter } from "../sse/emitter.js";
|
|
5
|
+
import { getActiveRunCount } from "../agent/active-runs.js";
|
|
6
|
+
import { createFridayNextLogger } from "../logging.js";
|
|
7
|
+
const log = createFridayNextLogger("health-runner", "info");
|
|
8
|
+
export class HealthCheckRunner {
|
|
9
|
+
timer = null;
|
|
10
|
+
options;
|
|
11
|
+
constructor(options = {}) {
|
|
12
|
+
this.options = {
|
|
13
|
+
enabled: options.enabled ?? true,
|
|
14
|
+
checkIntervalMs: options.checkIntervalMs ?? 60_000,
|
|
15
|
+
selfHeal: options.selfHeal ?? true,
|
|
16
|
+
};
|
|
17
|
+
}
|
|
18
|
+
updateOptions(opts) {
|
|
19
|
+
Object.assign(this.options, opts);
|
|
20
|
+
}
|
|
21
|
+
start(_api) {
|
|
22
|
+
this.stop();
|
|
23
|
+
if (!this.options.enabled) {
|
|
24
|
+
log.info("Self-health check disabled by config");
|
|
25
|
+
return;
|
|
26
|
+
}
|
|
27
|
+
log.info(`Starting self-health checks every ${this.options.checkIntervalMs}ms`);
|
|
28
|
+
this.timer = setInterval(() => {
|
|
29
|
+
this.runCheck().catch((err) => {
|
|
30
|
+
log.error(`Self-health check error: ${err instanceof Error ? err.message : String(err)}`);
|
|
31
|
+
});
|
|
32
|
+
}, this.options.checkIntervalMs);
|
|
33
|
+
if (this.timer && typeof this.timer.unref === "function") {
|
|
34
|
+
this.timer.unref();
|
|
35
|
+
}
|
|
36
|
+
}
|
|
37
|
+
stop() {
|
|
38
|
+
if (this.timer) {
|
|
39
|
+
clearInterval(this.timer);
|
|
40
|
+
this.timer = null;
|
|
41
|
+
}
|
|
42
|
+
}
|
|
43
|
+
async runCheck() {
|
|
44
|
+
const report = {
|
|
45
|
+
timestamp: Date.now(),
|
|
46
|
+
checks: [],
|
|
47
|
+
repairs: [],
|
|
48
|
+
overallStatus: "ok",
|
|
49
|
+
};
|
|
50
|
+
report.checks.push(this.checkConfig());
|
|
51
|
+
report.checks.push(this.checkSseEmitter());
|
|
52
|
+
report.checks.push(this.checkActiveRuns());
|
|
53
|
+
if (this.options.selfHeal) {
|
|
54
|
+
const configCheck = report.checks[0];
|
|
55
|
+
if (configCheck.status === "failed") {
|
|
56
|
+
report.repairs.push(await this.repairConfig());
|
|
57
|
+
}
|
|
58
|
+
const sseCheck = report.checks[1];
|
|
59
|
+
if (sseCheck.status === "failed") {
|
|
60
|
+
report.repairs.push(await this.repairSseEmitter());
|
|
61
|
+
}
|
|
62
|
+
}
|
|
63
|
+
const statuses = report.checks.map((c) => c.status);
|
|
64
|
+
if (statuses.includes("failed")) {
|
|
65
|
+
report.overallStatus = "failed";
|
|
66
|
+
}
|
|
67
|
+
else if (statuses.includes("degraded")) {
|
|
68
|
+
report.overallStatus = "degraded";
|
|
69
|
+
}
|
|
70
|
+
if (report.overallStatus !== "ok" || report.repairs.length > 0) {
|
|
71
|
+
log.warn(`Self-health result: ${report.overallStatus}, ` +
|
|
72
|
+
`checks=${report.checks.length}, repairs=${report.repairs.length}`);
|
|
73
|
+
}
|
|
74
|
+
return report;
|
|
75
|
+
}
|
|
76
|
+
checkConfig() {
|
|
77
|
+
try {
|
|
78
|
+
const runtime = getFridayNextRuntime();
|
|
79
|
+
if (!runtime?.config) {
|
|
80
|
+
return { name: "config", status: "failed", detail: "Runtime config loader not available" };
|
|
81
|
+
}
|
|
82
|
+
const cfg = resolveFridayNextConfig(getHostOpenClawConfigSnapshot(runtime.config));
|
|
83
|
+
if (!cfg.authToken) {
|
|
84
|
+
return { name: "config", status: "degraded", detail: "authToken is empty; all requests will 401" };
|
|
85
|
+
}
|
|
86
|
+
return { name: "config", status: "ok", detail: "Config resolved with authToken" };
|
|
87
|
+
}
|
|
88
|
+
catch (err) {
|
|
89
|
+
return {
|
|
90
|
+
name: "config",
|
|
91
|
+
status: "failed",
|
|
92
|
+
detail: `Config resolution error: ${err instanceof Error ? err.message : String(err)}`,
|
|
93
|
+
};
|
|
94
|
+
}
|
|
95
|
+
}
|
|
96
|
+
checkSseEmitter() {
|
|
97
|
+
try {
|
|
98
|
+
const connCount = sseEmitter.getConnectionCount();
|
|
99
|
+
return {
|
|
100
|
+
name: "sseEmitter",
|
|
101
|
+
status: "ok",
|
|
102
|
+
detail: `Active connections: ${connCount}`,
|
|
103
|
+
};
|
|
104
|
+
}
|
|
105
|
+
catch (err) {
|
|
106
|
+
return {
|
|
107
|
+
name: "sseEmitter",
|
|
108
|
+
status: "failed",
|
|
109
|
+
detail: `Emitter check error: ${err instanceof Error ? err.message : String(err)}`,
|
|
110
|
+
};
|
|
111
|
+
}
|
|
112
|
+
}
|
|
113
|
+
checkActiveRuns() {
|
|
114
|
+
try {
|
|
115
|
+
const count = getActiveRunCount();
|
|
116
|
+
return {
|
|
117
|
+
name: "activeRuns",
|
|
118
|
+
status: "ok",
|
|
119
|
+
detail: `Active runs: ${count}`,
|
|
120
|
+
};
|
|
121
|
+
}
|
|
122
|
+
catch (err) {
|
|
123
|
+
return {
|
|
124
|
+
name: "activeRuns",
|
|
125
|
+
status: "failed",
|
|
126
|
+
detail: `Active runs check error: ${err instanceof Error ? err.message : String(err)}`,
|
|
127
|
+
};
|
|
128
|
+
}
|
|
129
|
+
}
|
|
130
|
+
async repairConfig() {
|
|
131
|
+
try {
|
|
132
|
+
const runtime = getFridayNextRuntime();
|
|
133
|
+
if (!runtime?.config) {
|
|
134
|
+
return {
|
|
135
|
+
component: "config",
|
|
136
|
+
action: "re-resolve config",
|
|
137
|
+
result: "failed",
|
|
138
|
+
detail: "Runtime config loader not available",
|
|
139
|
+
};
|
|
140
|
+
}
|
|
141
|
+
resolveFridayNextConfig(getHostOpenClawConfigSnapshot(runtime.config));
|
|
142
|
+
return { component: "config", action: "re-resolve config", result: "ok", detail: "Config re-resolved" };
|
|
143
|
+
}
|
|
144
|
+
catch (err) {
|
|
145
|
+
return {
|
|
146
|
+
component: "config",
|
|
147
|
+
action: "re-resolve config",
|
|
148
|
+
result: "failed",
|
|
149
|
+
detail: err instanceof Error ? err.message : String(err),
|
|
150
|
+
};
|
|
151
|
+
}
|
|
152
|
+
}
|
|
153
|
+
async repairSseEmitter() {
|
|
154
|
+
return {
|
|
155
|
+
component: "sseEmitter",
|
|
156
|
+
action: "verify emitter",
|
|
157
|
+
result: "ok",
|
|
158
|
+
detail: "SSE emitter singleton is accessible",
|
|
159
|
+
};
|
|
160
|
+
}
|
|
161
|
+
}
|
|
162
|
+
let healthCheckRunner = null;
|
|
163
|
+
export function getHealthCheckRunner() {
|
|
164
|
+
if (!healthCheckRunner) {
|
|
165
|
+
healthCheckRunner = new HealthCheckRunner();
|
|
166
|
+
}
|
|
167
|
+
return healthCheckRunner;
|
|
168
|
+
}
|
|
169
|
+
export function resetHealthCheckRunnerForTest() {
|
|
170
|
+
if (healthCheckRunner) {
|
|
171
|
+
healthCheckRunner.stop();
|
|
172
|
+
healthCheckRunner = null;
|
|
173
|
+
}
|
|
174
|
+
}
|
|
@@ -0,0 +1,23 @@
|
|
|
1
|
+
import type { IncomingMessage, ServerResponse } from "node:http";
|
|
2
|
+
export interface HealthComponentStatus {
|
|
3
|
+
status: "ok" | "degraded" | "failed" | "pending" | "not_found";
|
|
4
|
+
detail: string;
|
|
5
|
+
[key: string]: unknown;
|
|
6
|
+
}
|
|
7
|
+
interface RepairAction {
|
|
8
|
+
component: string;
|
|
9
|
+
action: string;
|
|
10
|
+
result: "ok" | "failed";
|
|
11
|
+
detail: string;
|
|
12
|
+
}
|
|
13
|
+
export interface HealthCheckResult {
|
|
14
|
+
ok: boolean;
|
|
15
|
+
timestamp: number;
|
|
16
|
+
deviceId: string;
|
|
17
|
+
nodeDeviceId: string;
|
|
18
|
+
devicePairing?: HealthComponentStatus;
|
|
19
|
+
nodePairing?: HealthComponentStatus;
|
|
20
|
+
repairActions?: RepairAction[];
|
|
21
|
+
}
|
|
22
|
+
export declare function handleHealth(req: IncomingMessage, res: ServerResponse): Promise<boolean>;
|
|
23
|
+
export {};
|