@syengup/friday-channel-next 0.0.35 → 0.0.38
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.d.ts +4 -0
- package/dist/index.js +182 -0
- package/dist/src/agent/abort-run.d.ts +1 -0
- package/dist/src/agent/abort-run.js +11 -0
- package/dist/src/agent/active-runs.d.ts +9 -0
- package/dist/src/agent/active-runs.js +20 -0
- package/dist/src/agent/dispatch-bridge.d.ts +5 -0
- package/dist/src/agent/dispatch-bridge.js +12 -0
- package/dist/src/agent/media-bridge.d.ts +4 -0
- package/dist/src/agent/media-bridge.js +21 -0
- package/dist/src/agent/subagent-registry.d.ts +68 -0
- package/dist/src/agent/subagent-registry.js +142 -0
- package/dist/src/agent-forward-runtime.d.ts +17 -0
- package/dist/src/agent-forward-runtime.js +16 -0
- package/dist/src/agent-run-context-bridge.d.ts +13 -0
- package/dist/src/agent-run-context-bridge.js +23 -0
- package/dist/src/channel-actions.d.ts +13 -0
- package/dist/src/channel-actions.js +101 -0
- package/dist/src/channel.d.ts +6 -0
- package/dist/src/channel.js +248 -0
- package/dist/src/collect-message-media-paths.d.ts +11 -0
- package/dist/src/collect-message-media-paths.js +143 -0
- package/dist/src/config.d.ts +15 -0
- package/dist/src/config.js +39 -0
- package/dist/src/friday-inbound-stats.d.ts +2 -0
- package/dist/src/friday-inbound-stats.js +8 -0
- package/dist/src/friday-session.d.ts +40 -0
- package/dist/src/friday-session.js +395 -0
- package/dist/src/host-config.d.ts +1 -0
- package/dist/src/host-config.js +15 -0
- package/dist/src/http/handlers/cancel.d.ts +2 -0
- package/dist/src/http/handlers/cancel.js +33 -0
- package/dist/src/http/handlers/device-approve.d.ts +2 -0
- package/dist/src/http/handlers/device-approve.js +125 -0
- package/dist/src/http/handlers/files-download.d.ts +10 -0
- package/dist/src/http/handlers/files-download.js +210 -0
- package/dist/src/http/handlers/files-upload.d.ts +8 -0
- package/dist/src/http/handlers/files-upload.js +136 -0
- package/dist/src/http/handlers/files.d.ts +75 -0
- package/dist/src/http/handlers/files.js +305 -0
- package/dist/src/http/handlers/messages.d.ts +34 -0
- package/dist/src/http/handlers/messages.js +476 -0
- package/dist/src/http/handlers/models-list.d.ts +10 -0
- package/dist/src/http/handlers/models-list.js +113 -0
- package/dist/src/http/handlers/nodes-approve.d.ts +2 -0
- package/dist/src/http/handlers/nodes-approve.js +146 -0
- package/dist/src/http/handlers/sessions-delete.d.ts +2 -0
- package/dist/src/http/handlers/sessions-delete.js +49 -0
- package/dist/src/http/handlers/sessions-settings.d.ts +2 -0
- package/dist/src/http/handlers/sessions-settings.js +71 -0
- package/dist/src/http/handlers/sse.d.ts +2 -0
- package/dist/src/http/handlers/sse.js +70 -0
- package/dist/src/http/handlers/status.d.ts +2 -0
- package/dist/src/http/handlers/status.js +29 -0
- package/dist/src/http/middleware/auth.d.ts +13 -0
- package/dist/src/http/middleware/auth.js +29 -0
- package/dist/src/http/middleware/body.d.ts +2 -0
- package/dist/src/http/middleware/body.js +24 -0
- package/dist/src/http/middleware/cors.d.ts +2 -0
- package/dist/src/http/middleware/cors.js +11 -0
- package/dist/src/http/server.d.ts +19 -0
- package/dist/src/http/server.js +87 -0
- package/dist/src/logging.d.ts +7 -0
- package/dist/src/logging.js +28 -0
- package/dist/src/run-metadata.d.ts +25 -0
- package/dist/src/run-metadata.js +139 -0
- package/dist/src/runtime.d.ts +13 -0
- package/dist/src/runtime.js +5 -0
- package/dist/src/session/session-manager.d.ts +22 -0
- package/dist/src/session/session-manager.js +190 -0
- package/dist/src/session-usage-snapshot.d.ts +23 -0
- package/dist/src/session-usage-snapshot.js +65 -0
- package/dist/src/sse/emitter.d.ts +59 -0
- package/dist/src/sse/emitter.js +219 -0
- package/dist/src/sse/offline-queue.d.ts +26 -0
- package/dist/src/sse/offline-queue.js +134 -0
- package/dist/src/vendor/runtime-store.d.ts +26 -0
- package/dist/src/vendor/runtime-store.js +60 -0
- package/index.ts +10 -4
- package/package.json +11 -10
- package/src/agent/subagent-registry.ts +195 -0
- package/src/channel.ts +6 -4
- package/src/e2e/subagent-smoke.e2e.test.ts +223 -0
- package/src/e2e/subagent.e2e.test.ts +502 -0
- package/src/friday-session.ts +140 -1
- package/src/http/handlers/device-approve.test.ts +0 -1
- package/src/http/handlers/device-approve.ts +0 -2
- package/src/http/handlers/files-download.ts +4 -1
- package/src/http/handlers/files.ts +7 -4
- package/src/http/handlers/messages.ts +54 -4
- package/src/http/handlers/models-list.ts +24 -2
- package/src/http/handlers/nodes-approve.test.ts +288 -0
- package/src/http/handlers/nodes-approve.ts +189 -0
- package/src/http/server.ts +5 -0
- package/src/openclaw.d.ts +5 -0
- package/src/sse/emitter.ts +1 -1
- package/src/test-support/mock-runtime.ts +2 -0
|
@@ -0,0 +1,139 @@
|
|
|
1
|
+
const runRouteById = new Map();
|
|
2
|
+
const runMetadataById = new Map();
|
|
3
|
+
const finalDeliveredRunIds = new Set();
|
|
4
|
+
/** Vitest / harness: clears per-run metadata and final-delivered flags (not routes). */
|
|
5
|
+
export function resetRunMetadataForTest() {
|
|
6
|
+
runMetadataById.clear();
|
|
7
|
+
finalDeliveredRunIds.clear();
|
|
8
|
+
}
|
|
9
|
+
export function registerRunRoute(route) {
|
|
10
|
+
if (!route.runId.trim())
|
|
11
|
+
return;
|
|
12
|
+
runRouteById.set(route.runId, route);
|
|
13
|
+
}
|
|
14
|
+
export function getRunRoute(runId) {
|
|
15
|
+
return runRouteById.get(runId);
|
|
16
|
+
}
|
|
17
|
+
export function setRunMetadata(runId, metadata) {
|
|
18
|
+
if (!runId.trim())
|
|
19
|
+
return;
|
|
20
|
+
const existing = runMetadataById.get(runId) ?? {};
|
|
21
|
+
runMetadataById.set(runId, { ...existing, ...metadata });
|
|
22
|
+
}
|
|
23
|
+
export function getRunMetadata(runId) {
|
|
24
|
+
return runMetadataById.get(runId);
|
|
25
|
+
}
|
|
26
|
+
export function markRunFinalDelivered(runId) {
|
|
27
|
+
if (!runId.trim())
|
|
28
|
+
return;
|
|
29
|
+
finalDeliveredRunIds.add(runId);
|
|
30
|
+
}
|
|
31
|
+
export function hasRunFinalDelivered(runId) {
|
|
32
|
+
return finalDeliveredRunIds.has(runId);
|
|
33
|
+
}
|
|
34
|
+
function finiteNumber(value) {
|
|
35
|
+
if (typeof value === "number" && Number.isFinite(value))
|
|
36
|
+
return value;
|
|
37
|
+
return undefined;
|
|
38
|
+
}
|
|
39
|
+
function recordValue(value) {
|
|
40
|
+
if (value && typeof value === "object" && !Array.isArray(value)) {
|
|
41
|
+
return value;
|
|
42
|
+
}
|
|
43
|
+
return null;
|
|
44
|
+
}
|
|
45
|
+
function pickInputTokens(u) {
|
|
46
|
+
return (finiteNumber(u.input) ??
|
|
47
|
+
finiteNumber(u.inputTokens) ??
|
|
48
|
+
finiteNumber(u.input_tokens) ??
|
|
49
|
+
finiteNumber(u.promptTokens) ??
|
|
50
|
+
finiteNumber(u.prompt_tokens) ??
|
|
51
|
+
finiteNumber(u.prompt_n));
|
|
52
|
+
}
|
|
53
|
+
function pickOutputTokens(u) {
|
|
54
|
+
return (finiteNumber(u.output) ??
|
|
55
|
+
finiteNumber(u.outputTokens) ??
|
|
56
|
+
finiteNumber(u.output_tokens) ??
|
|
57
|
+
finiteNumber(u.completionTokens) ??
|
|
58
|
+
finiteNumber(u.completion_tokens) ??
|
|
59
|
+
finiteNumber(u.predicted_n));
|
|
60
|
+
}
|
|
61
|
+
function pickCacheRead(u) {
|
|
62
|
+
const inDet = recordValue(u.input_tokens_details);
|
|
63
|
+
const prDet = recordValue(u.prompt_tokens_details);
|
|
64
|
+
return (finiteNumber(u.cacheRead) ??
|
|
65
|
+
finiteNumber(u.cache_read) ??
|
|
66
|
+
finiteNumber(u.cache_read_input_tokens) ??
|
|
67
|
+
finiteNumber(u.cached_tokens) ??
|
|
68
|
+
(inDet ? finiteNumber(inDet.cached_tokens) : undefined) ??
|
|
69
|
+
(prDet ? finiteNumber(prDet.cached_tokens) : undefined));
|
|
70
|
+
}
|
|
71
|
+
function pickCacheWrite(u) {
|
|
72
|
+
return (finiteNumber(u.cacheWrite) ??
|
|
73
|
+
finiteNumber(u.cache_write) ??
|
|
74
|
+
finiteNumber(u.cache_creation_input_tokens));
|
|
75
|
+
}
|
|
76
|
+
/** Best-effort prompt-side context footprint from a provider usage object. */
|
|
77
|
+
export function contextTokensFromUsageRecord(u) {
|
|
78
|
+
const inp = pickInputTokens(u);
|
|
79
|
+
const cr = pickCacheRead(u);
|
|
80
|
+
const cw = pickCacheWrite(u);
|
|
81
|
+
const total = finiteNumber(u.total) ?? finiteNumber(u.total_tokens) ?? finiteNumber(u.totalTokens);
|
|
82
|
+
const out = pickOutputTokens(u);
|
|
83
|
+
if (inp !== undefined || cr !== undefined || cw !== undefined) {
|
|
84
|
+
return Math.max(0, Math.floor((inp ?? 0) + (cr ?? 0) + (cw ?? 0)));
|
|
85
|
+
}
|
|
86
|
+
if (total !== undefined && out !== undefined && total >= out) {
|
|
87
|
+
return Math.max(0, Math.floor(total - out));
|
|
88
|
+
}
|
|
89
|
+
return undefined;
|
|
90
|
+
}
|
|
91
|
+
function pickContextWindowMaxFromData(data) {
|
|
92
|
+
const v = finiteNumber(data.contextWindow) ??
|
|
93
|
+
finiteNumber(data.context_window) ??
|
|
94
|
+
finiteNumber(data.maxContextTokens) ??
|
|
95
|
+
finiteNumber(data.max_context_tokens);
|
|
96
|
+
if (typeof v === "number" && v > 0)
|
|
97
|
+
return Math.floor(v);
|
|
98
|
+
return undefined;
|
|
99
|
+
}
|
|
100
|
+
export function ingestAgentEventMetadata(runId, data) {
|
|
101
|
+
if (!runId.trim())
|
|
102
|
+
return;
|
|
103
|
+
const next = {};
|
|
104
|
+
const modelName = (typeof data.modelName === "string" && data.modelName.trim()) ||
|
|
105
|
+
(typeof data.model === "string" && data.model.trim()) ||
|
|
106
|
+
undefined;
|
|
107
|
+
if (modelName)
|
|
108
|
+
next.modelName = modelName;
|
|
109
|
+
const usage = recordValue(data.usage);
|
|
110
|
+
const totalTokens = finiteNumber(data.totalTokens) ??
|
|
111
|
+
finiteNumber(data.total_tokens) ??
|
|
112
|
+
finiteNumber(usage?.totalTokens) ??
|
|
113
|
+
finiteNumber(usage?.total_tokens) ??
|
|
114
|
+
finiteNumber(usage?.total);
|
|
115
|
+
if (typeof totalTokens === "number" && totalTokens > 0) {
|
|
116
|
+
next.totalTokens = Math.floor(totalTokens);
|
|
117
|
+
}
|
|
118
|
+
const usageForContext = usage ?? data;
|
|
119
|
+
const ctxUsed = contextTokensFromUsageRecord(usageForContext);
|
|
120
|
+
if (typeof ctxUsed === "number" && ctxUsed > 0) {
|
|
121
|
+
next.contextTokensUsed = ctxUsed;
|
|
122
|
+
}
|
|
123
|
+
const ctxMax = pickContextWindowMaxFromData(data);
|
|
124
|
+
if (typeof ctxMax === "number") {
|
|
125
|
+
next.contextWindowMax = ctxMax;
|
|
126
|
+
}
|
|
127
|
+
if (!next.contextWindowMax && usage) {
|
|
128
|
+
const fromUsage = pickContextWindowMaxFromData(usage);
|
|
129
|
+
if (typeof fromUsage === "number") {
|
|
130
|
+
next.contextWindowMax = fromUsage;
|
|
131
|
+
}
|
|
132
|
+
}
|
|
133
|
+
if (next.modelName ||
|
|
134
|
+
typeof next.totalTokens === "number" ||
|
|
135
|
+
typeof next.contextTokensUsed === "number" ||
|
|
136
|
+
typeof next.contextWindowMax === "number") {
|
|
137
|
+
setRunMetadata(runId, next);
|
|
138
|
+
}
|
|
139
|
+
}
|
|
@@ -0,0 +1,13 @@
|
|
|
1
|
+
type FridayRuntime = {
|
|
2
|
+
config: {
|
|
3
|
+
loadConfig: () => unknown;
|
|
4
|
+
};
|
|
5
|
+
logger?: {
|
|
6
|
+
info?: (...args: unknown[]) => void;
|
|
7
|
+
warn?: (...args: unknown[]) => void;
|
|
8
|
+
};
|
|
9
|
+
};
|
|
10
|
+
export declare const setFridayNextRuntime: (next: FridayRuntime) => void;
|
|
11
|
+
export declare const getFridayNextRuntime: () => FridayRuntime;
|
|
12
|
+
export declare const clearFridayNextRuntime: () => void;
|
|
13
|
+
export {};
|
|
@@ -0,0 +1,5 @@
|
|
|
1
|
+
import { createPluginRuntimeStore } from "./vendor/runtime-store.js";
|
|
2
|
+
const { setRuntime, getRuntime, clearRuntime } = createPluginRuntimeStore("Friday Next runtime not initialized");
|
|
3
|
+
export const setFridayNextRuntime = setRuntime;
|
|
4
|
+
export const getFridayNextRuntime = getRuntime;
|
|
5
|
+
export const clearFridayNextRuntime = clearRuntime;
|
|
@@ -0,0 +1,22 @@
|
|
|
1
|
+
export declare function splitModelRef(modelRef: string): {
|
|
2
|
+
provider?: string;
|
|
3
|
+
modelId: string;
|
|
4
|
+
};
|
|
5
|
+
export declare function toSessionStoreKey(rawSessionKey: string): string;
|
|
6
|
+
export declare function ensureSessionLevels(sessionKey: string, reasoningLevel: string, thinkingLevel: string, historyDir?: string): void;
|
|
7
|
+
export declare function resolveSessionsDir(historyDir?: string): string;
|
|
8
|
+
export interface DeleteSessionResult {
|
|
9
|
+
sessionKey: string;
|
|
10
|
+
sessionId?: string;
|
|
11
|
+
transcriptDeleted?: boolean;
|
|
12
|
+
}
|
|
13
|
+
export declare function deleteFridaySession(sessionKey: string, historyDir?: string): DeleteSessionResult;
|
|
14
|
+
export interface FridaySessionSettings {
|
|
15
|
+
reasoningLevel?: string;
|
|
16
|
+
thinkingLevel?: string;
|
|
17
|
+
modelRef?: string;
|
|
18
|
+
providerOverride?: string;
|
|
19
|
+
modelOverride?: string;
|
|
20
|
+
}
|
|
21
|
+
export declare function setSessionSettings(sessionKey: string, settings: FridaySessionSettings, historyDir?: string): FridaySessionSettings;
|
|
22
|
+
export declare function getSessionSettings(sessionKey: string, historyDir?: string): FridaySessionSettings;
|
|
@@ -0,0 +1,190 @@
|
|
|
1
|
+
import { join } from "node:path";
|
|
2
|
+
import os from "node:os";
|
|
3
|
+
import { readFileSync, writeFileSync, unlinkSync } from "node:fs";
|
|
4
|
+
const FRIDAY_AGENT_ID = "main";
|
|
5
|
+
const SESSION_ID_RE = /^[a-z0-9][a-z0-9._-]{0,127}$/i;
|
|
6
|
+
function deriveOpenClawBaseDir(historyDir) {
|
|
7
|
+
if (historyDir) {
|
|
8
|
+
const match = historyDir.replace(/\/+$/, "").match(/(.*\/\.openclaw)\//);
|
|
9
|
+
if (match?.[1])
|
|
10
|
+
return match[1];
|
|
11
|
+
}
|
|
12
|
+
return join(os.homedir(), ".openclaw");
|
|
13
|
+
}
|
|
14
|
+
export function splitModelRef(modelRef) {
|
|
15
|
+
const slashIdx = modelRef.indexOf("/");
|
|
16
|
+
if (slashIdx > 0) {
|
|
17
|
+
return { provider: modelRef.slice(0, slashIdx), modelId: modelRef.slice(slashIdx + 1) };
|
|
18
|
+
}
|
|
19
|
+
return { modelId: modelRef };
|
|
20
|
+
}
|
|
21
|
+
export function toSessionStoreKey(rawSessionKey) {
|
|
22
|
+
const raw = rawSessionKey.trim();
|
|
23
|
+
const lowered = raw.trim().toLowerCase();
|
|
24
|
+
if (!raw || lowered === "main") {
|
|
25
|
+
return `agent:${FRIDAY_AGENT_ID}:main`;
|
|
26
|
+
}
|
|
27
|
+
const parts = lowered.split(":").filter(Boolean);
|
|
28
|
+
if (parts.length >= 3 && parts[0] === "agent") {
|
|
29
|
+
const agentId = parts[1];
|
|
30
|
+
const rest = parts.slice(2).join(":");
|
|
31
|
+
if (agentId && rest) {
|
|
32
|
+
return `agent:${agentId}:${rest}`;
|
|
33
|
+
}
|
|
34
|
+
}
|
|
35
|
+
if (lowered.startsWith("agent:")) {
|
|
36
|
+
return lowered;
|
|
37
|
+
}
|
|
38
|
+
return `agent:${FRIDAY_AGENT_ID}:${lowered}`;
|
|
39
|
+
}
|
|
40
|
+
function toSafeSessionId(raw) {
|
|
41
|
+
const s = raw.trim();
|
|
42
|
+
if (SESSION_ID_RE.test(s))
|
|
43
|
+
return s;
|
|
44
|
+
const slug = s
|
|
45
|
+
.replace(/[^a-z0-9._-]+/gi, "-")
|
|
46
|
+
.replace(/-+/g, "-")
|
|
47
|
+
.replace(/^[-._]+|[-._]+$/g, "");
|
|
48
|
+
const base = slug || "session";
|
|
49
|
+
const prefixed = /^[a-z0-9]/i.test(base) ? base : `s${base}`;
|
|
50
|
+
return prefixed.slice(0, 128);
|
|
51
|
+
}
|
|
52
|
+
function sessionIdForSessionsFile(fileKey, rawSessionKey) {
|
|
53
|
+
const candidates = [rawSessionKey.trim(), fileKey.trim()];
|
|
54
|
+
for (const c of candidates) {
|
|
55
|
+
if (SESSION_ID_RE.test(c))
|
|
56
|
+
return c;
|
|
57
|
+
if (c.startsWith(`agent:${FRIDAY_AGENT_ID}:`)) {
|
|
58
|
+
const tail = c.slice(`agent:${FRIDAY_AGENT_ID}:`.length);
|
|
59
|
+
if (SESSION_ID_RE.test(tail))
|
|
60
|
+
return tail;
|
|
61
|
+
return toSafeSessionId(tail);
|
|
62
|
+
}
|
|
63
|
+
}
|
|
64
|
+
return toSafeSessionId(rawSessionKey || fileKey);
|
|
65
|
+
}
|
|
66
|
+
function resolveSessionsFilePath(historyDir) {
|
|
67
|
+
const base = deriveOpenClawBaseDir(historyDir);
|
|
68
|
+
return join(base, "agents/main/sessions/sessions.json");
|
|
69
|
+
}
|
|
70
|
+
function readSessionsData(path) {
|
|
71
|
+
try {
|
|
72
|
+
return JSON.parse(readFileSync(path, "utf-8"));
|
|
73
|
+
}
|
|
74
|
+
catch {
|
|
75
|
+
return null;
|
|
76
|
+
}
|
|
77
|
+
}
|
|
78
|
+
function writeSessionsData(path, data) {
|
|
79
|
+
try {
|
|
80
|
+
writeFileSync(path, JSON.stringify(data, null, 2), "utf-8");
|
|
81
|
+
}
|
|
82
|
+
catch {
|
|
83
|
+
// best-effort
|
|
84
|
+
}
|
|
85
|
+
}
|
|
86
|
+
function upsertSessionEntry(data, fileKey, sessionKey) {
|
|
87
|
+
const safeSessionId = sessionIdForSessionsFile(fileKey, sessionKey);
|
|
88
|
+
if (!data[fileKey]) {
|
|
89
|
+
data[fileKey] = { sessionId: safeSessionId, updatedAt: Date.now(), systemSent: true };
|
|
90
|
+
}
|
|
91
|
+
const currentSessionId = data[fileKey]["sessionId"];
|
|
92
|
+
if (typeof currentSessionId !== "string" || !SESSION_ID_RE.test(currentSessionId)) {
|
|
93
|
+
data[fileKey]["sessionId"] = safeSessionId;
|
|
94
|
+
}
|
|
95
|
+
}
|
|
96
|
+
export function ensureSessionLevels(sessionKey, reasoningLevel, thinkingLevel, historyDir) {
|
|
97
|
+
setSessionSettings(sessionKey, { reasoningLevel, thinkingLevel }, historyDir);
|
|
98
|
+
}
|
|
99
|
+
export function resolveSessionsDir(historyDir) {
|
|
100
|
+
const base = deriveOpenClawBaseDir(historyDir);
|
|
101
|
+
return join(base, "agents/main/sessions");
|
|
102
|
+
}
|
|
103
|
+
export function deleteFridaySession(sessionKey, historyDir) {
|
|
104
|
+
const result = { sessionKey };
|
|
105
|
+
const sessionsFile = resolveSessionsFilePath(historyDir);
|
|
106
|
+
const data = readSessionsData(sessionsFile);
|
|
107
|
+
if (!data)
|
|
108
|
+
return result;
|
|
109
|
+
const fileKey = toSessionStoreKey(sessionKey);
|
|
110
|
+
const entry = data[fileKey];
|
|
111
|
+
if (!entry)
|
|
112
|
+
return result;
|
|
113
|
+
const sessionId = typeof entry["sessionId"] === "string" ? entry["sessionId"] : undefined;
|
|
114
|
+
const sessionFilePath = typeof entry["sessionFile"] === "string" ? entry["sessionFile"] : undefined;
|
|
115
|
+
result.sessionId = sessionId;
|
|
116
|
+
if (sessionFilePath) {
|
|
117
|
+
try {
|
|
118
|
+
unlinkSync(sessionFilePath);
|
|
119
|
+
result.transcriptDeleted = true;
|
|
120
|
+
}
|
|
121
|
+
catch { /* gone already */ }
|
|
122
|
+
}
|
|
123
|
+
if (sessionId) {
|
|
124
|
+
const dir = resolveSessionsDir(historyDir);
|
|
125
|
+
for (const suffix of [".trajectory.jsonl", ".trajectory-path.json"]) {
|
|
126
|
+
try {
|
|
127
|
+
unlinkSync(join(dir, `${sessionId}${suffix}`));
|
|
128
|
+
}
|
|
129
|
+
catch { /* optional */ }
|
|
130
|
+
}
|
|
131
|
+
}
|
|
132
|
+
delete data[fileKey];
|
|
133
|
+
writeSessionsData(sessionsFile, data);
|
|
134
|
+
return result;
|
|
135
|
+
}
|
|
136
|
+
export function setSessionSettings(sessionKey, settings, historyDir) {
|
|
137
|
+
try {
|
|
138
|
+
const sessionsFile = resolveSessionsFilePath(historyDir);
|
|
139
|
+
const data = readSessionsData(sessionsFile);
|
|
140
|
+
if (!data)
|
|
141
|
+
return {};
|
|
142
|
+
const fileKey = toSessionStoreKey(sessionKey);
|
|
143
|
+
upsertSessionEntry(data, fileKey, sessionKey);
|
|
144
|
+
const fieldKeys = [
|
|
145
|
+
"reasoningLevel", "thinkingLevel", "modelRef", "providerOverride", "modelOverride",
|
|
146
|
+
];
|
|
147
|
+
let updated = false;
|
|
148
|
+
for (const key of fieldKeys) {
|
|
149
|
+
const value = settings[key];
|
|
150
|
+
if (value !== undefined && data[fileKey][key] !== value) {
|
|
151
|
+
data[fileKey][key] = value;
|
|
152
|
+
updated = true;
|
|
153
|
+
}
|
|
154
|
+
}
|
|
155
|
+
if (updated) {
|
|
156
|
+
writeSessionsData(sessionsFile, data);
|
|
157
|
+
}
|
|
158
|
+
return readSettingsFromEntry(data[fileKey]);
|
|
159
|
+
}
|
|
160
|
+
catch {
|
|
161
|
+
return {};
|
|
162
|
+
}
|
|
163
|
+
}
|
|
164
|
+
function readSettingsFromEntry(entry) {
|
|
165
|
+
const provider = typeof entry["providerOverride"] === "string" ? entry["providerOverride"] : undefined;
|
|
166
|
+
const model = typeof entry["modelOverride"] === "string" ? entry["modelOverride"] : undefined;
|
|
167
|
+
const storedModelRef = typeof entry["modelRef"] === "string" ? entry["modelRef"] : undefined;
|
|
168
|
+
const modelRef = storedModelRef ?? (provider && model ? `${provider}/${model}` : undefined);
|
|
169
|
+
return {
|
|
170
|
+
reasoningLevel: typeof entry["reasoningLevel"] === "string" ? entry["reasoningLevel"] : undefined,
|
|
171
|
+
thinkingLevel: typeof entry["thinkingLevel"] === "string" ? entry["thinkingLevel"] : undefined,
|
|
172
|
+
modelRef,
|
|
173
|
+
};
|
|
174
|
+
}
|
|
175
|
+
export function getSessionSettings(sessionKey, historyDir) {
|
|
176
|
+
try {
|
|
177
|
+
const sessionsFile = resolveSessionsFilePath(historyDir);
|
|
178
|
+
const data = readSessionsData(sessionsFile);
|
|
179
|
+
if (!data)
|
|
180
|
+
return {};
|
|
181
|
+
const fileKey = toSessionStoreKey(sessionKey);
|
|
182
|
+
const entry = data[fileKey];
|
|
183
|
+
if (!entry)
|
|
184
|
+
return {};
|
|
185
|
+
return readSettingsFromEntry(entry);
|
|
186
|
+
}
|
|
187
|
+
catch {
|
|
188
|
+
return {};
|
|
189
|
+
}
|
|
190
|
+
}
|
|
@@ -0,0 +1,23 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Stable DTO for Friday SSE `lifecycle` terminal frames (`data.sessionUsage`).
|
|
3
|
+
* Populated from OpenClaw `SessionEntry` after `persistSessionUsageUpdate`.
|
|
4
|
+
*/
|
|
5
|
+
export type FridaySessionUsagePayload = {
|
|
6
|
+
modelId?: string;
|
|
7
|
+
modelProvider?: string;
|
|
8
|
+
tokens?: {
|
|
9
|
+
input?: number;
|
|
10
|
+
output?: number;
|
|
11
|
+
cacheRead?: number;
|
|
12
|
+
cacheWrite?: number;
|
|
13
|
+
total?: number;
|
|
14
|
+
totalFresh?: boolean;
|
|
15
|
+
};
|
|
16
|
+
context?: {
|
|
17
|
+
windowMax?: number;
|
|
18
|
+
used?: number;
|
|
19
|
+
};
|
|
20
|
+
estimatedCostUsd?: number;
|
|
21
|
+
};
|
|
22
|
+
/** Build a compact snapshot from a loaded session store entry (unknown shape). */
|
|
23
|
+
export declare function buildSessionUsageSnapshot(entry: Record<string, unknown>): FridaySessionUsagePayload | undefined;
|
|
@@ -0,0 +1,65 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Stable DTO for Friday SSE `lifecycle` terminal frames (`data.sessionUsage`).
|
|
3
|
+
* Populated from OpenClaw `SessionEntry` after `persistSessionUsageUpdate`.
|
|
4
|
+
*/
|
|
5
|
+
function finiteNonNeg(n) {
|
|
6
|
+
if (typeof n !== "number" || !Number.isFinite(n))
|
|
7
|
+
return undefined;
|
|
8
|
+
const t = Math.trunc(n);
|
|
9
|
+
return t >= 0 ? t : undefined;
|
|
10
|
+
}
|
|
11
|
+
function finiteCost(n) {
|
|
12
|
+
if (typeof n !== "number" || !Number.isFinite(n) || n < 0)
|
|
13
|
+
return undefined;
|
|
14
|
+
return n;
|
|
15
|
+
}
|
|
16
|
+
/** Build a compact snapshot from a loaded session store entry (unknown shape). */
|
|
17
|
+
export function buildSessionUsageSnapshot(entry) {
|
|
18
|
+
const payload = {};
|
|
19
|
+
const modelId = typeof entry.model === "string" ? entry.model.trim() : "";
|
|
20
|
+
if (modelId)
|
|
21
|
+
payload.modelId = modelId;
|
|
22
|
+
const modelProvider = typeof entry.modelProvider === "string" ? entry.modelProvider.trim() : "";
|
|
23
|
+
if (modelProvider)
|
|
24
|
+
payload.modelProvider = modelProvider;
|
|
25
|
+
const tokens = {};
|
|
26
|
+
const input = finiteNonNeg(entry.inputTokens);
|
|
27
|
+
const output = finiteNonNeg(entry.outputTokens);
|
|
28
|
+
const cacheRead = finiteNonNeg(entry.cacheRead);
|
|
29
|
+
const cacheWrite = finiteNonNeg(entry.cacheWrite);
|
|
30
|
+
const total = finiteNonNeg(entry.totalTokens);
|
|
31
|
+
if (input !== undefined)
|
|
32
|
+
tokens.input = input;
|
|
33
|
+
if (output !== undefined)
|
|
34
|
+
tokens.output = output;
|
|
35
|
+
if (cacheRead !== undefined)
|
|
36
|
+
tokens.cacheRead = cacheRead;
|
|
37
|
+
if (cacheWrite !== undefined)
|
|
38
|
+
tokens.cacheWrite = cacheWrite;
|
|
39
|
+
if (total !== undefined)
|
|
40
|
+
tokens.total = total;
|
|
41
|
+
if (entry.totalTokensFresh === true)
|
|
42
|
+
tokens.totalFresh = true;
|
|
43
|
+
if (Object.keys(tokens).length > 0)
|
|
44
|
+
payload.tokens = tokens;
|
|
45
|
+
const context = {};
|
|
46
|
+
const windowMax = finiteNonNeg(entry.contextTokens);
|
|
47
|
+
const used = finiteNonNeg(entry.totalTokens);
|
|
48
|
+
if (windowMax !== undefined)
|
|
49
|
+
context.windowMax = windowMax;
|
|
50
|
+
if (used !== undefined)
|
|
51
|
+
context.used = used;
|
|
52
|
+
if (Object.keys(context).length > 0)
|
|
53
|
+
payload.context = context;
|
|
54
|
+
const cost = finiteCost(entry.estimatedCostUsd);
|
|
55
|
+
if (cost !== undefined)
|
|
56
|
+
payload.estimatedCostUsd = cost;
|
|
57
|
+
if (!payload.modelId &&
|
|
58
|
+
!payload.modelProvider &&
|
|
59
|
+
!payload.tokens &&
|
|
60
|
+
!payload.context &&
|
|
61
|
+
payload.estimatedCostUsd === undefined) {
|
|
62
|
+
return undefined;
|
|
63
|
+
}
|
|
64
|
+
return payload;
|
|
65
|
+
}
|
|
@@ -0,0 +1,59 @@
|
|
|
1
|
+
import type { ServerResponse } from "node:http";
|
|
2
|
+
export type SseEventType = "connected" | "agent" | "deliver" | "tool-hook" | "outbound" | "ping" | "subagent";
|
|
3
|
+
export interface SseEvent {
|
|
4
|
+
type: SseEventType;
|
|
5
|
+
data: Record<string, unknown>;
|
|
6
|
+
}
|
|
7
|
+
type BacklogEntry = {
|
|
8
|
+
id: number;
|
|
9
|
+
event: SseEvent;
|
|
10
|
+
};
|
|
11
|
+
export declare class SseConnection {
|
|
12
|
+
readonly deviceId: string;
|
|
13
|
+
private readonly res;
|
|
14
|
+
private closed;
|
|
15
|
+
private pending;
|
|
16
|
+
private flushTimer;
|
|
17
|
+
private waitingDrain;
|
|
18
|
+
constructor(deviceId: string, res: ServerResponse);
|
|
19
|
+
send(entry: BacklogEntry | SseEvent, flushNow?: boolean): void;
|
|
20
|
+
sendRaw(line: string): void;
|
|
21
|
+
private scheduleFlush;
|
|
22
|
+
private flush;
|
|
23
|
+
close(): void;
|
|
24
|
+
get isClosed(): boolean;
|
|
25
|
+
}
|
|
26
|
+
declare class SseEmitterRegistry {
|
|
27
|
+
private connections;
|
|
28
|
+
private runEmitter;
|
|
29
|
+
private lastRunIdByDevice;
|
|
30
|
+
private eventSeqByDevice;
|
|
31
|
+
private backlogLimit;
|
|
32
|
+
getConnectionCount(): number;
|
|
33
|
+
setBacklogLimit(limit: number): void;
|
|
34
|
+
getBacklogLimit(): number;
|
|
35
|
+
/** Last persisted / assigned SSE id for device (for `connected.lastSeq`). */
|
|
36
|
+
latestSeqForDevice(deviceId: string): number;
|
|
37
|
+
addConnection(deviceId: string, res: ServerResponse): SseConnection;
|
|
38
|
+
/**
|
|
39
|
+
* @param expectedConn When provided, only removes if this connection is still the active one
|
|
40
|
+
* (avoids stale `req.close` after a reconnect replaced the map entry).
|
|
41
|
+
*/
|
|
42
|
+
removeConnection(deviceId: string, expectedConn?: SseConnection): void;
|
|
43
|
+
getConnection(deviceId: string): SseConnection | undefined;
|
|
44
|
+
private nextEntry;
|
|
45
|
+
replayBacklog(deviceId: string, afterEventId: number): number;
|
|
46
|
+
broadcast(event: SseEvent, deviceId?: string, flushNow?: boolean): void;
|
|
47
|
+
trackDeviceForRun(deviceId: string, runId: string): void;
|
|
48
|
+
untrackRun(runId: string): void;
|
|
49
|
+
hasTrackedDevices(runId: string): boolean;
|
|
50
|
+
getDeviceIdByRunId(runId: string): string | null;
|
|
51
|
+
getSoleConnectedDeviceId(): string | null;
|
|
52
|
+
getLastRunIdForDevice(deviceId: string): string | null;
|
|
53
|
+
broadcastToRun(runId: string, event: SseEvent, flushNow?: boolean): void;
|
|
54
|
+
broadcastToolEvent(deviceId: string, runId: string, event: SseEvent, flushNow?: boolean): void;
|
|
55
|
+
/** Vitest / e2e: drop connections and in-memory seq maps (does not delete disk queue files). */
|
|
56
|
+
resetForTest(): void;
|
|
57
|
+
}
|
|
58
|
+
export declare const sseEmitter: SseEmitterRegistry;
|
|
59
|
+
export {};
|