@syengup/friday-channel-next 0.1.13 → 0.1.15

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 (44) 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 +26 -4
  5. package/dist/src/history/normalize-message.d.ts +67 -0
  6. package/dist/src/history/normalize-message.js +224 -0
  7. package/dist/src/history/read-transcript.d.ts +22 -0
  8. package/dist/src/history/read-transcript.js +136 -0
  9. package/dist/src/http/handlers/files.d.ts +3 -2
  10. package/dist/src/http/handlers/files.js +20 -5
  11. package/dist/src/http/handlers/history-messages.d.ts +13 -0
  12. package/dist/src/http/handlers/history-messages.js +100 -0
  13. package/dist/src/http/handlers/history-sessions.d.ts +23 -0
  14. package/dist/src/http/handlers/history-sessions.js +163 -0
  15. package/dist/src/http/handlers/history-set-title.d.ts +10 -0
  16. package/dist/src/http/handlers/history-set-title.js +77 -0
  17. package/dist/src/http/handlers/messages.js +6 -38
  18. package/dist/src/http/handlers/sessions-settings.js +22 -7
  19. package/dist/src/http/server.js +15 -0
  20. package/dist/src/session/session-manager.d.ts +30 -1
  21. package/dist/src/session/session-manager.js +50 -1
  22. package/package.json +2 -2
  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-mirror-suppression.test.ts +36 -0
  27. package/src/channel.outbound.test.ts +137 -0
  28. package/src/channel.ts +33 -6
  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 +8 -46
  40. package/src/http/handlers/sessions-settings.ts +23 -6
  41. package/src/http/server.ts +18 -0
  42. package/src/session/session-manager.test.ts +42 -0
  43. package/src/session/session-manager.ts +73 -3
  44. package/src/test-support/mock-runtime.ts +2 -0
@@ -27,12 +27,12 @@ export type FridayReplyPayload = {
27
27
  import { resolveFridayNextConfig } from "../../config.js";
28
28
  import { getHostOpenClawConfigSnapshot } from "../../host-config.js";
29
29
  import { getFridayNextRuntime } from "../../runtime.js";
30
- import { getFridayAgentForwardRuntime } from "../../agent-forward-runtime.js";
31
30
  import {
32
- agentIdFromSessionKey,
31
+ resolveAgentDefaults,
33
32
  setSessionSettings,
34
33
  splitModelRef,
35
34
  toSessionStoreKey,
35
+ type FridaySessionSettingsUpdate,
36
36
  } from "../../session/session-manager.js";
37
37
  import { sseEmitter } from "../../sse/emitter.js";
38
38
  import { extractBearerToken } from "../middleware/auth.js";
@@ -425,58 +425,20 @@ export async function handleMessages(req: IncomingMessage, res: ServerResponse):
425
425
 
426
426
  const cfg = resolveFridayNextConfig(getHostOpenClawConfigSnapshot(runtime.config));
427
427
 
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);
433
- let defaultModel: string | undefined;
434
- let defaultThinking: string | undefined;
435
- try {
436
- const forwardRt = getFridayAgentForwardRuntime();
437
- if (forwardRt) {
438
- const ocCfg = (forwardRt.getConfig() ?? {}) as Record<string, unknown>;
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
-
456
- const agentDefaults = agents?.defaults as Record<string, unknown> | undefined;
457
- const model = agentDefaults?.model as Record<string, unknown> | undefined;
458
- const globalModel = typeof model?.primary === "string" ? (model.primary as string) : undefined;
459
- const globalThinking =
460
- typeof agentDefaults?.thinkingDefault === "string"
461
- ? (agentDefaults.thinkingDefault as string)
462
- : undefined;
463
-
464
- defaultModel = perAgentModel ?? globalModel;
465
- defaultThinking = perAgentThinking ?? globalThinking;
466
- }
467
- } catch {
468
- // Config not available (tests) — leave defaults undefined.
469
- }
428
+ // Resolve defaults from the OpenClaw agent config so settings are never left empty. Prefers the
429
+ // target agent's own model/thinking over the global defaults (see resolveAgentDefaults).
430
+ const { model: defaultModel, thinking: defaultThinking } = resolveAgentDefaults(baseSessionKey);
470
431
 
