Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
2 changes: 1 addition & 1 deletion package.json
Original file line number Diff line number Diff line change
@@ -1,6 +1,6 @@
{
"name": "@langfuse/codex-observability-plugin",
"version": "0.1.1",
"version": "0.1.2",
"description": "OpenAI Codex plugin that traces agent turns, tool calls, and subagents to Langfuse",
"keywords": [
"codex",
Expand Down
2 changes: 1 addition & 1 deletion plugins/tracing/.codex-plugin/plugin.json
Original file line number Diff line number Diff line change
@@ -1,6 +1,6 @@
{
"name": "tracing",
"version": "0.1.1",
"version": "0.1.2",
"description": "Trace OpenAI Codex sessions to Langfuse.",
"author": {
"name": "Langfuse",
Expand Down
14 changes: 13 additions & 1 deletion plugins/tracing/hooks/hooks.json
Original file line number Diff line number Diff line change
Expand Up @@ -5,12 +5,24 @@
"hooks": [
{
"type": "command",
"command": "node \"${CODEX_HOME:-$HOME/.codex}/plugins/cache/codex-observability-plugin/tracing/0.1.1/dist/index.mjs\"",
"command": "node \"${CODEX_HOME:-$HOME/.codex}/plugins/cache/codex-observability-plugin/tracing/0.1.2/dist/index.mjs\"",
"timeout": 30,
"statusMessage": "Uploading Codex trace to Langfuse"
}
]
}
],
"SessionEnd": [
{
"hooks": [
{
"type": "command",
"command": "node \"${CODEX_HOME:-$HOME/.codex}/plugins/cache/codex-observability-plugin/tracing/0.1.2/dist/index.mjs\"",
"timeout": 30,
"statusMessage": "Uploading final Codex turn to Langfuse"
}
]
}
]
}
}
58 changes: 53 additions & 5 deletions plugins/tracing/test/hook-command.test.ts
Original file line number Diff line number Diff line change
Expand Up @@ -19,11 +19,17 @@ function makeTempDir(prefix: string): string {
return dir;
}

function readHookCommand(): string {
const config = JSON.parse(fs.readFileSync(hookConfigFile, "utf-8")) as {
hooks: { Stop: Array<{ hooks: Array<{ command: string }> }> };
};
return config.hooks.Stop[0].hooks[0].command;
type HookConfig = {
hooks: Record<string, Array<{ hooks: Array<{ command: string }> }> | undefined>;
};

function readHookCommand(event: "Stop" | "SessionEnd" = "Stop"): string {
const config = JSON.parse(fs.readFileSync(hookConfigFile, "utf-8")) as HookConfig;
const group = config.hooks[event];
if (!group?.[0]?.hooks?.[0]?.command) {
throw new Error(`hooks.json has no ${event} hook command`);
}
return group[0].hooks[0].command;
}

function readPluginVersion(): string {
Expand Down Expand Up @@ -114,3 +120,45 @@ describe("bundled Stop hook command", () => {
);
});
});

describe("bundled SessionEnd hook command", () => {
// The Stop hook fires after every turn, but Codex appends `task_complete` for
// that turn *after* the hook reads the rollout, so the trailing turn is
// deferred (see `convertRollout`). Nothing re-runs after the last turn, so
// without a SessionEnd hook the final turn of every session is never
// exported — and a single-turn session produces no trace at all.
//
// Codex flushes the transcript before dispatching SessionEnd ("failed to
// flush transcript before SessionEnd hook"), so the trailing turn is complete
// by the time this hook runs. The sidecar ledger keeps it exactly-once.
it("is declared so the deferred trailing turn still gets exported", () => {
expect(() => readHookCommand("SessionEnd")).not.toThrow();
});

it("runs the same bundle as the Stop hook", () => {
expect(readHookCommand("SessionEnd")).toBe(readHookCommand("Stop"));
});

it("runs from an arbitrary session cwd", async () => {
const codexHome = makeTempDir("lf-codex-home-");
const sessionCwd = makeTempDir("lf-codex-cwd-");
stageInstalledPlugin(codexHome);

const { code, stderr, stdout } = await runShellCommand(readHookCommand("SessionEnd"), {
cwd: sessionCwd,
env: {
...process.env,
CODEX_HOME: codexHome,
HOME: codexHome,
},
input: JSON.stringify({
hook_event_name: "SessionEnd",
transcript_path: path.join(sessionCwd, "rollout.jsonl"),
}),
});

expect(code).toBe(0);
expect(stdout).toBe("");
expect(stderr).toBe("");
});
});