@syengup/friday-channel-next 0.1.12 → 0.1.14
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/src/agent-forward-runtime.d.ts +6 -0
- package/dist/src/agent-forward-runtime.js +2 -0
- package/dist/src/channel-actions.js +9 -1
- package/dist/src/channel.js +17 -4
- package/dist/src/friday-session.js +3 -4
- package/dist/src/history/normalize-message.d.ts +67 -0
- package/dist/src/history/normalize-message.js +224 -0
- package/dist/src/history/read-transcript.d.ts +22 -0
- package/dist/src/history/read-transcript.js +136 -0
- package/dist/src/http/handlers/files.d.ts +3 -2
- package/dist/src/http/handlers/files.js +20 -5
- package/dist/src/http/handlers/history-messages.d.ts +13 -0
- package/dist/src/http/handlers/history-messages.js +100 -0
- package/dist/src/http/handlers/history-sessions.d.ts +23 -0
- package/dist/src/http/handlers/history-sessions.js +163 -0
- package/dist/src/http/handlers/history-set-title.d.ts +10 -0
- package/dist/src/http/handlers/history-set-title.js +77 -0
- package/dist/src/http/handlers/messages.js +21 -6
- package/dist/src/http/server.js +15 -0
- package/dist/src/session/session-manager.d.ts +6 -0
- package/dist/src/session/session-manager.js +20 -8
- package/package.json +10 -11
- package/src/agent-forward-runtime.ts +10 -0
- package/src/channel-actions.test.ts +111 -0
- package/src/channel-actions.ts +10 -1
- package/src/channel.outbound.test.ts +137 -0
- package/src/channel.ts +24 -6
- package/src/friday-session.ts +3 -5
- package/src/history/normalize-message.test.ts +154 -0
- package/src/history/normalize-message.ts +292 -0
- package/src/history/read-transcript.ts +136 -0
- package/src/http/handlers/files.ts +21 -5
- package/src/http/handlers/history-messages.test.ts +144 -0
- package/src/http/handlers/history-messages.ts +123 -0
- package/src/http/handlers/history-sessions.test.ts +146 -0
- package/src/http/handlers/history-sessions.ts +184 -0
- package/src/http/handlers/history-set-title.test.ts +115 -0
- package/src/http/handlers/history-set-title.ts +86 -0
- package/src/http/handlers/messages.ts +31 -3
- package/src/http/server.ts +18 -0
- package/src/session/session-manager.test.ts +90 -0
- package/src/session/session-manager.ts +21 -8
- package/src/test-support/mock-runtime.ts +2 -0
|
@@ -28,7 +28,12 @@ import { resolveFridayNextConfig } from "../../config.js";
|
|
|
28
28
|
import { getHostOpenClawConfigSnapshot } from "../../host-config.js";
|
|
29
29
|
import { getFridayNextRuntime } from "../../runtime.js";
|
|
30
30
|
import { getFridayAgentForwardRuntime } from "../../agent-forward-runtime.js";
|
|
31
|
-
import {
|
|
31
|
+
import {
|
|
32
|
+
agentIdFromSessionKey,
|
|
33
|
+
setSessionSettings,
|
|
34
|
+
splitModelRef,
|
|
35
|
+
toSessionStoreKey,
|
|
36
|
+
} from "../../session/session-manager.js";
|
|
32
37
|
import { sseEmitter } from "../../sse/emitter.js";
|
|
33
38
|
import { extractBearerToken } from "../middleware/auth.js";
|
|
34
39
|
import { readJsonBody } from "../middleware/body.js";
|
|
@@ -421,6 +426,10 @@ export async function handleMessages(req: IncomingMessage, res: ServerResponse):
|
|
|
421
426
|
const cfg = resolveFridayNextConfig(getHostOpenClawConfigSnapshot(runtime.config));
|
|
422
427
|
|
|
423
428
|
// Resolve defaults from the OpenClaw agent config so settings are never left empty.
|
|
429
|
+
// The target agent comes from the app-supplied sessionKey (`agent:<id>:<rest>`); prefer that
|
|
430
|
+
// agent's own configured model/thinking over the global defaults so non-main agents are not
|
|
431
|
+
// silently forced onto the global default model.
|
|
432
|
+
const targetAgentId = agentIdFromSessionKey(baseSessionKey);
|
|
424
433
|
let defaultModel: string | undefined;
|
|
425
434
|
let defaultThinking: string | undefined;
|
|
426
435
|
try {
|
|
@@ -428,13 +437,32 @@ export async function handleMessages(req: IncomingMessage, res: ServerResponse):
|
|
|
428
437
|
if (forwardRt) {
|
|
429
438
|
const ocCfg = (forwardRt.getConfig() ?? {}) as Record<string, unknown>;
|
|
430
439
|
const agents = ocCfg.agents as Record<string, unknown> | undefined;
|
|
440
|
+
|
|
441
|
+
const agentEntry = (agents?.list as Array<Record<string, unknown>> | undefined)?.find(
|
|
442
|
+
(a) => agentIdFromSessionKey(`agent:${String(a?.id ?? "")}:x`) === targetAgentId,
|
|
443
|
+
);
|
|
444
|
+
const agentModel = agentEntry?.model;
|
|
445
|
+
const perAgentModel =
|
|
446
|
+
typeof agentModel === "string"
|
|
447
|
+
? agentModel
|
|
448
|
+
: typeof (agentModel as Record<string, unknown> | undefined)?.primary === "string"
|
|
449
|
+
? ((agentModel as Record<string, unknown>).primary as string)
|
|
450
|
+
: undefined;
|
|
451
|
+
const perAgentThinking =
|
|
452
|
+
typeof agentEntry?.thinkingDefault === "string"
|
|
453
|
+
? (agentEntry.thinkingDefault as string)
|
|
454
|
+
: undefined;
|
|
455
|
+
|
|
431
456
|
const agentDefaults = agents?.defaults as Record<string, unknown> | undefined;
|
|
432
457
|
const model = agentDefaults?.model as Record<string, unknown> | undefined;
|
|
433
|
-
|
|
434
|
-
|
|
458
|
+
const globalModel = typeof model?.primary === "string" ? (model.primary as string) : undefined;
|
|
459
|
+
const globalThinking =
|
|
435
460
|
typeof agentDefaults?.thinkingDefault === "string"
|
|
436
461
|
? (agentDefaults.thinkingDefault as string)
|
|
437
462
|
: undefined;
|
|
463
|
+
|
|
464
|
+
defaultModel = perAgentModel ?? globalModel;
|
|
465
|
+
defaultThinking = perAgentThinking ?? globalThinking;
|
|
438
466
|
}
|
|
439
467
|
} catch {
|
|
440
468
|
// Config not available (tests) — leave defaults undefined.
|
package/src/http/server.ts
CHANGED
|
@@ -16,6 +16,9 @@ import { handleNodesApprove } from "./handlers/nodes-approve.js";
|
|
|
16
16
|
import { handleSessionsSettings } from "./handlers/sessions-settings.js";
|
|
17
17
|
import { handleModelsList } from "./handlers/models-list.js";
|
|
18
18
|
import { handleAgentsList } from "./handlers/agents-list.js";
|
|
19
|
+
import { handleHistorySessions } from "./handlers/history-sessions.js";
|
|
20
|
+
import { handleHistoryMessages } from "./handlers/history-messages.js";
|
|
21
|
+
import { handleHistorySetTitle } from "./handlers/history-set-title.js";
|
|
19
22
|
import { handleStatus } from "./handlers/status.js";
|
|
20
23
|
import { handleHealth } from "./handlers/health.js";
|
|
21
24
|
import { applyCorsHeaders } from "./middleware/cors.js";
|
|
@@ -86,6 +89,21 @@ async function handleFridayNextRoute(
|
|
|
86
89
|
return await handleStatus(req, res);
|
|
87
90
|
}
|
|
88
91
|
|
|
92
|
+
// Route: GET /friday-next/history/sessions (list all sessions across agents)
|
|
93
|
+
if (req.method === "GET" && pathname === "/friday-next/history/sessions") {
|
|
94
|
+
return await handleHistorySessions(req, res);
|
|
95
|
+
}
|
|
96
|
+
|
|
97
|
+
// Route: GET /friday-next/history/messages?sessionKey=&agentId=&limit=
|
|
98
|
+
if (req.method === "GET" && pathname === "/friday-next/history/messages") {
|
|
99
|
+
return await handleHistoryMessages(req, res);
|
|
100
|
+
}
|
|
101
|
+
|
|
102
|
+
// Route: PUT /friday-next/sessions/title (sync app session name → server displayName)
|
|
103
|
+
if ((req.method === "PUT" || req.method === "POST") && pathname === "/friday-next/sessions/title") {
|
|
104
|
+
return await handleHistorySetTitle(req, res);
|
|
105
|
+
}
|
|
106
|
+
|
|
89
107
|
// Route: GET /friday-next/health?deviceId=...&nodeDeviceId=...&selfHeal=true
|
|
90
108
|
if (req.method === "GET" && pathname === "/friday-next/health") {
|
|
91
109
|
return await handleHealth(req, res);
|
|
@@ -0,0 +1,90 @@
|
|
|
1
|
+
import { describe, it, expect, beforeEach, afterEach } from "vitest";
|
|
2
|
+
import { mkdtempSync, mkdirSync, writeFileSync, readFileSync, rmSync } from "node:fs";
|
|
3
|
+
import { join } from "node:path";
|
|
4
|
+
import { tmpdir } from "node:os";
|
|
5
|
+
import {
|
|
6
|
+
agentIdFromSessionKey,
|
|
7
|
+
toSessionStoreKey,
|
|
8
|
+
setSessionSettings,
|
|
9
|
+
getSessionSettings,
|
|
10
|
+
} from "./session-manager.js";
|
|
11
|
+
|
|
12
|
+
describe("agentIdFromSessionKey", () => {
|
|
13
|
+
it("extracts the agent id from a fully-qualified key", () => {
|
|
14
|
+
expect(agentIdFromSessionKey("agent:operator:friday:direct:abc:1")).toBe("operator");
|
|
15
|
+
expect(agentIdFromSessionKey("agent:ha-maestro:main")).toBe("ha-maestro");
|
|
16
|
+
});
|
|
17
|
+
|
|
18
|
+
it("falls back to main for bare / legacy keys", () => {
|
|
19
|
+
expect(agentIdFromSessionKey("main")).toBe("main");
|
|
20
|
+
expect(agentIdFromSessionKey("friday:direct:dev:1")).toBe("main");
|
|
21
|
+
expect(agentIdFromSessionKey("")).toBe("main");
|
|
22
|
+
});
|
|
23
|
+
|
|
24
|
+
it("rejects path-unsafe agent ids (no traversal)", () => {
|
|
25
|
+
expect(agentIdFromSessionKey("agent:../../etc:foo")).toBe("main");
|
|
26
|
+
});
|
|
27
|
+
});
|
|
28
|
+
|
|
29
|
+
describe("per-agent session settings file routing", () => {
|
|
30
|
+
let baseDir: string;
|
|
31
|
+
let historyDir: string;
|
|
32
|
+
|
|
33
|
+
// historyDir must contain a `.openclaw` segment so deriveOpenClawBaseDir resolves the base.
|
|
34
|
+
function seedSessionsFile(agentId: string): string {
|
|
35
|
+
const dir = join(baseDir, ".openclaw", "agents", agentId, "sessions");
|
|
36
|
+
mkdirSync(dir, { recursive: true });
|
|
37
|
+
const file = join(dir, "sessions.json");
|
|
38
|
+
writeFileSync(file, JSON.stringify({}), "utf-8");
|
|
39
|
+
return file;
|
|
40
|
+
}
|
|
41
|
+
|
|
42
|
+
function readEntry(agentId: string, fileKey: string): Record<string, unknown> | undefined {
|
|
43
|
+
const file = join(baseDir, ".openclaw", "agents", agentId, "sessions", "sessions.json");
|
|
44
|
+
const data = JSON.parse(readFileSync(file, "utf-8")) as Record<string, Record<string, unknown>>;
|
|
45
|
+
return data[fileKey];
|
|
46
|
+
}
|
|
47
|
+
|
|
48
|
+
beforeEach(() => {
|
|
49
|
+
baseDir = mkdtempSync(join(tmpdir(), "friday-sm-"));
|
|
50
|
+
historyDir = join(baseDir, ".openclaw", "friday-next", "history");
|
|
51
|
+
});
|
|
52
|
+
|
|
53
|
+
afterEach(() => {
|
|
54
|
+
rmSync(baseDir, { recursive: true, force: true });
|
|
55
|
+
});
|
|
56
|
+
|
|
57
|
+
it("writes settings for a non-main agent into agents/<agentId>/sessions", () => {
|
|
58
|
+
seedSessionsFile("operator");
|
|
59
|
+
const sessionKey = "agent:operator:friday:direct:dev:1";
|
|
60
|
+
|
|
61
|
+
setSessionSettings(sessionKey, { reasoningLevel: "stream", thinkingLevel: "high" }, historyDir);
|
|
62
|
+
|
|
63
|
+
const entry = readEntry("operator", toSessionStoreKey(sessionKey));
|
|
64
|
+
expect(entry?.reasoningLevel).toBe("stream");
|
|
65
|
+
expect(entry?.thinkingLevel).toBe("high");
|
|
66
|
+
|
|
67
|
+
// Round-trips through getSessionSettings from the same per-agent file.
|
|
68
|
+
const read = getSessionSettings(sessionKey, historyDir);
|
|
69
|
+
expect(read.reasoningLevel).toBe("stream");
|
|
70
|
+
expect(read.thinkingLevel).toBe("high");
|
|
71
|
+
});
|
|
72
|
+
|
|
73
|
+
it("does not leak a non-main agent's settings into the main store", () => {
|
|
74
|
+
seedSessionsFile("operator");
|
|
75
|
+
seedSessionsFile("main");
|
|
76
|
+
|
|
77
|
+
setSessionSettings("agent:operator:s1", { modelRef: "openai/gpt-x" }, historyDir);
|
|
78
|
+
|
|
79
|
+
expect(readEntry("operator", "agent:operator:s1")?.modelRef).toBe("openai/gpt-x");
|
|
80
|
+
expect(getSessionSettings("main", historyDir).modelRef).toBeUndefined();
|
|
81
|
+
});
|
|
82
|
+
|
|
83
|
+
it("still routes bare/main keys to agents/main", () => {
|
|
84
|
+
seedSessionsFile("main");
|
|
85
|
+
|
|
86
|
+
setSessionSettings("main", { thinkingLevel: "low" }, historyDir);
|
|
87
|
+
|
|
88
|
+
expect(readEntry("main", "agent:main:main")?.thinkingLevel).toBe("low");
|
|
89
|
+
});
|
|
90
|
+
});
|
|
@@ -4,6 +4,8 @@ import { readFileSync, writeFileSync } from "node:fs";
|
|
|
4
4
|
|
|
5
5
|
const FRIDAY_AGENT_ID = "main";
|
|
6
6
|
const SESSION_ID_RE = /^[a-z0-9][a-z0-9._-]{0,127}$/i;
|
|
7
|
+
/** Path/shell-safe agent id (mirrors OpenClaw's `normalizeAgentId`). Anything else falls back to `main`. */
|
|
8
|
+
const SAFE_AGENT_ID_RE = /^[a-z0-9][a-z0-9_-]*$/;
|
|
7
9
|
|
|
8
10
|
function deriveOpenClawBaseDir(historyDir?: string): string {
|
|
9
11
|
if (historyDir) {
|
|
@@ -41,6 +43,17 @@ export function toSessionStoreKey(rawSessionKey: string): string {
|
|
|
41
43
|
return `agent:${FRIDAY_AGENT_ID}:${lowered}`;
|
|
42
44
|
}
|
|
43
45
|
|
|
46
|
+
/**
|
|
47
|
+
* Extract the agent id from a (possibly raw) session key. The downstream app now owns the
|
|
48
|
+
* full `agent:<id>:<rest>` key, so non-`main` agents must read/write their own session store
|
|
49
|
+
* directory. `agent:<id>:<rest>` → `<id>`; bare/legacy keys (or an unsafe id) → `main`.
|
|
50
|
+
*/
|
|
51
|
+
export function agentIdFromSessionKey(rawSessionKey: string): string {
|
|
52
|
+
const canonical = toSessionStoreKey(rawSessionKey);
|
|
53
|
+
const id = canonical.match(/^agent:([^:]+):/)?.[1];
|
|
54
|
+
return id && SAFE_AGENT_ID_RE.test(id) ? id : FRIDAY_AGENT_ID;
|
|
55
|
+
}
|
|
56
|
+
|
|
44
57
|
function toSafeSessionId(raw: string): string {
|
|
45
58
|
const s = raw.trim();
|
|
46
59
|
if (SESSION_ID_RE.test(s)) return s;
|
|
@@ -57,8 +70,8 @@ function sessionIdForSessionsFile(fileKey: string, rawSessionKey: string): strin
|
|
|
57
70
|
const candidates = [rawSessionKey.trim(), fileKey.trim()];
|
|
58
71
|
for (const c of candidates) {
|
|
59
72
|
if (SESSION_ID_RE.test(c)) return c;
|
|
60
|
-
|
|
61
|
-
|
|
73
|
+
const tail = c.match(/^agent:[^:]+:(.+)$/)?.[1];
|
|
74
|
+
if (tail) {
|
|
62
75
|
if (SESSION_ID_RE.test(tail)) return tail;
|
|
63
76
|
return toSafeSessionId(tail);
|
|
64
77
|
}
|
|
@@ -66,9 +79,9 @@ function sessionIdForSessionsFile(fileKey: string, rawSessionKey: string): strin
|
|
|
66
79
|
return toSafeSessionId(rawSessionKey || fileKey);
|
|
67
80
|
}
|
|
68
81
|
|
|
69
|
-
function resolveSessionsFilePath(historyDir
|
|
82
|
+
function resolveSessionsFilePath(historyDir: string | undefined, agentId: string): string {
|
|
70
83
|
const base = deriveOpenClawBaseDir(historyDir);
|
|
71
|
-
return join(base, "agents
|
|
84
|
+
return join(base, "agents", agentId, "sessions", "sessions.json");
|
|
72
85
|
}
|
|
73
86
|
|
|
74
87
|
function readSessionsData(path: string): Record<string, Record<string, unknown>> | null {
|
|
@@ -125,11 +138,11 @@ export function setSessionSettings(
|
|
|
125
138
|
historyDir?: string,
|
|
126
139
|
): FridaySessionSettings {
|
|
127
140
|
try {
|
|
128
|
-
const
|
|
141
|
+
const fileKey = toSessionStoreKey(sessionKey);
|
|
142
|
+
const sessionsFile = resolveSessionsFilePath(historyDir, agentIdFromSessionKey(fileKey));
|
|
129
143
|
const data = readSessionsData(sessionsFile);
|
|
130
144
|
if (!data) return {};
|
|
131
145
|
|
|
132
|
-
const fileKey = toSessionStoreKey(sessionKey);
|
|
133
146
|
upsertSessionEntry(data, fileKey, sessionKey);
|
|
134
147
|
|
|
135
148
|
const fieldKeys: (keyof FridaySessionSettings)[] = [
|
|
@@ -172,11 +185,11 @@ export function getSessionSettings(
|
|
|
172
185
|
historyDir?: string,
|
|
173
186
|
): FridaySessionSettings {
|
|
174
187
|
try {
|
|
175
|
-
const
|
|
188
|
+
const fileKey = toSessionStoreKey(sessionKey);
|
|
189
|
+
const sessionsFile = resolveSessionsFilePath(historyDir, agentIdFromSessionKey(fileKey));
|
|
176
190
|
const data = readSessionsData(sessionsFile);
|
|
177
191
|
if (!data) return {};
|
|
178
192
|
|
|
179
|
-
const fileKey = toSessionStoreKey(sessionKey);
|
|
180
193
|
const entry = data[fileKey];
|
|
181
194
|
if (!entry) return {};
|
|
182
195
|
return readSettingsFromEntry(entry);
|
|
@@ -3,6 +3,7 @@ import os from "node:os";
|
|
|
3
3
|
import path from "node:path";
|
|
4
4
|
import { setFridayNextRuntime } from "../runtime.js";
|
|
5
5
|
import { setOfflineQueueBaseDirForTest } from "../sse/offline-queue.js";
|
|
6
|
+
import { setAttachmentsDirForTest } from "../http/handlers/files.js";
|
|
6
7
|
import { sseEmitter } from "../sse/emitter.js";
|
|
7
8
|
import { resetActiveRunsForTest } from "../agent/active-runs.js";
|
|
8
9
|
import { resetRunMetadataForTest } from "../run-metadata.js";
|
|
@@ -36,6 +37,7 @@ export function setMockRuntime(opts: MockRuntimeOptions = {}): void {
|
|
|
36
37
|
resetSubagentRegistryForTest();
|
|
37
38
|
const historyDir = opts.historyDir ?? createTempHistoryDir();
|
|
38
39
|
setOfflineQueueBaseDirForTest(path.join(historyDir, "events-queue"));
|
|
40
|
+
setAttachmentsDirForTest(path.join(historyDir, "attachments"));
|
|
39
41
|
const cfg = {
|
|
40
42
|
gateway: {
|
|
41
43
|
auth: {
|