diff --git a/CHANGELOG.md b/CHANGELOG.md
--- a/CHANGELOG.md
+++ b/CHANGELOG.md
@@ -7,6 +7,22 @@
 
 ## [Unreleased]
 
+## [baikai-openai 0.7.3.1] - 2026-11-14
+
+### Fixed
+
+- Require `baikai ^>=0.10.1.1` in library and tests so new adapter consumers
+  receive the Codex final-message correction.
+
+## [baikai 0.10.1.1] - 2026-11-14
+
+### Fixed
+
+- `baikai`: the Codex batch parser returns the last completed agent message
+  instead of concatenating complete messages, preserving structured JSON after
+  intermediate commentary. Started and updated items cannot replace the final
+  answer; usage and thread observations still cover the whole run ([BUG-2](docs/bug-reports/codex-concatenates-complete-agent-messages.md)).
+
 ## [baikai 0.10.1.0] - 2026-11-03
 
 ### Added
diff --git a/baikai.cabal b/baikai.cabal
--- a/baikai.cabal
+++ b/baikai.cabal
@@ -1,6 +1,6 @@
 cabal-version: 3.4
 name: baikai
-version: 0.10.1.0
+version: 0.10.1.1
 synopsis: Unified Haskell interface for multiple AI providers
 description:
   baikai provides a unified, provider-agnostic Haskell interface for working
diff --git a/src/Baikai/Provider/Cli/Internal.hs b/src/Baikai/Provider/Cli/Internal.hs
--- a/src/Baikai/Provider/Cli/Internal.hs
+++ b/src/Baikai/Provider/Cli/Internal.hs
@@ -205,7 +205,7 @@
 -- must be recorded as 'Baikai.Evidence.Unobserved' downstream and must
 -- never be filled in from the request.
 data CodexRunReport = CodexRunReport