471
432
  const modelRef = payload.modelRef ?? defaultModel;
472
433
  const reasoningLevel = payload.reasoningLevel ?? "stream";
473
434
  const thinkingLevel = payload.thinkingLevel ?? defaultThinking;
474
435
 
475
- const settings: Record<string, string | undefined> = {};
436
+ const settings: FridaySessionSettingsUpdate = {};
476
437
  if (modelRef) {
477
438
  settings.modelRef = modelRef;
478
439
  const split = splitModelRef(modelRef);
479
- settings.providerOverride = split.provider;
440
+ // `?? null` clears a stale provider when the resolved ref is bare (no `provider/` prefix).
441
+ settings.providerOverride = split.provider ?? null;
480
442
  settings.modelOverride = split.modelId;
481
443
  }
482
444
  if (reasoningLevel) settings.reasoningLevel = reasoningLevel;
@@ -3,6 +3,8 @@ import {
3
3
  setSessionSettings,
4
4
  getSessionSettings,
5
5
  splitModelRef,
6
+ resolveAgentDefaults,
7
+ type FridaySessionSettingsUpdate,
6
8
  } from "../../session/session-manager.js";
7
9
  import { readJsonBody } from "../middleware/body.js";
8
10
  import { extractBearerToken } from "../middleware/auth.js";
@@ -57,7 +59,7 @@ export async function handleSessionsSettings(
57
59
 
58
60
  const reasoningLevel = typeof body?.reasoningLevel === "string" ? body.reasoningLevel : undefined;
59
61
  const thinkingLevel = typeof body?.thinkingLevel === "string" ? body.thinkingLevel : undefined;
60
- const modelRef = typeof body?.modelRef === "string" ? body.modelRef : undefined;
62
+ const modelRef = typeof body?.modelRef === "string" ? body.modelRef.trim() : undefined;
61
63
 
62
64
  const errors: string[] = [];
63
65
  if (reasoningLevel !== undefined && !VALID_REASONING.has(reasoningLevel)) {
@@ -74,11 +76,26 @@ export async function handleSessionsSettings(
74
76
  return true;
75
77
  }
76
78
 
77
- const settings: Record<string, string | undefined> = { reasoningLevel, thinkingLevel, modelRef };
78
- if (modelRef) {
79
- const split = splitModelRef(modelRef);
80
- settings["providerOverride"] = split.provider;
81
- settings["modelOverride"] = split.modelId;
79
+ // The app omits (or empties) modelRef to mean "use the agent's default model". Resolve that
80
+ // default and write it as an *explicit* override, identical in shape to any other selection — so
81
+ // the agent runs the default exactly the way it runs an explicitly-picked model. Do NOT just
82
+ // clear the override here: the session entry is shared with the OpenClaw core, which stamps it
83
+ // with provenance fields (`modelOverrideSource`, `model`, `modelProvider`); deleting only our
84
+ // three fields leaves those dangling and the core mis-resolves to a fallback model.
85
+ const effectiveModelRef = modelRef || resolveAgentDefaults(sessionKey).model;
86
+
87
+ const settings: FridaySessionSettingsUpdate = { reasoningLevel, thinkingLevel };
88
+ if (effectiveModelRef) {
89
+ const split = splitModelRef(effectiveModelRef);
90
+ settings.modelRef = effectiveModelRef;
91
+ // `?? null` clears a stale provider when the ref is bare (no `provider/` prefix).
92
+ settings.providerOverride = split.provider ?? null;
93
+ settings.modelOverride = split.modelId;
94
+ } else {
95
+ // No configured default to resolve (e.g. config unavailable) — clear rather than pin a stale model.
96
+ settings.modelRef = null;
97
+ settings.providerOverride = null;
98
+ settings.modelOverride = null;
82
99
  }
83
100
 
84
101
  const result = setSessionSettings(sessionKey, settings);
@@ -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);
@@ -87,4 +87,46 @@ describe("per-agent session settings file routing", () => {
87
87
 
88
88
  expect(readEntry("main", "agent:main:main")?.thinkingLevel).toBe("low");
89
89
  });
90
+
91
+ it("clears a stored model override when the trio is set to null (default re-selected)", () => {
92
+ seedSessionsFile("main");
93
+
94
+ // Prior non-default selection.
95
+ setSessionSettings(
96
+ "main",
97
+ { modelRef: "openai/gpt-x", providerOverride: "openai", modelOverride: "gpt-x" },
98
+ historyDir,
99
+ );
100
+ expect(getSessionSettings("main", historyDir).modelRef).toBe("openai/gpt-x");
101
+
102
+ // Switching back to the agent default sends nulls → override is removed, not merged.
103
+ setSessionSettings(
104
+ "main",
105
+ { modelRef: null, providerOverride: null, modelOverride: null },
106
+ historyDir,
107
+ );
108
+
109
+ const entry = readEntry("main", "agent:main:main")!;
110
+ expect(entry.modelRef).toBeUndefined();
111
+ expect(entry.providerOverride).toBeUndefined();
112
+ expect(entry.modelOverride).toBeUndefined();
113
+ expect(getSessionSettings("main", historyDir).modelRef).toBeUndefined();
114
+ });
115
+
116
+ it("leaves the stored model override untouched when fields are undefined", () => {
117
+ seedSessionsFile("main");
118
+
119
+ setSessionSettings(
120
+ "main",
121
+ { modelRef: "openai/gpt-x", providerOverride: "openai", modelOverride: "gpt-x" },
122
+ historyDir,
123
+ );
124
+ // A thinking-only update (model fields undefined) must not disturb the override.
125
+ setSessionSettings("main", { thinkingLevel: "high" }, historyDir);
126
+
127
+ const entry = readEntry("main", "agent:main:main")!;
128
+ expect(entry.thinkingLevel).toBe("high");
129
+ expect(entry.modelRef).toBe("openai/gpt-x");
130
+ expect(entry.providerOverride).toBe("openai");
131
+ });
90
132
  });
