Skip to content
Open
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
5 changes: 4 additions & 1 deletion src/v1.ts
Original file line number Diff line number Diff line change
Expand Up @@ -64,8 +64,11 @@ const eventHook = (event: OpencodeEvent) =>
};

if (event.type === "session.idle") {
yield* log("info", "Flushing spans");
// Finalize before awaiting anything: the log call is an HTTP round trip
// to the server, and a prompt sent as this turn ends can start the next
// turn in that window, which finalizing afterwards would then end.
finalizeSessionTracing(event.properties.sessionID);
yield* log("info", "Flushing spans");

yield* langfuse.forceFlush;
}
Expand Down
81 changes: 80 additions & 1 deletion test/integration/v1.test.ts
Original file line number Diff line number Diff line change
Expand Up @@ -158,6 +158,9 @@ let collectorBaseUrl: string;
let collectorPort: number | undefined;
let toolListCalls = 0;
let toolListShouldFail = false;
// Held open by a test to keep the next app.log call pending, the way a slow
// round trip to the OpenCode server does.
let pendingAppLog: Promise<void> | undefined;

const startedAt = 1_750_000_000_000;

Expand Down Expand Up @@ -473,7 +476,11 @@ const createHooks = async (
process.env.LANGFUSE_BASE_URL = baseUrl;
const client = Schema.decodeUnknownSync(PluginClientSchema)({
app: {
log: () => Promise.resolve(),
log: () => {
const pending = pendingAppLog ?? Promise.resolve();
pendingAppLog = undefined;
return pending;
},
},
session: {
messages: ({ path }: { path: { id: string } }) =>
Expand Down Expand Up @@ -543,6 +550,7 @@ beforeEach(async () => {
hooksDisposed = false;
toolListCalls = 0;
toolListShouldFail = false;
pendingAppLog = undefined;
sessionMessagesShouldFail = false;
server = createServer((request, response) => {
const chunks: Buffer[] = [];
Expand Down Expand Up @@ -3269,6 +3277,77 @@ describe("built plugin", { concurrent: false }, () => {
]);
});

test("keeps a turn that starts while the previous idle is still logging", async () => {
const sessionID = "idle-race-session";
const started = startedAt;

await sendUserMessage({
sessionID,
messageID: "idle-race-user-1",
text: "Read the README",
started,
});
await startGeneration({
id: "idle-race-step-1",
sessionID,
started: started + 100,
});
await completeGeneration({
sessionID,
userMessageID: "idle-race-user-1",
assistantMessageID: "idle-race-assistant-1",
started: started + 100,
completed: started + 500,
text: "The README describes the project",
});

const idleLog = Promise.withResolvers<undefined>();
pendingAppLog = idleLog.promise;
const requestCount = requests.length;
const firstIdle = emitEvent({
type: "session.idle",
properties: { sessionID },
});

await sendUserMessage({
sessionID,
messageID: "idle-race-user-2",
text: "Now summarize it",
started: started + 501,
});
idleLog.resolve(undefined);
await firstIdle;

await startGeneration({
id: "idle-race-step-2",
sessionID,
started: started + 600,
});
await completeGeneration({
sessionID,
userMessageID: "idle-race-user-2",
assistantMessageID: "idle-race-assistant-2",
started: started + 600,
completed: started + 900,
text: "A summary",
});
await emitEvent({ type: "session.idle", properties: { sessionID } });

const generations = requests
.slice(requestCount)
.flatMap(getSpans)
.filter((span) => span.name === "opencode.generation");

expect(
generations.map(
(span) =>
Schema.decodeUnknownSync(Schema.Struct({ messageID: Schema.String }))(
getJsonAttribute(span, "langfuse.observation.metadata"),
).messageID,
),
).toContain("idle-race-assistant-2");
});

test("keeps the history complete when the plugin restarts mid-session", async () => {
// The conversation lives in OpenCode, not in the plugin, so a restart
// must not truncate what a generation input shows. A plugin that
Expand Down
Loading