@syengup/friday-channel-next 0.1.39 → 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/agent/subagent-registry.d.ts +4 -0
- package/dist/src/agent/subagent-registry.js +1 -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 +59 -1
- package/dist/src/http/handlers/agents-list.js +5 -1
- 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/agent/subagent-registry.ts +3 -1
- 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/e2e/subagent.e2e.test.ts +6 -0
- package/src/friday-session.forward-agent.test.ts +127 -0
- package/src/friday-session.ts +76 -1
- package/src/http/handlers/agents-list.test.ts +28 -0
- package/src/http/handlers/agents-list.ts +5 -1
- 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
|
@@ -71,6 +71,34 @@ describe("handleAgentsList", () => {
|
|
|
71
71
|
expect(body.agents).toEqual([{ id: "main", isDefault: true }]);
|
|
72
72
|
});
|
|
73
73
|
|
|
74
|
+
it("resolves the implicit main name from IDENTITY.md when no agents.list exists", async () => {
|
|
75
|
+
const workspace = fs.mkdtempSync(path.join(os.tmpdir(), "friday-identity-main-"));
|
|
76
|
+
fs.writeFileSync(
|
|
77
|
+
path.join(workspace, "IDENTITY.md"),
|
|
78
|
+
"# IDENTITY.md\n\n- **Name:** F.R.I.D.A.Y\n- **Emoji:** 🌿\n",
|
|
79
|
+
);
|
|
80
|
+
try {
|
|
81
|
+
setFridayAgentForwardRuntime({
|
|
82
|
+
runtime: {
|
|
83
|
+
agent: {
|
|
84
|
+
session: { resolveStorePath: () => "", loadSessionStore: () => ({}) },
|
|
85
|
+
resolveAgentWorkspaceDir: () => workspace,
|
|
86
|
+
},
|
|
87
|
+
config: { current: () => ({ agents: { defaults: {} } }) },
|
|
88
|
+
},
|
|
89
|
+
} as any);
|
|
90
|
+
|
|
91
|
+
const res = new MockRes();
|
|
92
|
+
await handleAgentsList(makeReq(AUTH), res as any);
|
|
93
|
+
|
|
94
|
+
const body = JSON.parse(res.body);
|
|
95
|
+
expect(body.defaultAgentId).toBe("main");
|
|
96
|
+
expect(body.agents).toEqual([{ id: "main", name: "F.R.I.D.A.Y", isDefault: true }]);
|
|
97
|
+
} finally {
|
|
98
|
+
fs.rmSync(workspace, { recursive: true, force: true });
|
|
99
|
+
}
|
|
100
|
+
});
|
|
101
|
+
|
|
74
102
|
it("lists configured agents with normalized ids and resolved fields", async () => {
|
|
75
103
|
setConfig({
|
|
76
104
|
agents: {
|
|
@@ -106,8 +106,12 @@ function resolveConfiguredAgents(): ResolvedAgents {
|
|
|
106
106
|
const list = agents?.list as Array<Record<string, unknown>> | undefined;
|
|
107
107
|
|
|
108
108
|
if (!Array.isArray(list) || list.length === 0) {
|
|
109
|
+
// Implicit `main` agent (no `agents.list`): config carries no name, so fall
|
|
110
|
+
// back to the workspace IDENTITY.md `Name` — the same source ControlUI and
|
|
111
|
+
// the list branch below use — instead of letting the app show the raw id.
|
|
112
|
+
const name = readWorkspaceIdentityName(rt, cfg, DEFAULT_AGENT_ID);
|
|
109
113
|
return {
|
|
110
|
-
agents: [{ id: DEFAULT_AGENT_ID, isDefault: true }],
|
|
114
|
+
agents: [{ id: DEFAULT_AGENT_ID, isDefault: true, ...(name ? { name } : {}) }],
|
|
111
115
|
defaultAgentId: DEFAULT_AGENT_ID,
|
|
112
116
|
};
|
|
113
117
|
}
|
|
@@ -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
|
-
}
|