From bee18acc98373dbaa42864e93701ebb763bcdadc Mon Sep 17 00:00:00 2001 From: Kazuki Nakashima <65545348+lynnswap@users.noreply.github.com> Date: Sat, 4 Jul 2026 13:38:55 +0900 Subject: [PATCH] fix(appserver): consume finalized review responses --- Package.resolved | 4 +- Package.swift | 6 +- .../AppServerClientTests.swift | 58 +++++++++++++++++++ .../xcshareddata/swiftpm/Package.resolved | 4 +- 4 files changed, 65 insertions(+), 7 deletions(-) diff --git a/Package.resolved b/Package.resolved index 02368d2..8dfb149 100644 --- a/Package.resolved +++ b/Package.resolved @@ -1,12 +1,12 @@ { - "originHash" : "643ee9f1e86f0f8cc956f0b9542148281e53145a190653855e03c6e8f2d29c5b", + "originHash" : "711aa87f833d42050080f043e999477df164429e02013f117e52aea172c59432", "pins" : [ { "identity" : "codexkit", "kind" : "remoteSourceControl", "location" : "https://github.com/lynnswap/CodexKit.git", "state" : { - "revision" : "6de9e019d35095390252102bd30b069e1090c874" + "revision" : "23276c81cf160b773b3f9fb5ae710d257838cad0" } }, { diff --git a/Package.swift b/Package.swift index 1d27640..350b2f5 100644 --- a/Package.swift +++ b/Package.swift @@ -33,9 +33,9 @@ let package = Package( .package(url: "https://github.com/modelcontextprotocol/swift-sdk.git", exact: "0.12.1"), .package(url: "https://github.com/apple/swift-nio.git", from: "2.97.1"), .package(url: "https://github.com/lynnswap/ObservationBridge.git", .upToNextMinor(from: "0.12.0")), - // CodexKit has no tagged releases yet; pin the reviewed main revision - // (lynnswap/CodexKit#16 merge). - .package(url: "https://github.com/lynnswap/CodexKit.git", revision: "6de9e019d35095390252102bd30b069e1090c874"), + // CodexKit has no tagged releases yet; pin the final review response + // contract fix (lynnswap/CodexKit#17). + .package(url: "https://github.com/lynnswap/CodexKit.git", revision: "23276c81cf160b773b3f9fb5ae710d257838cad0"), ], targets: [ .target( diff --git a/Tests/CodexReviewAppServerTests/AppServerClientTests.swift b/Tests/CodexReviewAppServerTests/AppServerClientTests.swift index 6a31084..d1a8afb 100644 --- a/Tests/CodexReviewAppServerTests/AppServerClientTests.swift +++ b/Tests/CodexReviewAppServerTests/AppServerClientTests.swift @@ -108,6 +108,62 @@ struct AppServerClientTests { #expect(try await iterator.next() == .completed(finalReview: "Looks good.")) } + @Test func backendCompletesReviewFromFinalAnswerDeltaWhenTerminalPayloadIsSparse() async throws { + let runtime = try await CodexAppServerTestRuntime.start() + try await runtime.transport.enqueueThreadStart(threadID: "thread-1", model: "gpt-5") + try await runtime.transport.enqueueReviewStart(turnID: "turn-1", reviewThreadID: "review-thread") + await runtime.transport.waitForNotificationStreamCount(1) + let backend = AppServerCodexReviewBackend(appServer: runtime.server) + + let attempt = try await backend.startReview(makeReviewStart(target: .baseBranch("main"))) + var iterator = eventSequence(attempt).makeAsyncIterator() + + try await runtime.transport.emitServerNotification( + method: "item/agentMessage/delta", + params: TestDeltaNotification( + threadID: "review-thread", + turnID: "turn-1", + itemID: "msg-1", + delta: "Looks good.", + phase: "final_answer" + ) + ) + try await runtime.transport.emitServerNotification( + method: "turn/completed", + params: TestTurnNotification( + threadID: "review-thread", + turn: .init(id: "turn-1", status: "completed") + ) + ) + + #expect( + try await iterator.next() == .started(turnID: "turn-1", reviewThreadID: "review-thread", model: "gpt-5")) + #expect(try await iterator.next() == .completed(finalReview: "Looks good.")) + } + + @Test func backendFailsCompletedReviewWithoutFinalAnswer() async throws { + let runtime = try await CodexAppServerTestRuntime.start() + try await runtime.transport.enqueueThreadStart(threadID: "thread-1", model: "gpt-5") + try await runtime.transport.enqueueReviewStart(turnID: "turn-1", reviewThreadID: "review-thread") + await runtime.transport.waitForNotificationStreamCount(1) + let backend = AppServerCodexReviewBackend(appServer: runtime.server) + + let attempt = try await backend.startReview(makeReviewStart(target: .baseBranch("main"))) + var iterator = eventSequence(attempt).makeAsyncIterator() + + try await runtime.transport.emitServerNotification( + method: "turn/completed", + params: TestTurnNotification( + threadID: "review-thread", + turn: .init(id: "turn-1", status: "completed") + ) + ) + + #expect( + try await iterator.next() == .started(turnID: "turn-1", reviewThreadID: "review-thread", model: "gpt-5")) + #expect(try await iterator.next() == .failed("Review completed without a final response.")) + } + @Test func backendIgnoresAgentMessageDeltasInLifecycleStream() async throws { let runtime = try await CodexAppServerTestRuntime.start() try await runtime.transport.enqueueThreadStart(threadID: "thread-1", model: "gpt-5") @@ -593,12 +649,14 @@ private struct TestDeltaNotification: Encodable, Sendable { var turnID: String var itemID: String var delta: String + var phase: String? enum CodingKeys: String, CodingKey { case threadID = "threadId" case turnID = "turnId" case itemID = "itemId" case delta + case phase } } diff --git a/Tools/ReviewMonitor/CodexReviewMonitor.xcodeproj/project.xcworkspace/xcshareddata/swiftpm/Package.resolved b/Tools/ReviewMonitor/CodexReviewMonitor.xcodeproj/project.xcworkspace/xcshareddata/swiftpm/Package.resolved index 79e0d60..d55cce3 100644 --- a/Tools/ReviewMonitor/CodexReviewMonitor.xcodeproj/project.xcworkspace/xcshareddata/swiftpm/Package.resolved +++ b/Tools/ReviewMonitor/CodexReviewMonitor.xcodeproj/project.xcworkspace/xcshareddata/swiftpm/Package.resolved @@ -1,12 +1,12 @@ { - "originHash" : "11d1673255c652381957935c0aa0fcc8f6cba52200a3e3e3c7296d58e6207f76", + "originHash" : "090f3e640b9c9ff9c1ef794fed79ee1dcdb35f013243ff248225a302aebbbb8a", "pins" : [ { "identity" : "codexkit", "kind" : "remoteSourceControl", "location" : "https://github.com/lynnswap/CodexKit.git", "state" : { - "revision" : "6de9e019d35095390252102bd30b069e1090c874" + "revision" : "23276c81cf160b773b3f9fb5ae710d257838cad0" } }, {