@syengup/friday-channel-next 1.0.3 → 1.0.4
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/index.js +9 -4
- package/dist/src/friday-session.js +12 -6
- package/dist/src/history/normalize-message.js +15 -0
- package/index.ts +9 -4
- package/package.json +1 -1
- package/src/friday-session.forward-agent.test.ts +21 -0
- package/src/friday-session.ts +14 -6
- package/src/history/normalize-message.test.ts +27 -0
- package/src/history/normalize-message.ts +15 -0
package/dist/index.js
CHANGED
|
@@ -16,8 +16,6 @@ import { ensureCodexReasoningSummary } from "./src/codex-reasoning-config.js";
|
|
|
16
16
|
const hookLogger = createFridayNextLogger("hook");
|
|
17
17
|
export { fridayNextChannelPlugin } from "./src/channel.js";
|
|
18
18
|
export { setFridayNextRuntime } from "./src/runtime.js";
|
|
19
|
-
/** `api.on` returns void — register tool hooks at most once per process. */
|
|
20
|
-
let fridayNextToolHooksRegistered = false;
|
|
21
19
|
let disposeAgentEventListener = null;
|
|
22
20
|
/**
|
|
23
21
|
* Track the last `api` instance on which HTTP routes were registered.
|
|
@@ -142,10 +140,17 @@ export default defineChannelPluginEntry({
|
|
|
142
140
|
total: event.usage?.total,
|
|
143
141
|
}, event.model, event.provider);
|
|
144
142
|
});
|
|
145
|
-
|
|
143
|
+
// Tool hooks (subagent_delivery_target / before_tool_call / after_tool_call) must follow the
|
|
144
|
+
// SAME re-registration discipline as the HTTP routes and onAgentEvent listener above. When the
|
|
145
|
+
// health-monitor restarts the plugin, `registerFull` receives a fresh `api` and the host
|
|
146
|
+
// dispatches hooks on THAT api; hooks left bound to the first api go silent for the rest of the
|
|
147
|
+
// process. The old one-time boolean guard bound them to whichever api arrived first — which is
|
|
148
|
+
// exactly why Codex `command_output` (the A3 after_tool_call stdout synthesis) fired on some
|
|
149
|
+
// gateway processes and not others. Re-register on every genuinely-new api; skip only a repeat
|
|
150
|
+
// call with the same api (matching the `!sameApi` gate the routes use).
|
|
151
|
+
if (sameApi) {
|
|
146
152
|
return;
|
|
147
153
|
}
|
|
148
|
-
fridayNextToolHooksRegistered = true;
|
|
149
154
|
// Make Codex (ChatGPT/OAuth) models emit reasoning summary text so the app can stream
|
|
150
155
|
// "thinking". OpenClaw never sets this; we assert it on the plugin side. Best-effort.
|
|
151
156
|
ensureCodexReasoningSummary((msg) => hookLogger.info(msg));
|
|
@@ -346,12 +346,18 @@ export function forwardAgentEventRaw(evt) {
|
|
|
346
346
|
// Codex app-server projects every tool/command call onto BOTH the standard `tool` stream
|
|
347
347
|
// (carrying args + the real result) AND a redundant `item` event (kind:"tool"/"command"),
|
|
348
348
|
// and core flags that item `suppressChannelProgress: true` ("do not surface in channel
|
|
349
|
-
// progress"). Forwarding the suppressed item
|
|
350
|
-
//
|
|
351
|
-
//
|
|
352
|
-
//
|
|
353
|
-
//
|
|
354
|
-
|
|
349
|
+
// progress"). Forwarding the suppressed *tool* item double-renders every non-exec tool in the
|
|
350
|
+
// app — the `tool`-stream row plus a second `item kind:tool` row, with the result landing only
|
|
351
|
+
// on the first. So drop ONLY suppressed `kind:"tool"` items.
|
|
352
|
+
//
|
|
353
|
+
// BUT keep suppressed `kind:"command"`/`"process"` items: the `tool` stream's exec result is
|
|
354
|
+
// just {exitCode,duration} (no stdout), and the app bootstraps its command-terminal row from
|
|
355
|
+
// the `item kind:command` event — that row is what the synthesized `command_output` end event
|
|
356
|
+
// then attaches the real stdout to. Dropping it left exec tools with a command line and no
|
|
357
|
+
// output in the trace. (Reasoning items preamble/analysis are never suppressed.)
|
|
358
|
+
if (evt.stream === "item" &&
|
|
359
|
+
evt.data.suppressChannelProgress === true &&
|
|
360
|
+
evt.data.kind === "tool") {
|
|
355
361
|
return;
|
|
356
362
|
}
|
|
357
363
|
// Register sessionKey → runId so we can resolve parentRunId
|
|
@@ -91,6 +91,21 @@ function parseContent(content) {
|
|
|
91
91
|
}
|
|
92
92
|
break;
|
|
93
93
|
}
|
|
94
|
+
case "toolResult":
|
|
95
|
+
case "tool_result": {
|
|
96
|
+
// Codex (app-server backend) persists an exec/bash tool's STDOUT in a content
|
|
97
|
+
// block whose `type` is "toolResult" (not "text") — the output lives in the
|
|
98
|
+
// block's `text`/`content`. The native/Anthropic path uses plain "text" blocks,
|
|
99
|
+
// so this case was never hit and the command output was silently dropped from
|
|
100
|
+
// history (`parsed.text` stayed ""), leaving the app's trace tool row with no
|
|
101
|
+
// result. ControlUI shows it because its projection reads these blocks.
|
|
102
|
+
const t = readString(block.text) || readString(block.content);
|
|
103
|
+
if (t) {
|
|
104
|
+
textParts.push(t);
|
|
105
|
+
out.images.push(...extractMediaMarkers(t));
|
|
106
|
+
}
|
|
107
|
+
break;
|
|
108
|
+
}
|
|
94
109
|
case "thinking": {
|
|
95
110
|
const t = readString(block.thinking);
|
|
96
111
|
if (t)
|
package/index.ts
CHANGED
|
@@ -30,8 +30,6 @@ const hookLogger = createFridayNextLogger("hook");
|
|
|
30
30
|
export { fridayNextChannelPlugin } from "./src/channel.js";
|
|
31
31
|
export { setFridayNextRuntime } from "./src/runtime.js";
|
|
32
32
|
|
|
33
|
-
/** `api.on` returns void — register tool hooks at most once per process. */
|
|
34
|
-
let fridayNextToolHooksRegistered = false;
|
|
35
33
|
let disposeAgentEventListener: (() => void) | null = null;
|
|
36
34
|
/**
|
|
37
35
|
* Track the last `api` instance on which HTTP routes were registered.
|
|
@@ -163,10 +161,17 @@ export default defineChannelPluginEntry({
|
|
|
163
161
|
);
|
|
164
162
|
});
|
|
165
163
|
|
|
166
|
-
|
|
164
|
+
// Tool hooks (subagent_delivery_target / before_tool_call / after_tool_call) must follow the
|
|
165
|
+
// SAME re-registration discipline as the HTTP routes and onAgentEvent listener above. When the
|
|
166
|
+
// health-monitor restarts the plugin, `registerFull` receives a fresh `api` and the host
|
|
167
|
+
// dispatches hooks on THAT api; hooks left bound to the first api go silent for the rest of the
|
|
168
|
+
// process. The old one-time boolean guard bound them to whichever api arrived first — which is
|
|
169
|
+
// exactly why Codex `command_output` (the A3 after_tool_call stdout synthesis) fired on some
|
|
170
|
+
// gateway processes and not others. Re-register on every genuinely-new api; skip only a repeat
|
|
171
|
+
// call with the same api (matching the `!sameApi` gate the routes use).
|
|
172
|
+
if (sameApi) {
|
|
167
173
|
return;
|
|
168
174
|
}
|
|
169
|
-
fridayNextToolHooksRegistered = true;
|
|
170
175
|
|
|
171
176
|
// Make Codex (ChatGPT/OAuth) models emit reasoning summary text so the app can stream
|
|
172
177
|
// "thinking". OpenClaw never sets this; we assert it on the plugin side. Best-effort.
|
package/package.json
CHANGED
|
@@ -195,6 +195,27 @@ describe("forwardAgentEventRaw (thinking delta rewrite)", () => {
|
|
|
195
195
|
expect(sseEmitter.broadcastToRun).not.toHaveBeenCalled();
|
|
196
196
|
});
|
|
197
197
|
|
|
198
|
+
it("KEEPS suppressed item kind:command (exec row the command_output stdout attaches to)", () => {
|
|
199
|
+
// The `tool`-stream exec result is just {exitCode,duration}; the real stdout arrives on the
|
|
200
|
+
// synthesized `command_output` end event, which the app attaches to the command-terminal row
|
|
201
|
+
// bootstrapped from THIS `item kind:command`. Dropping it (as the old broad filter did) left
|
|
202
|
+
// exec tools with a command line and no output in the trace.
|
|
203
|
+
forwardAgentEventRaw({
|
|
204
|
+
runId,
|
|
205
|
+
seq: 1,
|
|
206
|
+
stream: "item",
|
|
207
|
+
sessionKey,
|
|
208
|
+
data: {
|
|
209
|
+
itemId: "call_exec1",
|
|
210
|
+
kind: "command",
|
|
211
|
+
phase: "start",
|
|
212
|
+
name: "bash",
|
|
213
|
+
suppressChannelProgress: true,
|
|
214
|
+
},
|
|
215
|
+
});
|
|
216
|
+
expect(sseEmitter.broadcastToRun).toHaveBeenCalledTimes(1);
|
|
217
|
+
});
|
|
218
|
+
|
|
198
219
|
it("forwards item events that are not suppressed (e.g. reasoning analysis markers)", () => {
|
|
199
220
|
forwardAgentEventRaw({
|
|
200
221
|
runId,
|
package/src/friday-session.ts
CHANGED
|
@@ -401,12 +401,20 @@ export function forwardAgentEventRaw(evt: ForwardAgentEventArgs): void {
|
|
|
401
401
|
// Codex app-server projects every tool/command call onto BOTH the standard `tool` stream
|
|
402
402
|
// (carrying args + the real result) AND a redundant `item` event (kind:"tool"/"command"),
|
|
403
403
|
// and core flags that item `suppressChannelProgress: true` ("do not surface in channel
|
|
404
|
-
// progress"). Forwarding the suppressed item
|
|
405
|
-
//
|
|
406
|
-
//
|
|
407
|
-
//
|
|
408
|
-
//
|
|
409
|
-
|
|
404
|
+
// progress"). Forwarding the suppressed *tool* item double-renders every non-exec tool in the
|
|
405
|
+
// app — the `tool`-stream row plus a second `item kind:tool` row, with the result landing only
|
|
406
|
+
// on the first. So drop ONLY suppressed `kind:"tool"` items.
|
|
407
|
+
//
|
|
408
|
+
// BUT keep suppressed `kind:"command"`/`"process"` items: the `tool` stream's exec result is
|
|
409
|
+
// just {exitCode,duration} (no stdout), and the app bootstraps its command-terminal row from
|
|
410
|
+
// the `item kind:command` event — that row is what the synthesized `command_output` end event
|
|
411
|
+
// then attaches the real stdout to. Dropping it left exec tools with a command line and no
|
|
412
|
+
// output in the trace. (Reasoning items preamble/analysis are never suppressed.)
|
|
413
|
+
if (
|
|
414
|
+
evt.stream === "item" &&
|
|
415
|
+
evt.data.suppressChannelProgress === true &&
|
|
416
|
+
evt.data.kind === "tool"
|
|
417
|
+
) {
|
|
410
418
|
return;
|
|
411
419
|
}
|
|
412
420
|
|
|
@@ -185,6 +185,33 @@ describe("normalizeHistoryMessage", () => {
|
|
|
185
185
|
expect(plain?.mediaPaths).toBeUndefined();
|
|
186
186
|
});
|
|
187
187
|
|
|
188
|
+
it("extracts Codex exec stdout from a `toolResult`-typed content block", () => {
|
|
189
|
+
// Codex (app-server) persists bash/exec stdout in a content block whose `type` is
|
|
190
|
+
// "toolResult" (not "text"); the native path only ever emits "text" blocks. Before the
|
|
191
|
+
// fix this fell through `default` and the command output was dropped (text="").
|
|
192
|
+
const out = normalizeHistoryMessage(
|
|
193
|
+
{
|
|
194
|
+
role: "toolResult",
|
|
195
|
+
toolCallId: "call_GSu3",
|
|
196
|
+
toolName: "bash",
|
|
197
|
+
content: [
|
|
198
|
+
{
|
|
199
|
+
type: "toolResult",
|
|
200
|
+
toolCallId: "call_GSu3",
|
|
201
|
+
name: "bash",
|
|
202
|
+
content: "Applications\nDesktop\nDocuments",
|
|
203
|
+
text: "Applications\nDesktop\nDocuments",
|
|
204
|
+
},
|
|
205
|
+
],
|
|
206
|
+
...meta("tr-1", 3),
|
|
207
|
+
},
|
|
208
|
+
0,
|
|
209
|
+
);
|
|
210
|
+
expect(out?.role).toBe("toolResult");
|
|
211
|
+
expect(out?.toolResult?.text).toBe("Applications\nDesktop\nDocuments");
|
|
212
|
+
expect(out?.toolResult?.toolCallId).toBe("call_GSu3");
|
|
213
|
+
});
|
|
214
|
+
|
|
188
215
|
it("flags compaction records via __openclaw.kind", () => {
|
|
189
216
|
const out = normalizeHistoryMessage(
|
|
190
217
|
{
|
|
@@ -158,6 +158,21 @@ function parseContent(content: unknown): ParsedContent {
|
|
|
158
158
|
}
|
|
159
159
|
break;
|
|
160
160
|
}
|
|
161
|
+
case "toolResult":
|
|
162
|
+
case "tool_result": {
|
|
163
|
+
// Codex (app-server backend) persists an exec/bash tool's STDOUT in a content
|
|
164
|
+
// block whose `type` is "toolResult" (not "text") — the output lives in the
|
|
165
|
+
// block's `text`/`content`. The native/Anthropic path uses plain "text" blocks,
|
|
166
|
+
// so this case was never hit and the command output was silently dropped from
|
|
167
|
+
// history (`parsed.text` stayed ""), leaving the app's trace tool row with no
|
|
168
|
+
// result. ControlUI shows it because its projection reads these blocks.
|
|
169
|
+
const t = readString(block.text) || readString(block.content);
|
|
170
|
+
if (t) {
|
|
171
|
+
textParts.push(t);
|
|
172
|
+
out.images.push(...extractMediaMarkers(t));
|
|
173
|
+
}
|
|
174
|
+
break;
|
|
175
|
+
}
|
|
161
176
|
case "thinking": {
|
|
162
177
|
const t = readString(block.thinking);
|
|
163
178
|
if (t) thinkingParts.push(t);
|