@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.
Files changed (43) hide show
  1. package/dist/src/agent-forward-runtime.d.ts +6 -0
  2. package/dist/src/agent-forward-runtime.js +2 -0
  3. package/dist/src/channel-actions.js +9 -1
  4. package/dist/src/channel.js +17 -4
  5. package/dist/src/friday-session.js +3 -4
  6. package/dist/src/history/normalize-message.d.ts +67 -0
  7. package/dist/src/history/normalize-message.js +224 -0
  8. package/dist/src/history/read-transcript.d.ts +22 -0
  9. package/dist/src/history/read-transcript.js +136 -0
  10. package/dist/src/http/handlers/files.d.ts +3 -2
  11. package/dist/src/http/handlers/files.js +20 -5
  12. package/dist/src/http/handlers/history-messages.d.ts +13 -0
  13. package/dist/src/http/handlers/history-messages.js +100 -0
  14. package/dist/src/http/handlers/history-sessions.d.ts +23 -0
  15. package/dist/src/http/handlers/history-sessions.js +163 -0
  16. package/dist/src/http/handlers/history-set-title.d.ts +10 -0
  17. package/dist/src/http/handlers/history-set-title.js +77 -0
  18. package/dist/src/http/handlers/messages.js +21 -6
  19. package/dist/src/http/server.js +15 -0
  20. package/dist/src/session/session-manager.d.ts +6 -0
  21. package/dist/src/session/session-manager.js +20 -8
  22. package/package.json +10 -11
  23. package/src/agent-forward-runtime.ts +10 -0
  24. package/src/channel-actions.test.ts +111 -0
  25. package/src/channel-actions.ts +10 -1
  26. package/src/channel.outbound.test.ts +137 -0
  27. package/src/channel.ts +24 -6
  28. package/src/friday-session.ts +3 -5
  29. package/src/history/normalize-message.test.ts +154 -0
  30. package/src/history/normalize-message.ts +292 -0
  31. package/src/history/read-transcript.ts +136 -0
  32. package/src/http/handlers/files.ts +21 -5
  33. package/src/http/handlers/history-messages.test.ts +144 -0
  34. package/src/http/handlers/history-messages.ts +123 -0
  35. package/src/http/handlers/history-sessions.test.ts +146 -0
  36. package/src/http/handlers/history-sessions.ts +184 -0
  37. package/src/http/handlers/history-set-title.test.ts +115 -0
  38. package/src/http/handlers/history-set-title.ts +86 -0
  39. package/src/http/handlers/messages.ts +31 -3
  40. package/src/http/server.ts +18 -0
  41. package/src/session/session-manager.test.ts +90 -0
  42. package/src/session/session-manager.ts +21 -8
  43. 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 { setSessionSettings, splitModelRef, toSessionStoreKey } from "../../session/session-manager.js";
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
- defaultModel = typeof model?.primary === "string" ? (model.primary as string) : undefined;
434
- defaultThinking =
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.
@@ -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
- if (c.startsWith(`agent:${FRIDAY_AGENT_ID}:`)) {
61
- const tail = c.slice(`agent:${FRIDAY_AGENT_ID}:`.length);
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?: string): string {
82
+ function resolveSessionsFilePath(historyDir: string | undefined, agentId: string): string {
70
83
  const base = deriveOpenClawBaseDir(historyDir);
71
- return join(base, "agents/main/sessions/sessions.json");
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 sessionsFile = resolveSessionsFilePath(historyDir);
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 sessionsFile = resolveSessionsFilePath(historyDir);
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: {