diff --git a/src/v2/legacy-client.ts b/src/v2/legacy-client.ts index 2f7a11bc..d9d36adb 100644 --- a/src/v2/legacy-client.ts +++ b/src/v2/legacy-client.ts @@ -239,8 +239,22 @@ export function toLegacyEvent(raw: any): { type: string; properties: any } { const envelope = raw?.payload ?? raw; const source = envelope?.type === "sync" && envelope.syncEvent ? envelope.syncEvent : envelope; const rawType = typeof source?.type === "string" ? source.type.replace(/\.1$/, "") : source?.type; - const type = rawType === "session.compaction.ended" ? "session.compacted" : rawType; const data = source?.data ?? {}; + // OpenCode v2 no longer emits the deprecated `session.idle` (nor an idle + // `session.status`). A finished session execution is the equivalent signal, + // so translate it back for the V1 idle handler (auto-capture and + // user-profile learning). + const isIdleStatus = rawType === "session.status" && data?.status?.type === "idle"; + const isExecutionEnd = + rawType === "session.execution.succeeded" || + rawType === "session.execution.failed" || + rawType === "session.execution.interrupted"; + const type = + rawType === "session.compaction.ended" + ? "session.compacted" + : isIdleStatus || isExecutionEnd + ? "session.idle" + : rawType; if (source && typeof source === "object" && "properties" in source) { return { type, properties: source.properties }; } diff --git a/tests/v2-legacy-client.test.ts b/tests/v2-legacy-client.test.ts index d2870cff..de3d0f49 100644 --- a/tests/v2-legacy-client.test.ts +++ b/tests/v2-legacy-client.test.ts @@ -121,6 +121,45 @@ describe("OpenCode v2 legacy client bridge", () => { }); }); + it("maps V2 idle session.status events back to session.idle", () => { + expect( + toLegacyEvent({ + type: "session.status", + data: { sessionID: "ses-1", status: { type: "idle" } }, + }) + ).toEqual({ + type: "session.idle", + properties: { sessionID: "ses-1", status: { type: "idle" } }, + }); + // Non-idle statuses and unrelated event types pass through untouched. + expect( + toLegacyEvent({ + type: "session.status", + data: { sessionID: "ses-1", status: { type: "busy" } }, + }).type + ).toBe("session.status"); + expect(toLegacyEvent({ type: "message.updated", data: { sessionID: "ses-1" } }).type).toBe( + "message.updated" + ); + }); + + it("maps finished V2 session executions to session.idle", () => { + for (const type of [ + "session.execution.succeeded", + "session.execution.failed", + "session.execution.interrupted", + ]) { + expect(toLegacyEvent({ type, data: { sessionID: "ses-1" } })).toEqual({ + type: "session.idle", + properties: { sessionID: "ses-1" }, + }); + } + // Execution lifecycle start is not a turn end and must not trigger capture. + expect( + toLegacyEvent({ type: "session.execution.started", data: { sessionID: "ses-1" } }).type + ).toBe("session.execution.started"); + }); + it("filters the global event stream by direct or session location", async () => { const ctx = createContext(); expect(