@syengup/friday-channel-next 0.1.40 → 1.0.1
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 +46 -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/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.forward-agent.test.ts +50 -0
- package/src/friday-session.ts +54 -2
- 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
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,37 @@ 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
|
+
|
|
376
|
+
// Codex app-server reasoning: newer OpenClaw cores stopped invoking the dispatch
|
|
377
|
+
// `onReasoningStream` callback (the A2 path in messages.ts) and instead stream the
|
|
378
|
+
// reasoning summary on the agent-event bus as `stream:"item" kind:"preamble"` with a
|
|
379
|
+
// cumulative `progressText` (source "codex-app-server"). The Friday app only renders
|
|
380
|
+
// `stream:"thinking"`, so translate it here — synthesize a thinking event reusing the
|
|
381
|
+
// cumulative→delta rewrite below. The raw preamble item is still forwarded but the app
|
|
382
|
+
// ignores unknown item kinds. (The onReasoningStream callback stays as a harmless
|
|
383
|
+
// fallback for cores that still fire it.)
|
|
384
|
+
if (
|
|
385
|
+
evt.stream === "item" &&
|
|
386
|
+
evt.data.kind === "preamble" &&
|
|
387
|
+
evt.data.source === "codex-app-server"
|
|
388
|
+
) {
|
|
389
|
+
codexRunIds.add(evt.runId);
|
|
390
|
+
const reasoningText = typeof evt.data.progressText === "string" ? evt.data.progressText : "";
|
|
391
|
+
if (reasoningText) {
|
|
392
|
+
forwardAgentEventRaw({
|
|
393
|
+
runId: evt.runId,
|
|
394
|
+
stream: "thinking",
|
|
395
|
+
data: { text: reasoningText },
|
|
396
|
+
sessionKey: evt.sessionKey ?? sk,
|
|
397
|
+
});
|
|
398
|
+
}
|
|
399
|
+
}
|
|
400
|
+
|
|
349
401
|
// Register sessionKey → runId so we can resolve parentRunId
|
|
350
402
|
if (sk && evt.stream === "lifecycle" && evt.data.phase === "start") {
|
|
351
403
|
registerSessionKeyForRun(sk, evt.runId);
|
|
@@ -442,8 +494,7 @@ export function forwardAgentEventRaw(evt: ForwardAgentEventArgs): void {
|
|
|
442
494
|
if (evt.stream === "lifecycle" && evt.data.phase === "start") {
|
|
443
495
|
const announced = parseAnnounceRunId(evt.runId);
|
|
444
496
|
if (announced) {
|
|
445
|
-
const entry =
|
|
446
|
-
lookupByChildSessionKey(announced.childSessionKey) ?? lookupByRunId(evt.runId);
|
|
497
|
+
const entry = lookupByChildSessionKey(announced.childSessionKey) ?? lookupByRunId(evt.runId);
|
|
447
498
|
sseEmitter.broadcast(
|
|
448
499
|
{
|
|
449
500
|
type: "subagent",
|
|
@@ -500,6 +551,7 @@ export function forwardAgentEventRaw(evt: ForwardAgentEventArgs): void {
|
|
|
500
551
|
}
|
|
501
552
|
if (phase === "end" || phase === "error") {
|
|
502
553
|
lastThinkingTextByRun.delete(evt.runId);
|
|
554
|
+
codexRunIds.delete(evt.runId);
|
|
503
555
|
}
|
|
504
556
|
}
|
|
505
557
|
|
|
@@ -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
|
-
}
|