@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.
@@ -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
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@syengup/friday-channel-next",
3
- "version": "1.0.0",
3
+ "version": "1.0.3",
4
4
  "description": "OpenClaw Friday Next Apple channel plugin",
5
5
  "license": "MIT",
6
6
  "type": "module",
@@ -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,
@@ -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,2 +0,0 @@
1
- import type { IncomingMessage } from "node:http";
2
- export declare function readJsonBody(req: IncomingMessage, maxBytes?: number): Promise<Record<string, unknown> | null>;
@@ -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,2 +0,0 @@
1
- import type { ServerResponse } from "node:http";
2
- export declare function applyCorsHeaders(res: ServerResponse): void;
@@ -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
- }