@syengup/friday-channel-next 1.0.0 → 1.0.3
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/friday-session.js +33 -0
- package/dist/src/http/handlers/messages.js +10 -0
- package/package.json +1 -1
- package/src/friday-session.forward-agent.test.ts +81 -0
- package/src/friday-session.ts +38 -2
- package/src/http/handlers/messages.ts +10 -0
- package/dist/src/http 2/middleware/auth.d.ts +0 -13
- package/dist/src/http 2/middleware/auth.js +0 -29
- package/dist/src/http 2/middleware/body.d.ts +0 -2
- package/dist/src/http 2/middleware/body.js +0 -24
- package/dist/src/http 2/middleware/cors.d.ts +0 -2
- package/dist/src/http 2/middleware/cors.js +0 -11
|
@@ -321,6 +321,39 @@ export function forwardAgentEventRaw(evt) {
|
|
|
321
321
|
if (typeof evt.stream === "string" && evt.stream.startsWith("codex_app_server")) {
|
|
322
322
|
codexRunIds.add(evt.runId);
|
|
323
323
|
}
|
|
324
|
+
// Codex app-server reasoning: newer OpenClaw cores stopped invoking the dispatch
|
|
325
|
+
// `onReasoningStream` callback (the A2 path in messages.ts) and instead stream the
|
|
326
|
+
// reasoning summary on the agent-event bus as `stream:"item" kind:"preamble"` with a
|
|
327
|
+
// cumulative `progressText` (source "codex-app-server"). The Friday app only renders
|
|
328
|
+
// `stream:"thinking"`, so translate it here — synthesize a thinking event reusing the
|
|
329
|
+
// cumulative→delta rewrite below. The raw preamble item is still forwarded but the app
|
|
330
|
+
// ignores unknown item kinds. (The onReasoningStream callback stays as a harmless
|
|
331
|
+
// fallback for cores that still fire it.)
|
|
332
|
+
if (evt.stream === "item" &&
|
|
333
|
+
evt.data.kind === "preamble" &&
|
|
334
|
+
evt.data.source === "codex-app-server") {
|
|
335
|
+
codexRunIds.add(evt.runId);
|
|
336
|
+
const reasoningText = typeof evt.data.progressText === "string" ? evt.data.progressText : "";
|
|
337
|
+
if (reasoningText) {
|
|
338
|
+
forwardAgentEventRaw({
|
|
339
|
+
runId: evt.runId,
|
|
340
|
+
stream: "thinking",
|
|
341
|
+
data: { text: reasoningText },
|
|
342
|
+
sessionKey: evt.sessionKey ?? sk,
|
|
343
|
+
});
|
|
344
|
+
}
|
|
345
|
+
}
|
|
346
|
+
// Codex app-server projects every tool/command call onto BOTH the standard `tool` stream
|
|
347
|
+
// (carrying args + the real result) AND a redundant `item` event (kind:"tool"/"command"),
|
|
348
|
+
// and core flags that item `suppressChannelProgress: true` ("do not surface in channel
|
|
349
|
+
// progress"). Forwarding the suppressed item anyway double-renders every non-exec tool in
|
|
350
|
+
// the app — the `tool`-stream row plus a second `item kind:tool` row, with the result landing
|
|
351
|
+
// only on the first. Honor the flag and drop suppressed items; the `tool` stream (and, for
|
|
352
|
+
// exec, the synthesized `command_output`) already carries everything the app renders. Codex
|
|
353
|
+
// reasoning items (preamble/analysis) are NOT suppressed, so this never touches thinking.
|
|
354
|
+
if (evt.stream === "item" && evt.data.suppressChannelProgress === true) {
|
|
355
|
+
return;
|
|
356
|
+
}
|
|
324
357
|
// Register sessionKey → runId so we can resolve parentRunId
|
|
325
358
|
if (sk && evt.stream === "lifecycle" && evt.data.phase === "start") {
|
|
326
359
|
registerSessionKeyForRun(sk, evt.runId);
|
|
@@ -467,6 +467,16 @@ export async function handleMessages(req, res) {
|
|
|
467
467
|
runId,
|
|
468
468
|
suppressTyping: true,
|
|
469
469
|
disableBlockStreaming: true,
|
|
470
|
+
// friday-next is a direct device channel: the final assistant reply auto-delivers
|
|
471
|
+
// to the app live over SSE (sendText), and the channel already declares ChatType
|
|
472
|
+
// "direct" + outbound.deliveryMode "direct". But OpenClaw core's source-reply policy
|
|
473
|
+
// (resolveSourceReplyDeliveryMode) can still resolve `message_tool_only` for this
|
|
474
|
+
// channel from its own defaults — when it does, the agent prompt tells the model
|
|
475
|
+
// "visible replies are NOT auto-delivered; use message(action=send) for everything",
|
|
476
|
+
// which makes Codex route its whole answer through the `message` tool (and that tool
|
|
477
|
+
// crashes on friday-next). Pin `automatic` so core honors the channel's own direct
|
|
478
|
+
// delivery and never instructs the model to deliver via the message tool.
|
|
479
|
+
sourceReplyDeliveryMode: "automatic",
|
|
470
480
|
// A1: feed the chosen thinking level into the run as a one-shot override so the model
|
|
471
481
|
// request asks for a reasoning summary. The session-stored `thinkingLevel` alone is NOT
|
|
472
482
|
// honored by the reply dispatch; `thinkingLevelOverride` has top priority in OpenClaw's
|
package/package.json
CHANGED
|
@@ -139,6 +139,87 @@ describe("forwardAgentEventRaw (thinking delta rewrite)", () => {
|
|
|
139
139
|
expect(third.delta).toBe(t1);
|
|
140
140
|
});
|
|
141
141
|
|
|
142
|
+
it("translates a Codex preamble item (progressText) into streamed thinking deltas", () => {
|
|
143
|
+
// Newer OpenClaw cores stopped invoking dispatch onReasoningStream for Codex and instead
|
|
144
|
+
// stream the reasoning summary on the agent-event bus as item/preamble with cumulative
|
|
145
|
+
// progressText. We translate it back into stream:"thinking" so the app renders it.
|
|
146
|
+
forwardAgentEventRaw({
|
|
147
|
+
runId,
|
|
148
|
+
seq: 1,
|
|
149
|
+
stream: "item",
|
|
150
|
+
sessionKey,
|
|
151
|
+
data: { kind: "preamble", source: "codex-app-server", phase: "update", progressText: "先把" },
|
|
152
|
+
});
|
|
153
|
+
forwardAgentEventRaw({
|
|
154
|
+
runId,
|
|
155
|
+
seq: 2,
|
|
156
|
+
stream: "item",
|
|
157
|
+
sessionKey,
|
|
158
|
+
data: {
|
|
159
|
+
kind: "preamble",
|
|
160
|
+
source: "codex-app-server",
|
|
161
|
+
phase: "update",
|
|
162
|
+
progressText: "先把标准",
|
|
163
|
+
},
|
|
164
|
+
});
|
|
165
|
+
|
|
166
|
+
const thinking = (sseEmitter.broadcastToRun as ReturnType<typeof vi.fn>).mock.calls
|
|
167
|
+
.map((c) => c[1].data)
|
|
168
|
+
.filter((d: { stream?: string }) => d.stream === "thinking");
|
|
169
|
+
expect(thinking).toHaveLength(2);
|
|
170
|
+
expect(thinking[0].data.text).toBe("先把");
|
|
171
|
+
expect(thinking[0].data.delta).toBe("先把");
|
|
172
|
+
expect(thinking[0].data.reasoningPrefixChars).toBe(0);
|
|
173
|
+
expect(thinking[1].data.text).toBe("先把标准");
|
|
174
|
+
expect(thinking[1].data.delta).toBe("标准");
|
|
175
|
+
expect(thinking[1].data.reasoningPrefixChars).toBe(2);
|
|
176
|
+
});
|
|
177
|
+
|
|
178
|
+
it("drops item events flagged suppressChannelProgress (Codex tool/command duplicates)", () => {
|
|
179
|
+
// Codex app-server projects every tool/command onto both the `tool` stream and a redundant
|
|
180
|
+
// `item` event flagged suppressChannelProgress:true. Forwarding the item double-renders the
|
|
181
|
+
// tool in the app. We honor the flag and drop it.
|
|
182
|
+
forwardAgentEventRaw({
|
|
183
|
+
runId,
|
|
184
|
+
seq: 1,
|
|
185
|
+
stream: "item",
|
|
186
|
+
sessionKey,
|
|
187
|
+
data: {
|
|
188
|
+
itemId: "call_abc",
|
|
189
|
+
kind: "tool",
|
|
190
|
+
phase: "start",
|
|
191
|
+
name: "web_search",
|
|
192
|
+
suppressChannelProgress: true,
|
|
193
|
+
},
|
|
194
|
+
});
|
|
195
|
+
expect(sseEmitter.broadcastToRun).not.toHaveBeenCalled();
|
|
196
|
+
});
|
|
197
|
+
|
|
198
|
+
it("forwards item events that are not suppressed (e.g. reasoning analysis markers)", () => {
|
|
199
|
+
forwardAgentEventRaw({
|
|
200
|
+
runId,
|
|
201
|
+
seq: 1,
|
|
202
|
+
stream: "item",
|
|
203
|
+
sessionKey,
|
|
204
|
+
data: { itemId: "rs_1", kind: "analysis", phase: "start", title: "Reasoning" },
|
|
205
|
+
});
|
|
206
|
+
expect(sseEmitter.broadcastToRun).toHaveBeenCalledTimes(1);
|
|
207
|
+
});
|
|
208
|
+
|
|
209
|
+
it("does not translate preamble items from a non-Codex source", () => {
|
|
210
|
+
forwardAgentEventRaw({
|
|
211
|
+
runId,
|
|
212
|
+
seq: 1,
|
|
213
|
+
stream: "item",
|
|
214
|
+
sessionKey,
|
|
215
|
+
data: { kind: "preamble", source: "something-else", progressText: "x" },
|
|
216
|
+
});
|
|
217
|
+
const thinking = (sseEmitter.broadcastToRun as ReturnType<typeof vi.fn>).mock.calls
|
|
218
|
+
.map((c) => c[1].data)
|
|
219
|
+
.filter((d: { stream?: string }) => d.stream === "thinking");
|
|
220
|
+
expect(thinking).toHaveLength(0);
|
|
221
|
+
});
|
|
222
|
+
|
|
142
223
|
it("merges run metadata into lifecycle.end (model, tokens, context usage)", () => {
|
|
143
224
|
forwardAgentEventRaw({
|
|
144
225
|
runId,
|
package/src/friday-session.ts
CHANGED
|
@@ -373,6 +373,43 @@ export function forwardAgentEventRaw(evt: ForwardAgentEventArgs): void {
|
|
|
373
373
|
codexRunIds.add(evt.runId);
|
|
374
374
|
}
|
|
375
375
|
|
|
376
|
+
// Codex app-server reasoning: newer OpenClaw cores stopped invoking the dispatch
|
|
377
|
+
// `onReasoningStream` callback (the A2 path in messages.ts) and instead stream the
|
|
378
|
+
// reasoning summary on the agent-event bus as `stream:"item" kind:"preamble"` with a
|
|
379
|
+
// cumulative `progressText` (source "codex-app-server"). The Friday app only renders
|
|
380
|
+
// `stream:"thinking"`, so translate it here — synthesize a thinking event reusing the
|
|
381
|
+
// cumulative→delta rewrite below. The raw preamble item is still forwarded but the app
|
|
382
|
+
// ignores unknown item kinds. (The onReasoningStream callback stays as a harmless
|
|
383
|
+
// fallback for cores that still fire it.)
|
|
384
|
+
if (
|
|
385
|
+
evt.stream === "item" &&
|
|
386
|
+
evt.data.kind === "preamble" &&
|
|
387
|
+
evt.data.source === "codex-app-server"
|
|
388
|
+
) {
|
|
389
|
+
codexRunIds.add(evt.runId);
|
|
390
|
+
const reasoningText = typeof evt.data.progressText === "string" ? evt.data.progressText : "";
|
|
391
|
+
if (reasoningText) {
|
|
392
|
+
forwardAgentEventRaw({
|
|
393
|
+
runId: evt.runId,
|
|
394
|
+
stream: "thinking",
|
|
395
|
+
data: { text: reasoningText },
|
|
396
|
+
sessionKey: evt.sessionKey ?? sk,
|
|
397
|
+
});
|
|
398
|
+
}
|
|
399
|
+
}
|
|
400
|
+
|
|
401
|
+
// Codex app-server projects every tool/command call onto BOTH the standard `tool` stream
|
|
402
|
+
// (carrying args + the real result) AND a redundant `item` event (kind:"tool"/"command"),
|
|
403
|
+
// and core flags that item `suppressChannelProgress: true` ("do not surface in channel
|
|
404
|
+
// progress"). Forwarding the suppressed item anyway double-renders every non-exec tool in
|
|
405
|
+
// the app — the `tool`-stream row plus a second `item kind:tool` row, with the result landing
|
|
406
|
+
// only on the first. Honor the flag and drop suppressed items; the `tool` stream (and, for
|
|
407
|
+
// exec, the synthesized `command_output`) already carries everything the app renders. Codex
|
|
408
|
+
// reasoning items (preamble/analysis) are NOT suppressed, so this never touches thinking.
|
|
409
|
+
if (evt.stream === "item" && evt.data.suppressChannelProgress === true) {
|
|
410
|
+
return;
|
|
411
|
+
}
|
|
412
|
+
|
|
376
413
|
// Register sessionKey → runId so we can resolve parentRunId
|
|
377
414
|
if (sk && evt.stream === "lifecycle" && evt.data.phase === "start") {
|
|
378
415
|
registerSessionKeyForRun(sk, evt.runId);
|
|
@@ -469,8 +506,7 @@ export function forwardAgentEventRaw(evt: ForwardAgentEventArgs): void {
|
|
|
469
506
|
if (evt.stream === "lifecycle" && evt.data.phase === "start") {
|
|
470
507
|
const announced = parseAnnounceRunId(evt.runId);
|
|
471
508
|
if (announced) {
|
|
472
|
-
const entry =
|
|
473
|
-
lookupByChildSessionKey(announced.childSessionKey) ?? lookupByRunId(evt.runId);
|
|
509
|
+
const entry = lookupByChildSessionKey(announced.childSessionKey) ?? lookupByRunId(evt.runId);
|
|
474
510
|
sseEmitter.broadcast(
|
|
475
511
|
{
|
|
476
512
|
type: "subagent",
|
|
@@ -635,6 +635,16 @@ export async function handleMessages(req: IncomingMessage, res: ServerResponse):
|
|
|
635
635
|
runId,
|
|
636
636
|
suppressTyping: true,
|
|
637
637
|
disableBlockStreaming: true,
|
|
638
|
+
// friday-next is a direct device channel: the final assistant reply auto-delivers
|
|
639
|
+
// to the app live over SSE (sendText), and the channel already declares ChatType
|
|
640
|
+
// "direct" + outbound.deliveryMode "direct". But OpenClaw core's source-reply policy
|
|
641
|
+
// (resolveSourceReplyDeliveryMode) can still resolve `message_tool_only` for this
|
|
642
|
+
// channel from its own defaults — when it does, the agent prompt tells the model
|
|
643
|
+
// "visible replies are NOT auto-delivered; use message(action=send) for everything",
|
|
644
|
+
// which makes Codex route its whole answer through the `message` tool (and that tool
|
|
645
|
+
// crashes on friday-next). Pin `automatic` so core honors the channel's own direct
|
|
646
|
+
// delivery and never instructs the model to deliver via the message tool.
|
|
647
|
+
sourceReplyDeliveryMode: "automatic",
|
|
638
648
|
// A1: feed the chosen thinking level into the run as a one-shot override so the model
|
|
639
649
|
// request asks for a reasoning summary. The session-stored `thinkingLevel` alone is NOT
|
|
640
650
|
// honored by the reply dispatch; `thinkingLevelOverride` has top priority in OpenClaw's
|
|
@@ -1,13 +0,0 @@
|
|
|
1
|
-
/**
|
|
2
|
-
* Bearer token authentication middleware for Friday HTTP routes.
|
|
3
|
-
*
|
|
4
|
-
* Validates that the bearer token matches the gateway's configured auth token.
|
|
5
|
-
* This ensures plugin HTTP endpoints use the same token as gateway WS connections.
|
|
6
|
-
*/
|
|
7
|
-
import type { IncomingMessage } from "node:http";
|
|
8
|
-
/**
|
|
9
|
-
* Extract and validate bearer token from Authorization header.
|
|
10
|
-
* Returns the token only if it matches the gateway's configured auth token.
|
|
11
|
-
* Returns null if token is missing, malformed, or doesn't match.
|
|
12
|
-
*/
|
|
13
|
-
export declare function extractBearerToken(req: IncomingMessage): string | null;
|
|
@@ -1,29 +0,0 @@
|
|
|
1
|
-
/**
|
|
2
|
-
* Bearer token authentication middleware for Friday HTTP routes.
|
|
3
|
-
*
|
|
4
|
-
* Validates that the bearer token matches the gateway's configured auth token.
|
|
5
|
-
* This ensures plugin HTTP endpoints use the same token as gateway WS connections.
|
|
6
|
-
*/
|
|
7
|
-
import { resolveFridayNextConfig } from "../../config.js";
|
|
8
|
-
import { getHostOpenClawConfigSnapshot } from "../../host-config.js";
|
|
9
|
-
import { getFridayNextRuntime } from "../../runtime.js";
|
|
10
|
-
/**
|
|
11
|
-
* Extract and validate bearer token from Authorization header.
|
|
12
|
-
* Returns the token only if it matches the gateway's configured auth token.
|
|
13
|
-
* Returns null if token is missing, malformed, or doesn't match.
|
|
14
|
-
*/
|
|
15
|
-
export function extractBearerToken(req) {
|
|
16
|
-
const auth = req.headers.authorization;
|
|
17
|
-
if (!auth || typeof auth !== "string")
|
|
18
|
-
return null;
|
|
19
|
-
const parts = auth.trim().split(/\s+/);
|
|
20
|
-
if (parts.length !== 2 || parts[0].toLowerCase() !== "bearer")
|
|
21
|
-
return null;
|
|
22
|
-
const token = parts[1];
|
|
23
|
-
// Validate token matches the gateway's configured auth token.
|
|
24
|
-
const cfg = getHostOpenClawConfigSnapshot(getFridayNextRuntime().config);
|
|
25
|
-
const runtimeConfig = resolveFridayNextConfig(cfg);
|
|
26
|
-
if (!runtimeConfig.authToken || token !== runtimeConfig.authToken)
|
|
27
|
-
return null;
|
|
28
|
-
return token;
|
|
29
|
-
}
|
|
@@ -1,24 +0,0 @@
|
|
|
1
|
-
export async function readJsonBody(req, maxBytes = 2 * 1024 * 1024) {
|
|
2
|
-
return await new Promise((resolve) => {
|
|
3
|
-
const chunks = [];
|
|
4
|
-
let total = 0;
|
|
5
|
-
req.on("data", (chunk) => {
|
|
6
|
-
total += chunk.length;
|
|
7
|
-
if (total > maxBytes) {
|
|
8
|
-
resolve(null);
|
|
9
|
-
req.destroy();
|
|
10
|
-
return;
|
|
11
|
-
}
|
|
12
|
-
chunks.push(chunk);
|
|
13
|
-
});
|
|
14
|
-
req.on("end", () => {
|
|
15
|
-
try {
|
|
16
|
-
resolve(JSON.parse(Buffer.concat(chunks).toString("utf-8")));
|
|
17
|
-
}
|
|
18
|
-
catch {
|
|
19
|
-
resolve(null);
|
|
20
|
-
}
|
|
21
|
-
});
|
|
22
|
-
req.on("error", () => resolve(null));
|
|
23
|
-
});
|
|
24
|
-
}
|
|
@@ -1,11 +0,0 @@
|
|
|
1
|
-
import { resolveFridayNextConfig } from "../../config.js";
|
|
2
|
-
import { getHostOpenClawConfigSnapshot } from "../../host-config.js";
|
|
3
|
-
import { getFridayNextRuntime } from "../../runtime.js";
|
|
4
|
-
export function applyCorsHeaders(res) {
|
|
5
|
-
const cfg = resolveFridayNextConfig(getHostOpenClawConfigSnapshot(getFridayNextRuntime().config));
|
|
6
|
-
if (!cfg.corsEnabled)
|
|
7
|
-
return;
|
|
8
|
-
res.setHeader("Access-Control-Allow-Origin", cfg.corsAllowOrigin || "*");
|
|
9
|
-
res.setHeader("Access-Control-Allow-Headers", "Authorization, Content-Type, Last-Event-ID");
|
|
10
|
-
res.setHeader("Access-Control-Allow-Methods", "GET,POST,OPTIONS");
|
|
11
|
-
}
|