-  { -- | The concatenated text of every @agent_message@ event.
+  { -- | The last completed @agent_message@ body, or empty text if none.
     message :: !Text,
     -- | Codex's own handle for the conversation, from the
     -- thread-start event.
@@ -221,10 +221,10 @@
 
 -- | The accumulator 'parseCodexJsonlStream' folds events into.
 --
--- Separate from 'CodexRunReport' only because the message text arrives
--- in pieces and is kept reversed until the fold finishes.
+-- Complete message bodies replace the candidate, including an empty body.
+-- They are independent answers rather than fragments to concatenate.
 data CodexAccumulator = CodexAccumulator
-  { messages :: ![Text],
+  { lastMessage :: !(Maybe Text),
     threadId :: !(Maybe Text),
     reportedModel :: !(Maybe Text),
     usage :: !(Maybe Usage)
@@ -234,7 +234,7 @@
 emptyCodexAccumulator :: CodexAccumulator
 emptyCodexAccumulator =
   CodexAccumulator
-    { messages = [],
+    { lastMessage = Nothing,
       threadId = Nothing,
       reportedModel = Nothing,
       usage = Nothing
@@ -243,6 +243,8 @@
 -- | Consume a stream of stdout bytes from @codex exec --json@, split on
 -- newlines, decode each line as JSON, and fold the events into what the
 -- run reported about itself.
+-- Only the last completed agent message supplies the answer; observations
+-- are collected independently from the entire stream.
 --
 -- A line that is not valid JSON is skipped rather than failing the run:
 -- codex writes progress chatter to stderr, but a future version writing
@@ -269,7 +271,7 @@
   let acc = absorbLine folded (joinPieces pending)
   pure
     CodexRunReport
-      { message = Text.concat (reverse (acc ^. #messages)),
+      { message = fromMaybe "" (acc ^. #lastMessage),
         threadId = acc ^. #threadId,
         reportedModel = acc ^. #reportedModel,
         usage = acc ^. #usage
@@ -296,7 +298,7 @@
 absorbCodexEvent acc v =
   withTurn
     ( acc
-        & #messages %~ maybe id (:) (extractAgentMessage v)
+        & #lastMessage %~ (extractAgentMessage v <|>)
         & #threadId %~ (<|> extractThreadId v)
     )
   where
@@ -308,8 +310,8 @@
           & #reportedModel .~ reported
 
 -- | Best-effort extractor for the assistant text inside a single
--- Codex @--json@ event. See the original implementation's
--- documentation for the schema variants accepted.
+-- Codex @--json@ event. Modern @item@ envelopes must be @item.completed@;
+-- older flat and @msg@-nested agent messages are also complete bodies.
 extractAgentMessage :: Value -> Maybe Text
 extractAgentMessage = parseMaybe parser
   where
@@ -317,10 +319,11 @@
       mItem <- o .:? "item"
       case (mItem :: Maybe Value) of
         Just (Aeson.Object io) -> do
+          eventType <- o .:? "type"
           ty <- io .:? "type"
-          case (ty :: Maybe Text) of
-            Just "agent_message" -> pickText io
-            _ -> tryMsg o
+          case (eventType :: Maybe Text, ty :: Maybe Text) of
+            (Just "item.completed", Just "agent_message") -> pickText io
+            _ -> fail "not a completed agent_message"
         _ -> tryMsg o
 
     tryMsg o = do
diff --git a/test/CliInternalSpec.hs b/test/CliInternalSpec.hs
--- a/test/CliInternalSpec.hs
+++ b/test/CliInternalSpec.hs
@@ -16,6 +16,7 @@
 import Data.ByteString (ByteString)
 import Data.ByteString qualified as BS
 import Data.ByteString.Char8 qualified as BS8
+import Data.ByteString.Lazy qualified as LBS
 import Data.Generics.Labels ()
 import Data.List (isInfixOf)
 import Data.Text qualified as Text
@@ -80,7 +81,53 @@
 codexParserTests =
   testGroup
     "codex exec --json event stream"
-    [ testCase "a recorded run yields its text, thread id, and token counts" $ do
+    [ testCase "two complete JSON answers return only the final object" $ do
+        let answer why = Aeson.object ["why" Aeson..= (why :: Text.Text), "what" Aeson..= ("X" :: Text.Text), "blocking" Aeson..= ("B" :: Text.Text), "status" Aeson..= ("S" :: Text.Text)]
+            encoded = Text.decodeUtf8 . LBS.toStrict . Aeson.encode
+        report <- parseCodex [codexMessage "item.completed" (encoded (answer "Intermediate")), codexMessage "item.completed" (encoded (answer "Final"))]
+        Aeson.eitherDecodeStrict' (Text.encodeUtf8 (report ^. #message)) @?= Right (answer "Final"),
+      testCase "three complete messages replace rather than append" $ do
+        report <- parseCodex (map (codexMessage "item.completed") ["first", "second", "third"])
+        report ^. #message @?= "third",
+      testCase "prose commentary is excluded from a final JSON answer" $ do
+        report <- parseCodex (map (codexMessage "item.completed") ["Working on it.", "{\"why\":\"Final\"}"])
+        report ^. #message @?= "{\"why\":\"Final\"}",
+      testCase "an empty completed message replaces preceding text" $ do
+        report <- parseCodex (map (codexMessage "item.completed") ["commentary", ""])
+        report ^. #message @?= "",
+      testCase "malformed final answer text replaces earlier valid JSON" $ do
+        report <- parseCodex (map (codexMessage "item.completed") ["{\"why\":\"valid\"}", "{\"why\":"])
+        report ^. #message @?= "{\"why\":"
+        assertBool "the selected body is still invalid JSON" (case Aeson.eitherDecodeStrict' @Aeson.Value (Text.encodeUtf8 (report ^. #message)) of Left _ -> True; Right _ -> False),
+      testCase "started and updated items never replace a completed answer" $ do
+        report <-
+          parseCodex
+            [ codexMessage "item.started" "before",
+              codexMessage "item.completed" "first",
+              codexMessage "item.updated" "between",
+              codexMessage "item.completed" "final",
+              codexMessage "item.started" "after-start",
+              codexMessage "item.updated" "after-update",
+              "{\"type\":\"item.completed\",\"item\":{\"type\":\"reasoning\",\"text\":\"reasoning\"}}\n",
+              "{\"type\":\"item.completed\",\"item\":{\"type\":\"command_execution\",\"text\":\"tool\"}}\n"
+            ]
+        report ^. #message @?= "final",
+      testCase "only incomplete items yield the existing no-answer result" $ do
+        report <- parseCodex [codexMessage "item.started" "pending", codexMessage "item.updated" "partial"]
+        report ^. #message @?= "",
+      testCase "legacy complete messages also replace their predecessors" $ do
+        nested <- parseCodex ["{\"msg\":{\"type\":\"agent_message\",\"message\":\"first\"}}\n{\"msg\":{\"type\":\"agent_message\",\"message\":\"nested-final\"}}\n"]
+        nested ^. #message @?= "nested-final"
+        flat <- parseCodex ["{\"type\":\"agent_message\",\"message\":\"first\"}\n{\"type\":\"agent_message\",\"text\":\"flat-final\"}\n"]
+        flat ^. #message @?= "flat-final",
+      testCase "message replacement survives byte chunks and a final missing newline" $ do
+        let bytes = codexMessage "item.completed" "earlier" <> BS.init (codexMessage "item.completed" "final λ")
+        report <- parseCodex (map BS.singleton (BS.unpack bytes))
+        report ^. #message @?= "final λ",
+      testCase "malformed outer lines cannot displace the last completed answer" $ do
+        report <- parseCodex [codexMessage "item.completed" "answer", "{broken\n", "{\"type\":\"item.completed\",\"item\":{\"type\":\"agent_message\"}}\n", "{\"type\":\"future-event\"}\n"]
+        report ^. #message @?= "answer",
+      testCase "a recorded run yields its text, thread id, and token counts" $ do
         recorded <- BS.readFile "test/fixtures/codex-events.jsonl"
         report <- parseCodex [recorded]
         report ^. #message @?= "ok"
@@ -189,6 +236,10 @@
               "assembling one two-megabyte event did not finish within ten seconds"
           Just report -> Text.length (report ^. #message) @?= 2000000
     ]
+
+codexMessage :: Text.Text -> Text.Text -> ByteString
+codexMessage eventType body =
+  LBS.toStrict (Aeson.encode (Aeson.object ["type" Aeson..= eventType, "item" Aeson..= Aeson.object ["type" Aeson..= ("agent_message" :: Text.Text), "text" Aeson..= body]])) <> "\n"
 
 -- ============================================================
 -- The claude result document