@@ -1,6 +1,7 @@
1
1
  import { join } from "node:path";
2
2
  import os from "node:os";
3
3
  import { readFileSync, writeFileSync } from "node:fs";
4
+ import { getFridayAgentForwardRuntime } from "../agent-forward-runtime.js";
4
5
 
5
6
  const FRIDAY_AGENT_ID = "main";
6
7
  const SESSION_ID_RE = /^[a-z0-9][a-z0-9._-]{0,127}$/i;
@@ -132,9 +133,24 @@ export interface FridaySessionSettings {
132
133
  modelOverride?: string;
133
134
  }
134
135
 
136
+ /**
137
+ * Update shape for {@link setSessionSettings}. A field set to a string writes that value, a field
138
+ * left `undefined` is untouched, and a field set to `null` **clears** the stored value. The `null`
139
+ * case is what lets the app reset a model override back to the agent default — without it the merge
140
+ * could only ever add/replace overrides, never remove them (the cause of "selecting the default
141
+ * model doesn't take effect": a prior `provider/model` override survived and was read back).
142
+ */
143
+ export type FridaySessionSettingsUpdate = {
144
+ reasoningLevel?: string | null;
145
+ thinkingLevel?: string | null;
146
+ modelRef?: string | null;
147
+ providerOverride?: string | null;
148
+ modelOverride?: string | null;
149
+ };
150
+
135
151
  export function setSessionSettings(
136
152
  sessionKey: string,
137
- settings: FridaySessionSettings,
153
+ settings: FridaySessionSettingsUpdate,
138
154
  historyDir?: string,
139
155
  ): FridaySessionSettings {
140
156
  try {
@@ -145,13 +161,22 @@ export function setSessionSettings(
145
161
 
146
162
  upsertSessionEntry(data, fileKey, sessionKey);
147
163
 
148
- const fieldKeys: (keyof FridaySessionSettings)[] = [
164
+ const fieldKeys: (keyof FridaySessionSettingsUpdate)[] = [
149
165
  "reasoningLevel", "thinkingLevel", "modelRef", "providerOverride", "modelOverride",
150
166
  ];
151
167
  let updated = false;
152
168
  for (const key of fieldKeys) {
153
169
  const value = settings[key];
154
- if (value !== undefined && data[fileKey][key] !== value) {
170
+ if (value === undefined) continue; // leave the stored value untouched
171
+ if (value === null) {
172
+ // Explicit clear — remove the override so the agent falls back to its default.
173
+ if (key in data[fileKey]) {
174
+ delete data[fileKey][key];
175
+ updated = true;
176
+ }
177
+ continue;
178
+ }
179
+ if (data[fileKey][key] !== value) {
155
180
  data[fileKey][key] = value;
156
181
  updated = true;
157
182
  }
@@ -197,3 +222,48 @@ export function getSessionSettings(
197
222
  return {};
198
223
  }
199
224
  }
225
+
226
+ /**
227
+ * Resolve the configured default model + thinking level for the agent that owns `sessionKey`,
228
+ * reading the live OpenClaw config. Prefers the target agent's own `model`/`thinkingDefault` over
229
+ * the global `agents.defaults`, so non-main agents aren't silently forced onto the global default.
230
+ *
231
+ * Used to write the default model as an **explicit** override when the app selects it (the app
232
+ * sends no modelRef for the default). Writing it explicitly — rather than clearing the stored
233
+ * override — keeps the shared session entry consistent with the core's provenance fields
234
+ * (`modelOverrideSource`, `model`, `modelProvider`); a bare clear leaves those dangling and the
235
+ * agent mis-resolves to a fallback model.
236
+ */
237
+ export function resolveAgentDefaults(sessionKey: string): { model?: string; thinking?: string } {
238
+ try {
239
+ const forwardRt = getFridayAgentForwardRuntime();
240
+ if (!forwardRt) return {};
241
+ const ocCfg = (forwardRt.getConfig() ?? {}) as Record<string, unknown>;
242
+ const agents = ocCfg.agents as Record<string, unknown> | undefined;
243
+ const targetAgentId = agentIdFromSessionKey(sessionKey);
244
+
245
+ const agentEntry = (agents?.list as Array<Record<string, unknown>> | undefined)?.find(
246
+ (a) => agentIdFromSessionKey(`agent:${String(a?.id ?? "")}:x`) === targetAgentId,
247
+ );
248
+ const agentModel = agentEntry?.model;
249
+ const perAgentModel =
250
+ typeof agentModel === "string"
251
+ ? agentModel
252
+ : typeof (agentModel as Record<string, unknown> | undefined)?.primary === "string"
253
+ ? ((agentModel as Record<string, unknown>).primary as string)
254
+ : undefined;
255
+ const perAgentThinking =
256
+ typeof agentEntry?.thinkingDefault === "string" ? (agentEntry.thinkingDefault as string) : undefined;
257
+
258
+ const agentDefaults = agents?.defaults as Record<string, unknown> | undefined;
259
+ const model = agentDefaults?.model as Record<string, unknown> | undefined;
260
+ const globalModel = typeof model?.primary === "string" ? (model.primary as string) : undefined;
261
+ const globalThinking =
262
+ typeof agentDefaults?.thinkingDefault === "string" ? (agentDefaults.thinkingDefault as string) : undefined;
263
+
264
+ return { model: perAgentModel ?? globalModel, thinking: perAgentThinking ?? globalThinking };
265
+ } catch {
266
+ // Config not available (e.g. unit tests) — caller decides the fallback.
267
+ return {};
268
+ }
269
+ }
@@ -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: {