@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.
- 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 +26 -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 +6 -38
- package/dist/src/http/handlers/sessions-settings.js +22 -7
- package/dist/src/http/server.js +15 -0
- package/dist/src/session/session-manager.d.ts +30 -1
- package/dist/src/session/session-manager.js +50 -1
- package/package.json +2 -2
- 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-mirror-suppression.test.ts +36 -0
- package/src/channel.outbound.test.ts +137 -0
- package/src/channel.ts +33 -6
- 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 +8 -46
- package/src/http/handlers/sessions-settings.ts +23 -6
- package/src/http/server.ts +18 -0
- package/src/session/session-manager.test.ts +42 -0
- package/src/session/session-manager.ts +73 -3
- 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
|
-
|
|
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
|
-
//
|
|
430
|
-
|
|
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:
|
|
436
|
+
const settings: FridaySessionSettingsUpdate = {};
|
|
476
437
|
if (modelRef) {
|
|
477
438
|
settings.modelRef = modelRef;
|
|
478
439
|
const split = splitModelRef(modelRef);
|
|
479
|
-
|
|
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
|
-
|
|
78
|
-
|
|
79
|
-
|
|
80
|
-
|
|
81
|
-
|
|
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);
|
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);
|
|
@@ -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:
|
|
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
|
|
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
|
|
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: {
|