@syengup/friday-channel-next 0.1.40 → 1.0.0
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/dist/index.js +59 -1
- package/dist/src/approval/friday-approval-capability.d.ts +44 -0
- package/dist/src/approval/friday-approval-capability.js +174 -0
- package/dist/src/channel.js +22 -0
- package/dist/src/codex-reasoning-config.d.ts +11 -0
- package/dist/src/codex-reasoning-config.js +83 -0
- package/dist/src/friday-session.d.ts +4 -0
- package/dist/src/friday-session.js +24 -0
- package/dist/src/http/handlers/approvals.d.ts +9 -0
- package/dist/src/http/handlers/approvals.js +54 -0
- package/dist/src/http/handlers/messages.js +19 -1
- package/dist/src/http/server.js +6 -0
- package/dist/src/http 2/middleware/auth.d.ts +13 -0
- package/dist/src/http 2/middleware/auth.js +29 -0
- package/dist/src/http 2/middleware/body.d.ts +2 -0
- package/dist/src/http 2/middleware/body.js +24 -0
- package/dist/src/http 2/middleware/cors.d.ts +2 -0
- package/dist/src/http 2/middleware/cors.js +11 -0
- package/dist/src/sse/emitter.d.ts +1 -1
- package/index.ts +61 -0
- package/package.json +15 -14
- package/src/approval/friday-approval-capability.test.ts +78 -0
- package/src/approval/friday-approval-capability.ts +227 -0
- package/src/channel.ts +25 -1
- package/src/codex-reasoning-config.test.ts +28 -0
- package/src/codex-reasoning-config.ts +82 -0
- package/src/friday-session.ts +28 -0
- package/src/http/handlers/approvals.ts +61 -0
- package/src/http/handlers/messages.ts +23 -1
- package/src/http/server.ts +7 -0
- package/src/sse/emitter.ts +2 -1
- package/dist/src/health/self-health.d.ts +0 -39
- package/dist/src/health/self-health.js +0 -174
- package/dist/src/http/handlers/sessions-delete.d.ts +0 -2
- package/dist/src/http/handlers/sessions-delete.js +0 -49
|
@@ -0,0 +1,82 @@
|
|
|
1
|
+
// Ensures the Codex app-server backend emits reasoning *summary* text so Friday can stream it.
|
|
2
|
+
//
|
|
3
|
+
// Background: OpenAI models authenticated via ChatGPT/OAuth run through OpenClaw's Codex
|
|
4
|
+
// app-server backend. That backend sends `reasoning_effort` per turn but never requests a
|
|
5
|
+
// reasoning summary, and OpenClaw exposes no `openclaw.json` lever for it. Without a summary the
|
|
6
|
+
// model's reasoning stays encrypted (`encrypted_content`) and no reasoning text reaches the
|
|
7
|
+
// channel — so the Friday app shows no streaming "thinking" for Codex models.
|
|
8
|
+
//
|
|
9
|
+
// The only switch that makes Codex return summary text is the Codex CLI's own
|
|
10
|
+
// `model_reasoning_summary` key in `~/.openclaw/agents/<id>/agent/codex-home/config.toml`.
|
|
11
|
+
// We keep the fix on the plugin side by asserting that key on activation (idempotently, for every
|
|
12
|
+
// agent that has a codex-home), so it survives OpenClaw rewrites of that file across restarts.
|
|
13
|
+
|
|
14
|
+
import { existsSync, mkdirSync, readFileSync, readdirSync, writeFileSync } from "node:fs";
|
|
15
|
+
import { homedir } from "node:os";
|
|
16
|
+
import { join } from "node:path";
|
|
17
|
+
|
|
18
|
+
const CONFIG_KEY = "model_reasoning_summary";
|
|
19
|
+
// "detailed" is the value verified end-to-end (reasoning streamed to the app). Codex also accepts
|
|
20
|
+
// "auto"/"concise"; tune here if the summaries feel too verbose.
|
|
21
|
+
const SUMMARY_VALUE = "detailed";
|
|
22
|
+
|
|
23
|
+
function resolveOpenClawHome(): string {
|
|
24
|
+
const env = process.env.OPENCLAW_HOME?.trim();
|
|
25
|
+
return env && env.length > 0 ? env : join(homedir(), ".openclaw");
|
|
26
|
+
}
|
|
27
|
+
|
|
28
|
+
/**
|
|
29
|
+
* True if a top-level `model_reasoning_summary` key already exists. TOML scoping matters: a key is
|
|
30
|
+
* only top-level (and thus honored by Codex) if it appears before the first `[section]` header, so
|
|
31
|
+
* we stop scanning at the first table header.
|
|
32
|
+
*/
|
|
33
|
+
export function hasTopLevelSummaryKey(content: string): boolean {
|
|
34
|
+
for (const raw of content.split(/\r?\n/)) {
|
|
35
|
+
const line = raw.trim();
|
|
36
|
+
if (line.startsWith("[")) break;
|
|
37
|
+
if (new RegExp(`^${CONFIG_KEY}\\s*=`).test(line)) return true;
|
|
38
|
+
}
|
|
39
|
+
return false;
|
|
40
|
+
}
|
|
41
|
+
|
|
42
|
+
function ensureKeyInCodexHome(codexHome: string): "added" | "present" | "skip" {
|
|
43
|
+
const configPath = join(codexHome, "config.toml");
|
|
44
|
+
const header = `${CONFIG_KEY} = "${SUMMARY_VALUE}"\n`;
|
|
45
|
+
if (!existsSync(configPath)) {
|
|
46
|
+
writeFileSync(configPath, header, "utf8");
|
|
47
|
+
return "added";
|
|
48
|
+
}
|
|
49
|
+
const content = readFileSync(configPath, "utf8");
|
|
50
|
+
if (hasTopLevelSummaryKey(content)) return "present";
|
|
51
|
+
// Prepend so the key stays top-level even if the file starts with a `[section]` table.
|
|
52
|
+
writeFileSync(configPath, `${header}\n${content}`, "utf8");
|
|
53
|
+
return "added";
|
|
54
|
+
}
|
|
55
|
+
|
|
56
|
+
/**
|
|
57
|
+
* Best-effort: ensure every agent's Codex config requests a reasoning summary. Never throws —
|
|
58
|
+
* activation must not fail because of a config write. `log` receives a one-line summary per change.
|
|
59
|
+
*/
|
|
60
|
+
export function ensureCodexReasoningSummary(log: (msg: string) => void): void {
|
|
61
|
+
try {
|
|
62
|
+
const agentsDir = join(resolveOpenClawHome(), "agents");
|
|
63
|
+
if (!existsSync(agentsDir)) return;
|
|
64
|
+
for (const agentId of readdirSync(agentsDir)) {
|
|
65
|
+
const codexHome = join(agentsDir, agentId, "agent", "codex-home");
|
|
66
|
+
// Only touch agents Codex has actually initialized (codex-home exists). New agents are
|
|
67
|
+
// picked up on the next activation/restart.
|
|
68
|
+
if (!existsSync(codexHome)) continue;
|
|
69
|
+
try {
|
|
70
|
+
mkdirSync(codexHome, { recursive: true });
|
|
71
|
+
const result = ensureKeyInCodexHome(codexHome);
|
|
72
|
+
if (result === "added") {
|
|
73
|
+
log(`codex reasoning summary enabled (agent=${agentId})`);
|
|
74
|
+
}
|
|
75
|
+
} catch (err) {
|
|
76
|
+
log(`codex reasoning summary write failed (agent=${agentId}): ${String(err)}`);
|
|
77
|
+
}
|
|
78
|
+
}
|
|
79
|
+
} catch (err) {
|
|
80
|
+
log(`codex reasoning summary ensure failed: ${String(err)}`);
|
|
81
|
+
}
|
|
82
|
+
}
|
package/src/friday-session.ts
CHANGED
|
@@ -33,6 +33,27 @@ export function resetThinkingStreamAccumStateForTest(): void {
|
|
|
33
33
|
lastThinkingTextByRun.clear();
|
|
34
34
|
}
|
|
35
35
|
|
|
36
|
+
/**
|
|
37
|
+
* Runs backed by the OpenClaw Codex app-server backend (model api `openai-chatgpt-responses`).
|
|
38
|
+
* They emit their activity under a `codex_app_server.*` stream namespace and — unlike the embedded
|
|
39
|
+
* runner — do NOT put reasoning text on the agent-event bus (`stream: "thinking"`); that text only
|
|
40
|
+
* arrives via the dispatch `onReasoningStream` callback. Likewise exec stdout never reaches the
|
|
41
|
+
* `command_output` stream. We mark a run as Codex the first time we see any `codex_app_server.*`
|
|
42
|
+
* frame so the message handler / tool hooks know to synthesize the missing `thinking` /
|
|
43
|
+
* `command_output` events for it (and ONLY for it — embedded runs already get both via the bus).
|
|
44
|
+
*/
|
|
45
|
+
const codexRunIds = new Set<string>();
|
|
46
|
+
|
|
47
|
+
/** True once a `codex_app_server.*` frame has been seen for this run. */
|
|
48
|
+
export function isCodexRun(runId: string): boolean {
|
|
49
|
+
return codexRunIds.has(runId);
|
|
50
|
+
}
|
|
51
|
+
|
|
52
|
+
/** Vitest-only */
|
|
53
|
+
export function resetCodexRunTrackingForTest(): void {
|
|
54
|
+
codexRunIds.clear();
|
|
55
|
+
}
|
|
56
|
+
|
|
36
57
|
/**
|
|
37
58
|
* OpenClaw `runId` → device UUID (uppercase).
|
|
38
59
|
* When `lifecycle.end` / `error` is emitted, the gateway may call `clearAgentRunContext` before this extension's
|
|
@@ -346,6 +367,12 @@ export function forwardAgentEventRaw(evt: ForwardAgentEventArgs): void {
|
|
|
346
367
|
|
|
347
368
|
openClawRunIdToDeviceId.set(evt.runId, deviceIdRaw.toUpperCase());
|
|
348
369
|
|
|
370
|
+
// Flag Codex app-server runs so the message handler / tool hooks synthesize the `thinking` /
|
|
371
|
+
// `command_output` events that this backend never emits on the bus (see `isCodexRun`).
|
|
372
|
+
if (typeof evt.stream === "string" && evt.stream.startsWith("codex_app_server")) {
|
|
373
|
+
codexRunIds.add(evt.runId);
|
|
374
|
+
}
|
|
375
|
+
|
|
349
376
|
// Register sessionKey → runId so we can resolve parentRunId
|
|
350
377
|
if (sk && evt.stream === "lifecycle" && evt.data.phase === "start") {
|
|
351
378
|
registerSessionKeyForRun(sk, evt.runId);
|
|
@@ -500,6 +527,7 @@ export function forwardAgentEventRaw(evt: ForwardAgentEventArgs): void {
|
|
|
500
527
|
}
|
|
501
528
|
if (phase === "end" || phase === "error") {
|
|
502
529
|
lastThinkingTextByRun.delete(evt.runId);
|
|
530
|
+
codexRunIds.delete(evt.runId);
|
|
503
531
|
}
|
|
504
532
|
}
|
|
505
533
|
|
|
@@ -0,0 +1,61 @@
|
|
|
1
|
+
import type { IncomingMessage, ServerResponse } from "node:http";
|
|
2
|
+
import { resolveApprovalOverGateway } from "openclaw/plugin-sdk/approval-gateway-runtime";
|
|
3
|
+
import { readJsonBody } from "../middleware/body.js";
|
|
4
|
+
import { extractBearerToken } from "../middleware/auth.js";
|
|
5
|
+
import { getHostOpenClawConfigSnapshot } from "../../host-config.js";
|
|
6
|
+
import { getFridayNextRuntime } from "../../runtime.js";
|
|
7
|
+
import { createFridayNextLogger } from "../../logging.js";
|
|
8
|
+
|
|
9
|
+
const VALID_DECISIONS = new Set(["allow-once", "allow-always", "deny"]);
|
|
10
|
+
|
|
11
|
+
/**
|
|
12
|
+
* POST /friday-next/approvals/{approvalId}
|
|
13
|
+
* Body: { decision: "allow-once" | "allow-always" | "deny", deviceId?: string }
|
|
14
|
+
*
|
|
15
|
+
* Submits the app user's decision for a pending exec/plugin approval back to the gateway. The bearer
|
|
16
|
+
* token gates auth (the device owner is the approver); the gateway then resumes / aborts the run.
|
|
17
|
+
*/
|
|
18
|
+
export async function handleApprovalDecision(
|
|
19
|
+
req: IncomingMessage,
|
|
20
|
+
res: ServerResponse,
|
|
21
|
+
approvalId: string,
|
|
22
|
+
): Promise<boolean> {
|
|
23
|
+
const log = createFridayNextLogger("approvals");
|
|
24
|
+
const json = (status: number, body: Record<string, unknown>) => {
|
|
25
|
+
res.statusCode = status;
|
|
26
|
+
res.setHeader("Content-Type", "application/json");
|
|
27
|
+
res.end(JSON.stringify(body));
|
|
28
|
+
return true;
|
|
29
|
+
};
|
|
30
|
+
|
|
31
|
+
if (req.method !== "POST") return json(405, { error: "Method Not Allowed" });
|
|
32
|
+
if (!extractBearerToken(req)) return json(401, { error: "Unauthorized: bearer token mismatch" });
|
|
33
|
+
if (!approvalId.trim()) return json(400, { error: "Missing approvalId" });
|
|
34
|
+
|
|
35
|
+
const body = await readJsonBody(req);
|
|
36
|
+
if (!body) return json(400, { error: "Invalid JSON body" });
|
|
37
|
+
|
|
38
|
+
const decision = typeof body.decision === "string" ? body.decision.trim() : "";
|
|
39
|
+
if (!VALID_DECISIONS.has(decision)) {
|
|
40
|
+
return json(400, { error: "decision must be allow-once | allow-always | deny" });
|
|
41
|
+
}
|
|
42
|
+
const deviceId = typeof body.deviceId === "string" ? body.deviceId.trim().toUpperCase() : "";
|
|
43
|
+
|
|
44
|
+
const cfg = getHostOpenClawConfigSnapshot(getFridayNextRuntime().config);
|
|
45
|
+
try {
|
|
46
|
+
await resolveApprovalOverGateway({
|
|
47
|
+
cfg: cfg as Parameters<typeof resolveApprovalOverGateway>[0]["cfg"],
|
|
48
|
+
approvalId: approvalId.trim(),
|
|
49
|
+
decision: decision as "allow-once" | "allow-always" | "deny",
|
|
50
|
+
senderId: deviceId || null,
|
|
51
|
+
allowPluginFallback: true,
|
|
52
|
+
clientDisplayName: deviceId ? `Friday Next (${deviceId})` : "Friday Next",
|
|
53
|
+
});
|
|
54
|
+
} catch (err) {
|
|
55
|
+
log.error(`resolveApprovalOverGateway failed: ${err instanceof Error ? err.message : String(err)}`);
|
|
56
|
+
return json(502, { error: "Approval resolution failed", detail: String(err) });
|
|
57
|
+
}
|
|
58
|
+
|
|
59
|
+
log.info(`approval ${approvalId} resolved decision=${decision} device=${deviceId || "(none)"}`);
|
|
60
|
+
return json(200, { ok: true, approvalId: approvalId.trim(), decision });
|
|
61
|
+
}
|
|
@@ -37,7 +37,11 @@ import {
|
|
|
37
37
|
import { sseEmitter } from "../../sse/emitter.js";
|
|
38
38
|
import { extractBearerToken } from "../middleware/auth.js";
|
|
39
39
|
import { readJsonBody } from "../middleware/body.js";
|
|
40
|
-
import {
|
|
40
|
+
import {
|
|
41
|
+
forwardAgentEventRaw,
|
|
42
|
+
isCodexRun,
|
|
43
|
+
registerFridaySessionDeviceMapping,
|
|
44
|
+
} from "../../friday-session.js";
|
|
41
45
|
import { touchFridayInbound } from "../../friday-inbound-stats.js";
|
|
42
46
|
import {
|
|
43
47
|
fridayAttachmentLookupKey,
|
|
@@ -631,6 +635,12 @@ export async function handleMessages(req: IncomingMessage, res: ServerResponse):
|
|
|
631
635
|
runId,
|
|
632
636
|
suppressTyping: true,
|
|
633
637
|
disableBlockStreaming: true,
|
|
638
|
+
// A1: feed the chosen thinking level into the run as a one-shot override so the model
|
|
639
|
+
// request asks for a reasoning summary. The session-stored `thinkingLevel` alone is NOT
|
|
640
|
+
// honored by the reply dispatch; `thinkingLevelOverride` has top priority in OpenClaw's
|
|
641
|
+
// resolution chain (get-reply-directives). Required for Codex (openai-chatgpt-responses)
|
|
642
|
+
// to emit any reasoning at all.
|
|
643
|
+
...(thinkingLevel ? { thinkingLevelOverride: thinkingLevel } : {}),
|
|
634
644
|
onModelSelected: (sel: any) => {
|
|
635
645
|
const name = typeof sel.model === "string" ? sel.model.trim() : "";
|
|
636
646
|
if (name) {
|
|
@@ -646,6 +656,18 @@ export async function handleMessages(req: IncomingMessage, res: ServerResponse):
|
|
|
646
656
|
: undefined;
|
|
647
657
|
const text = typeof rawText === "string" ? rawText : "";
|
|
648
658
|
log("REASONING_STREAM", normalizedDeviceId, runId, `textLen=${text.length}`);
|
|
659
|
+
// A2: the embedded runner already emits `stream: "thinking"` on the agent-event bus, so
|
|
660
|
+
// forwarding here would double it. The Codex app-server backend does NOT — reasoning text
|
|
661
|
+
// only arrives via this callback (cumulative snapshot). Forward it as a thinking event
|
|
662
|
+
// (reusing forwardAgentEventRaw's cumulative→delta rewrite) ONLY for Codex runs.
|
|
663
|
+
if (text && isCodexRun(runId)) {
|
|
664
|
+
forwardAgentEventRaw({
|
|
665
|
+
runId,
|
|
666
|
+
stream: "thinking",
|
|
667
|
+
data: { text },
|
|
668
|
+
sessionKey: baseSessionKey,
|
|
669
|
+
});
|
|
670
|
+
}
|
|
649
671
|
},
|
|
650
672
|
onReasoningEnd: async () => {
|
|
651
673
|
log("REASONING_STREAM_END", normalizedDeviceId, runId);
|
package/src/http/server.ts
CHANGED
|
@@ -13,6 +13,7 @@ import { handleFilesDownload } from "./handlers/files-download.js";
|
|
|
13
13
|
import { handleCancel } from "./handlers/cancel.js";
|
|
14
14
|
import { handleDeviceApprove } from "./handlers/device-approve.js";
|
|
15
15
|
import { handleNodesApprove } from "./handlers/nodes-approve.js";
|
|
16
|
+
import { handleApprovalDecision } from "./handlers/approvals.js";
|
|
16
17
|
import { handleSessionsSettings } from "./handlers/sessions-settings.js";
|
|
17
18
|
import { handleModelsList } from "./handlers/models-list.js";
|
|
18
19
|
import { handleAgentsList } from "./handlers/agents-list.js";
|
|
@@ -76,6 +77,12 @@ async function handleFridayNextRoute(req: IncomingMessage, res: ServerResponse):
|
|
|
76
77
|
return await handleNodesApprove(req, res);
|
|
77
78
|
}
|
|
78
79
|
|
|
80
|
+
// Route: POST /friday-next/approvals/{approvalId} (submit exec/plugin approval decision)
|
|
81
|
+
if (req.method === "POST" && pathname.startsWith("/friday-next/approvals/")) {
|
|
82
|
+
const approvalId = decodeURIComponent(pathname.slice("/friday-next/approvals/".length));
|
|
83
|
+
return await handleApprovalDecision(req, res, approvalId);
|
|
84
|
+
}
|
|
85
|
+
|
|
79
86
|
if (
|
|
80
87
|
(req.method === "PUT" || req.method === "GET") &&
|
|
81
88
|
pathname === "/friday-next/sessions/settings"
|
package/src/sse/emitter.ts
CHANGED
|
@@ -1,39 +0,0 @@
|
|
|
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 {};
|
|
@@ -1,174 +0,0 @@
|
|
|
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
|
-
}
|
|
@@ -1,49 +0,0 @@
|
|
|
1
|
-
import { deleteFridaySession, toSessionStoreKey } from "../../session/session-manager.js";
|
|
2
|
-
import { getActiveRunIds } from "../../agent/active-runs.js";
|
|
3
|
-
import { abortRun } from "../../agent/abort-run.js";
|
|
4
|
-
import { getRunRoute } from "../../run-metadata.js";
|
|
5
|
-
import { sseEmitter } from "../../sse/emitter.js";
|
|
6
|
-
import { readJsonBody } from "../middleware/body.js";
|
|
7
|
-
import { extractBearerToken } from "../middleware/auth.js";
|
|
8
|
-
async function cancelActiveRunsForSession(sessionKey) {
|
|
9
|
-
const storeKey = toSessionStoreKey(sessionKey);
|
|
10
|
-
const cancelled = [];
|
|
11
|
-
for (const runId of getActiveRunIds()) {
|
|
12
|
-
const route = getRunRoute(runId);
|
|
13
|
-
if (route?.sessionKey === storeKey) {
|
|
14
|
-
await abortRun(runId);
|
|
15
|
-
sseEmitter.untrackRun(runId);
|
|
16
|
-
cancelled.push(runId);
|
|
17
|
-
}
|
|
18
|
-
}
|
|
19
|
-
return cancelled;
|
|
20
|
-
}
|
|
21
|
-
export async function handleSessionsDelete(req, res) {
|
|
22
|
-
if (req.method !== "DELETE") {
|
|
23
|
-
res.statusCode = 405;
|
|
24
|
-
res.setHeader("Content-Type", "application/json");
|
|
25
|
-
res.end(JSON.stringify({ error: "Method Not Allowed" }));
|
|
26
|
-
return true;
|
|
27
|
-
}
|
|
28
|
-
const token = extractBearerToken(req);
|
|
29
|
-
if (!token) {
|
|
30
|
-
res.statusCode = 401;
|
|
31
|
-
res.setHeader("Content-Type", "application/json");
|
|
32
|
-
res.end(JSON.stringify({ error: "Unauthorized: bearer token mismatch" }));
|
|
33
|
-
return true;
|
|
34
|
-
}
|
|
35
|
-
const body = await readJsonBody(req);
|
|
36
|
-
const sessionKey = typeof body?.sessionKey === "string" ? body.sessionKey.trim() : "";
|
|
37
|
-
if (!sessionKey) {
|
|
38
|
-
res.statusCode = 400;
|
|
39
|
-
res.setHeader("Content-Type", "application/json");
|
|
40
|
-
res.end(JSON.stringify({ error: "Missing required field: sessionKey" }));
|
|
41
|
-
return true;
|
|
42
|
-
}
|
|
43
|
-
const cancelledRuns = await cancelActiveRunsForSession(sessionKey);
|
|
44
|
-
const result = deleteFridaySession(sessionKey);
|
|
45
|
-
res.statusCode = 200;
|
|
46
|
-
res.setHeader("Content-Type", "application/json");
|
|
47
|
-
res.end(JSON.stringify({ ok: true, ...result, cancelledRuns }));
|
|
48
|
-
return true;
|
|
49
|
-
}
|