Skip to content

sync: upstream Vercel Chat v4.26.0#43

Merged
patrick-chinchill merged 40 commits into
mainfrom
claude/port-upstream-4.26-b5zzY
Apr 23, 2026
Merged

sync: upstream Vercel Chat v4.26.0#43
patrick-chinchill merged 40 commits into
mainfrom
claude/port-upstream-4.26-b5zzY

Conversation

@patrick-chinchill
Copy link
Copy Markdown
Collaborator

@patrick-chinchill patrick-chinchill commented Apr 16, 2026

Ports four upstream commits (chat@4.25.0..chat@4.26.0):

  • fix(chat): guard fallback streaming against empty content (#357)
    Thread._fallback_stream() no longer edits or posts empty content during
    the LLM warm-up or when a chunk buffers to whitespace. With the
    placeholder disabled and an empty stream, posts a single space so the
    stream still returns a SentMessage.

  • feat(slack): empty header cells render as space in native table block
    (#373) - Replaces or " " truthiness fallback with an explicit length
    check so "0"-like strings are not replaced.

  • fix(gchat): render markdown links with <url|text> custom-label syntax
    (#356) - [Click here](https://example.com) now renders as
    <https://example.com|Click here> instead of Click here (https://...).

  • fix(chat): export standalone reviver for workflow-safe deserialization
    (#257) - New top-level chat_sdk.reviver lets Vercel Workflow functions
    deserialize Thread/Channel/Message without pulling adapter dependencies
    through the Chat instance. Thread.to_json()/Channel.to_json() now prefer
    the stored _adapter_name so objects revived without a singleton can
    still re-serialize. Python usage: json.loads(payload, object_hook=reviver).

Also:

  • UPSTREAM_PARITY = "4.26.0"
  • Version bumped to 0.4.26
  • README/CLAUDE.md/CHANGELOG.md updated
  • Existing fallback-streaming tests updated to reflect the new
    empty-content semantics (no more empty intermediate posts/edits)

Summary by CodeRabbit

  • New Features

    • Added standalone reviver for JSON deserialization of chat Thread/Channel/Message
    • Google Chat link formatting now emits and parses custom <url|text> links
  • Bug Fixes

    • Slack table rendering preserves empty cells as a single space
    • Streaming fallback avoids posting/editing empty or whitespace-only content and reliably clears placeholders
    • Google Chat link serialization safely handles labels with reserved characters
  • Documentation

    • Updated version/upstream references and added a divergence policy
  • Chores

    • Added CI lint/type-check workflow; bumped project version to 0.4.26 and added pyrefly dev config
  • Tests

    • Added/updated tests for serialization/reviver, link handling, Slack tables, and streaming behavior

claude added 3 commits April 16, 2026 23:12
Ports four upstream commits (chat@4.25.0..chat@4.26.0):

- fix(chat): guard fallback streaming against empty content (#357)
  Thread._fallback_stream() no longer edits or posts empty content during
  the LLM warm-up or when a chunk buffers to whitespace. With the
  placeholder disabled and an empty stream, posts a single space so the
  stream still returns a SentMessage.

- feat(slack): empty header cells render as space in native table block
  (#373) - Replaces `or " "` truthiness fallback with an explicit length
  check so "0"-like strings are not replaced.

- fix(gchat): render markdown links with <url|text> custom-label syntax
  (#356) - `[Click here](https://example.com)` now renders as
  `<https://example.com|Click here>` instead of `Click here (https://...)`.

- fix(chat): export standalone `reviver` for workflow-safe deserialization
  (#257) - New top-level chat_sdk.reviver lets Vercel Workflow functions
  deserialize Thread/Channel/Message without pulling adapter dependencies
  through the Chat instance. Thread.to_json()/Channel.to_json() now prefer
  the stored _adapter_name so objects revived without a singleton can
  still re-serialize. Python usage: json.loads(payload, object_hook=reviver).

Also:
- UPSTREAM_PARITY = "4.26.0"
- Version bumped to 0.4.26
- README/CLAUDE.md/CHANGELOG.md updated
- Existing fallback-streaming tests updated to reflect the new
  empty-content semantics (no more empty intermediate posts/edits)
Upstream 4.26 guards against empty edits in fallback streaming, which
avoids platform APIs rejecting empty bodies but leaves the "..." placeholder
stranded on the message forever when a stream produces no real content.

We diverge by issuing one final edit_message(" ") when the placeholder is
still the latest visible content and the stream produced only whitespace.
This mirrors the behavior of the no-placeholder branch (which already posts
" " for empty streams) and eliminates the SentMessage.text-vs-rendered-
content mismatch that was surfaced in review.

Documented in docs/UPSTREAM_SYNC.md under Known Non-Parity.

https://claude.ai/code/session_01XE1bMoQ5BjCvgi1iLGKKhG
The fallback-streaming placeholder fix was added in f932f26 but only
recorded in UPSTREAM_SYNC.md. Surface it in the two other places
developers look: the CHANGELOG under 0.4.26, and ARCHITECTURE.md near
the fallback streaming description.

https://claude.ai/code/session_01XE1bMoQ5BjCvgi1iLGKKhG
Copy link
Copy Markdown

@chatgpt-codex-connector chatgpt-codex-connector Bot left a comment

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

💡 Codex Review

current_msg = Message.from_json(current_msg_raw)

P1 Badge Handle pre-revived currentMessage in Thread.from_json

With the new standalone reviver used as json.loads(..., object_hook=reviver), nested dicts are revived before their parents, so a thread payload containing currentMessage will arrive at ThreadImpl.from_json with currentMessage already as a Message instance. This code unconditionally calls Message.from_json(current_msg_raw), which raises AttributeError (Message has no .get) and breaks deserialization for valid serialized threads that include currentMessage.

ℹ️ About Codex in GitHub

Your team has set up Codex to review pull requests in this repo. Reviews are triggered when you

  • Open a pull request for review
  • Mark a draft as ready
  • Comment "@codex review".

If Codex has suggestions, it will comment; otherwise it will react with 👍.

Codex can also answer questions or update the PR. Try commenting "@codex address that feedback".

Copy link
Copy Markdown
Contributor

@gemini-code-assist gemini-code-assist Bot left a comment

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Code Review

This pull request updates the SDK to version 0.4.26, syncing with Vercel Chat 4.26.0. Key additions include a standalone reviver for deserialization without a Chat instance and workflow-safe to_json methods. It also improves fallback streaming by guarding against empty content and fixes rendering issues for Slack tables and Google Chat links. Feedback includes correcting a typo in the CHANGELOG release date and moving local imports in tests to the module level to comply with PEP 8.

Comment thread CHANGELOG.md
Comment thread tests/test_serialization.py Outdated
claude added 2 commits April 16, 2026 23:59
Codex identified that ``json.loads(..., object_hook=reviver)`` revives
nested dicts bottom-up, so by the time ``ThreadImpl.from_json`` runs,
a payload's ``currentMessage`` is already a ``Message`` instance, not a
dict. The old code then called ``Message.from_json(current_msg_raw)``
on that Message, which raised ``AttributeError`` on ``.get`` and broke
deserialization of any serialized thread containing a currentMessage.

Fix:
- ``Message.from_json`` returns ``data`` unchanged if it's already a Message
- ``ThreadImpl.from_json`` / ``ChannelImpl.from_json`` do the same, plus
  pass the (possibly already-Message) ``currentMessage`` through the
  now-idempotent ``Message.from_json``
- Regression test exercises the exact object_hook bottom-up path with a
  nested currentMessage

Also moved ``TestStandaloneReviver``'s per-method local imports to the
module top level per gemini-code-assist review (PEP 8 E402).

https://claude.ai/code/session_01XE1bMoQ5BjCvgi1iLGKKhG
Adds a dedicated `Lint & Type Check` workflow modeled on the chinchill-api
pattern:

- Ruff check/format now covers src/, tests/, and scripts/ (previously only
  src/, which let the Gemini PEP 8 finding on tests/test_serialization.py
  slip past CI).
- Pyrefly type check with a baseline file (.pyrefly-baseline.json).
  Baseline captures the 213 existing errors in src/ so we can enable
  pyrefly immediately without a large type-cleanup detour — new errors
  fail CI, pre-existing ones are allowed.
- Audit step moved from the test matrix into the lint job so the
  pytest workflow only runs tests.
- Per-step `continue-on-error: true` with aggregate failure at the end,
  so one failure doesn't hide the others — developers see all issues at
  once.

Optional adapter deps (slack_sdk, nacl, google, redis, asyncpg, httpx,
jwt) are listed in `replace-imports-with-any` so lazy imports don't
flag missing-import when the corresponding extra isn't installed.

Refresh baseline after legitimate fixes with:
  uv run pyrefly check --baseline=.pyrefly-baseline.json --update-baseline

https://claude.ai/code/session_01XE1bMoQ5BjCvgi1iLGKKhG
Comment thread .github/workflows/lint.yml Fixed
claude and others added 2 commits April 17, 2026 01:23
Two small CI fixes:

1. The new Lint & Type Check job failed on its first run because
   `astral-sh/setup-uv@v4`'s `enable-cache: true` and the `actions/cache`
   step both key off `uv.lock`, which we gitignore. Dropped both cache
   layers — matches the existing test.yml pattern and removes the
   hashFiles('uv.lock') failure.

2. github-advanced-security flagged the workflows for missing
   GITHUB_TOKEN permissions. Added `permissions: { contents: read }` at
   the workflow level to both lint.yml and test.yml — minimum needed for
   checkout + tests.

https://claude.ai/code/session_01XE1bMoQ5BjCvgi1iLGKKhG
…ntain permissions'

Co-authored-by: Copilot Autofix powered by AI <62310815+github-advanced-security[bot]@users.noreply.github.com>
Signed-off-by: patrick-chinchill <patrick@chinchill.ai>
@patrick-chinchill patrick-chinchill marked this pull request as draft April 17, 2026 01:41
@patrick-chinchill patrick-chinchill marked this pull request as ready for review April 17, 2026 01:41
Copy link
Copy Markdown
Collaborator Author

@patrick-chinchill patrick-chinchill left a comment

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

🤖 Codex Code Review

Summary

Re-reviewed claude/port-upstream-4.26-b5zzY at 6070884. The earlier blockers are fixed: whitespace-only fallback streams now clear the placeholder correctly, and the standalone reviver() path now safely handles nested currentMessage values revived via json.loads(..., object_hook=reviver).

Findings

No issues found after review.

Verification Performed

  • Ran uv run ruff check src tests
    • result: passed
  • Ran uv run pytest tests/test_serialization.py tests/test_thread_faithful.py tests/test_chat_faithful.py tests/test_slack_format.py tests/test_gchat_format_extended.py -q
    • result: 343 passed, 2 skipped
  • Verified the nested currentMessage regression test in tests/test_serialization.py passes.
  • Re-ran a direct json.loads(..., object_hook=reviver) repro and confirmed it now returns a ThreadImpl with a real nested Message instance.

Verdict

APPROVE

Copy link
Copy Markdown

@chatgpt-codex-connector chatgpt-codex-connector Bot left a comment

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

💡 Codex Review

Here are some automated review suggestions for this pull request.

Reviewed commit: 607088421b

ℹ️ About Codex in GitHub

Your team has set up Codex to review pull requests in this repo. Reviews are triggered when you

  • Open a pull request for review
  • Mark a draft as ready
  • Comment "@codex review".

If Codex has suggestions, it will comment; otherwise it will react with 👍.

Codex can also answer questions or update the PR. Try commenting "@codex address that feedback".

Comment thread src/chat_sdk/adapters/google_chat/format_converter.py
Comment thread src/chat_sdk/thread.py Outdated
"currentMessage": self._current_message.to_json() if self._current_message else None,
"isDM": self._is_dm,
"adapterName": self.adapter.name,
"adapterName": self._adapter_name or self.adapter.name,
Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

P2 Badge Serialize the bound adapter when one is explicitly set

Using _adapter_name first causes to_json() to emit the original payload adapter name even after ThreadImpl.from_json(..., adapter=...) has bound a different adapter instance. In that scenario, runtime calls use the explicit adapter but serialized output points to a stale adapter, so a later deserialize can resolve the wrong adapter (or fail) despite the current thread being valid.

Useful? React with 👍 / 👎.

Copy link
Copy Markdown
Collaborator Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Addressed in commit 6c3871c (same commit). ThreadImpl.from_json(data, adapter=X) and ChannelImpl.from_json(data, adapter=X) now set _adapter_name = adapter.name so to_json() reflects the bound adapter, not a stale name from the payload. Regression tests in tests/test_serialization.py::test_should_sync_adapter_name_when_explicit_adapter_is_bound and tests/test_channel_faithful.py::test_should_sync_adapter_name_when_explicit_adapter_is_bound. Safe to resolve.


Generated by Claude Code

claude added 4 commits April 17, 2026 01:54
…ebind

Two Codex P1/P2 review findings:

P1 (gchat): commit 4cc87dd in the 4.26 sync switched link rendering to
Google Chat's <url|text> custom-label syntax but didn't teach to_ast()
and extract_plain_text() to parse it back. Messages posted with
[label](url) round-tripped to raw "<url|label>" text with no link node,
breaking downstream handlers that expect structured links or clean
plain text. Added the inverse regex in both methods.

P2 (serde): ThreadImpl.to_json() / ChannelImpl.to_json() preferred
_adapter_name over self.adapter.name to preserve the original name
through standalone-reviver round-trips. But when a caller explicitly
binds a different adapter via from_json(data, adapter=X), the
serialized output kept the stale name from the payload while runtime
calls used X — a later deserialize could resolve the wrong adapter or
fail. Now from_json() sets _adapter_name = adapter.name when an
explicit adapter is bound so serialize and runtime stay in sync.

Regression tests:
- tests/test_gchat_format_extended.py: link parse + plain-text strip
- tests/test_serialization.py: thread adapter_name sync
- tests/test_channel_faithful.py: channel adapter_name sync

https://claude.ai/code/session_01XE1bMoQ5BjCvgi1iLGKKhG
Copilot Autofix added a permissions block before `concurrency`
without noticing 0cc5d11 had already added one after `concurrency`.
Remove the redundant second block; Copilot's position (before
concurrency) is the more conventional GitHub Actions placement.

https://claude.ai/code/session_01XE1bMoQ5BjCvgi1iLGKKhG
Same as the entry on the type-cleanup branch — keeps the
parallel-subagent worktree directory out of git.

https://claude.ai/code/session_01XE1bMoQ5BjCvgi1iLGKKhG
Both Codex P1/P2 fixes in c4e3bee were introduced without checking
whether upstream handles these cases — turns out upstream has the
same latent bugs in both places:

- packages/adapter-gchat/src/markdown.ts toAst() doesn't parse
  <url|text> back to a link node.
- packages/chat/src/thread.ts fromJSON(json, adapter?) sets
  _adapter but leaves _adapterName at the payload value.

So our fixes are intentional divergences, not ports. Add both to
the Known Non-Parity table and call them out in the CHANGELOG so
we don't re-discover them on the next sync.

https://claude.ai/code/session_01XE1bMoQ5BjCvgi1iLGKKhG
@patrick-chinchill patrick-chinchill marked this pull request as draft April 17, 2026 02:15
@patrick-chinchill patrick-chinchill marked this pull request as ready for review April 17, 2026 02:15
Copy link
Copy Markdown

@chatgpt-codex-connector chatgpt-codex-connector Bot left a comment

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

💡 Codex Review

Here are some automated review suggestions for this pull request.

Reviewed commit: 2ea626be65

ℹ️ About Codex in GitHub

Your team has set up Codex to review pull requests in this repo. Reviews are triggered when you

  • Open a pull request for review
  • Mark a draft as ready
  • Comment "@codex review".

If Codex has suggestions, it will comment; otherwise it will react with 👍.

Codex can also answer questions or update the PR. Try commenting "@codex address that feedback".

Comment thread src/chat_sdk/adapters/google_chat/format_converter.py
Codex P2 caught a gap in c4e3bee: _node_to_gchat emits <url|text>
for every link node regardless of scheme, but the regex I added to
to_ast() / extract_plain_text() only matched http(s)://. So
[Email](mailto:test@example.com) would render as
<mailto:test@example.com|Email> and then round-trip back as raw
text — non-HTTP links still lost their link nodes and plain-text
extraction on the way back.

Widen both regexes to accept any RFC 3986 scheme
([a-zA-Z][a-zA-Z0-9+.-]*:), covering mailto, tel, ftp, etc. Add a
regression that exercises three representative schemes.

https://claude.ai/code/session_01XE1bMoQ5BjCvgi1iLGKKhG
@patrick-chinchill patrick-chinchill marked this pull request as draft April 17, 2026 02:48
@patrick-chinchill patrick-chinchill marked this pull request as ready for review April 17, 2026 02:48
Copy link
Copy Markdown

@chatgpt-codex-connector chatgpt-codex-connector Bot left a comment

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

💡 Codex Review

Here are some automated review suggestions for this pull request.

Reviewed commit: bf7456355f

ℹ️ About Codex in GitHub

Your team has set up Codex to review pull requests in this repo. Reviews are triggered when you

  • Open a pull request for review
  • Mark a draft as ready
  • Comment "@codex review".

If Codex has suggestions, it will comment; otherwise it will react with 👍.

Codex can also answer questions or update the PR. Try commenting "@codex address that feedback".

Comment thread src/chat_sdk/adapters/google_chat/format_converter.py
claude added 2 commits April 17, 2026 02:56
…>/|/newline

Codex P2: Google Chat's <url|text> syntax has no way to escape the `|` or
`>` delimiters inside the label, and a newline would break the single-line
form too. Emitting these characters produces malformed output — both
Google Chat's own renderer and our round-trip regex stop at the first
`>` / `|`, truncating the label.

Fall back to the pre-4.26 `text (url)` form in those cases. The URL is
still auto-detected as a link by Google Chat; the label text is
preserved intact; and the output round-trips cleanly through our
parsers (just without a structured link node for that edge case, which
matches upstream behavior for any non-`<url|text>` link form).

Regression test in test_gchat_format_extended.py covers `a > b`,
`a | b`, and `a\nb` labels.

https://claude.ai/code/session_01XE1bMoQ5BjCvgi1iLGKKhG
Commit dfef313 added a fallback from <url|text> to "text (url)" when
the label contains |, >, or newline — upstream emits the malformed
<url|a > b> form verbatim. Add the divergence to both the
Known Non-Parity table and the 0.4.26 CHANGELOG entry so we don't
rediscover it next sync.

https://claude.ai/code/session_01XE1bMoQ5BjCvgi1iLGKKhG
@coderabbitai
Copy link
Copy Markdown

coderabbitai Bot commented Apr 17, 2026

Note

Reviews paused

It looks like this branch is under active development. To avoid overwhelming you with review comments due to an influx of new commits, CodeRabbit has automatically paused this review. You can configure this behavior by changing the reviews.auto_review.auto_pause_after_reviewed_commits setting.

Use the following commands to manage reviews:

  • @coderabbitai resume to resume automatic reviews.
  • @coderabbitai review to trigger a single review.

Use the checkboxes below for quick actions:

  • ▶️ Resume reviews
  • 🔍 Trigger review
📝 Walkthrough

Walkthrough

Adds a lint/type-check CI workflow, bumps version to 0.4.26, introduces a top-level JSON reviver, aligns stored adapter names during (de)serialization, changes fallback streaming to avoid empty messages and clear placeholders, improves Google Chat/Slack format handling, and expands tests and docs.

Changes

Cohort / File(s) Summary
CI/CD Workflows
/.github/workflows/lint.yml, /.github/workflows/test.yml
Added a “Lint & Type Check” workflow (ruff, ruff format check, audit_test_quality, pyrefly) with path filters, concurrency and draft-PR skip; removed redundant lint steps from test workflow and set contents: read.
Version & Tooling
pyproject.toml, README.md, CLAUDE.md, /.gitignore
Bumped project version to 0.4.26, added pyrefly>=0.61 and [tool.pyrefly] config, updated upstream/version text, and ignored .claude/.
Public API Export
src/chat_sdk/__init__.py, src/chat_sdk/reviver.py
Exported reviver at package root and added reviver(value: Any) -> Any to support json.loads(..., object_hook=reviver) revival of Thread/Channel/Message.
Serialization & Adapter Sync
src/chat_sdk/thread.py, src/chat_sdk/channel.py, src/chat_sdk/types.py
to_json() now prefers ._adapter_name when set; from_json() accepts already-revived instances (idempotent) and, when given an explicit adapter, updates ._adapter_name to match the bound adapter. Function signatures updated to accept instance-or-dict where applicable.
Format Converters
src/chat_sdk/adapters/google_chat/format_converter.py, src/chat_sdk/adapters/slack/format_converter.py
Google Chat: parse `<url
Fallback Streaming Behavior
src/chat_sdk/thread.py
Refined fallback streaming: skip initial post/edit for whitespace-only committable chunks; avoid posting/editing empty content; when stream ends whitespace-only, attempt to clear placeholder by editing it to a single space " ", log warnings on edit failure.
Documentation & Changelog
CHANGELOG.md, docs/ARCHITECTURE.md, docs/UPSTREAM_SYNC.md
Documented reviver API, placeholder-clearing behavior, adapter-name sync, Google Chat link round-trip, and added a Divergence Policy with Known Non-Parity entries.
Tests — Serialization & Reviver
tests/test_serialization.py, tests/test_channel_faithful.py
Added tests for reviver as object_hook, nested revival ordering, idempotent from_json, and adapter-name synchronization when an explicit adapter is bound.
Tests — Formats & Streaming
tests/test_gchat_format_extended.py, tests/test_slack_format.py, tests/test_thread_faithful.py, tests/test_chat_faithful.py
Added Google Chat custom-link parse/emit tests (including non-HTTP schemes and reserved-char fallbacks), Slack empty-cell tests, and updated streaming tests to reflect whitespace-placeholder clearing and final post/edit behaviors.

Sequence Diagram(s)

sequenceDiagram
    participant Renderer as Renderer
    participant Thread as ThreadImpl
    participant Adapter as Adapter
    Renderer->>Thread: provide rendered chunk / get_committable_text()
    alt committable non-whitespace
        Thread->>Adapter: post initial message (placeholder or content)
        Adapter-->>Thread: returns message id
        Thread->>Adapter: edit message for subsequent non-whitespace commits
    else only whitespace or empty
        Thread-->>Renderer: skip initial post/edit
        alt stream ended whitespace-only and placeholder exists
            Thread->>Adapter: edit placeholder to " "
            Adapter-->>Thread: ack / error (logs warning on failure)
        end
    end
    Thread->>Adapter: final post or final edit depending on last non-whitespace content
Loading

Estimated code review effort

🎯 4 (Complex) | ⏱️ ~45 minutes

Poem

🐰 I nibble at JSON, reviving each nest,
Syncing adapter names so serialization’s blessed.
Placeholders cleared with a gentle space hop,
Links keep their brackets, empty cells don’t stop —
A rabbit’s small cheer for tests passing nonstop. 🥕✨

🚥 Pre-merge checks | ✅ 2 | ❌ 1

❌ Failed checks (1 warning)

Check name Status Explanation Resolution
Docstring Coverage ⚠️ Warning Docstring coverage is 37.04% which is insufficient. The required threshold is 80.00%. Write docstrings for the functions missing them to satisfy the coverage threshold.
✅ Passed checks (2 passed)
Check name Status Explanation
Description Check ✅ Passed Check skipped - CodeRabbit’s high-level summary is enabled.
Title check ✅ Passed The title 'sync: upstream Vercel Chat v4.26.0' directly and clearly describes the main change: synchronizing the project with upstream Vercel Chat version 4.26.0. It aligns well with the PR objectives.

✏️ Tip: You can configure your own custom pre-merge checks in the settings.

✨ Finishing Touches
🧪 Generate unit tests (beta)
  • Create PR with unit tests
  • Commit unit tests in branch claude/port-upstream-4.26-b5zzY

Comment @coderabbitai help to get the list of available commands and usage tips.

Add a Divergence Policy section to docs/UPSTREAM_SYNC.md with:
- Criteria for when to diverge (data loss / malformed wire / hard UX
  failure; cosmetic issues stay on parity).
- Required workflow (file upstream issue, diverge commit prefix, add
  non-parity row, in-code breadcrumb, regression test, changelog entry).
- Review signal (sync PR title convention, budget of max 2 divergences
  per sync before escalating).

Retroactively add `# Divergence from upstream — see docs/UPSTREAM_SYNC.md`
breadcrumbs at the four divergence sites introduced in this sync:

- src/chat_sdk/thread.py `_adapter_name = adapter.name` in from_json
- src/chat_sdk/channel.py same in ChannelImpl.from_json
- src/chat_sdk/adapters/google_chat/format_converter.py to_ast()
  custom-link regex
- src/chat_sdk/adapters/google_chat/format_converter.py extract_plain_text()
  custom-link regex
- src/chat_sdk/adapters/google_chat/format_converter.py reserved-char
  fallback branch in _node_to_gchat

The placeholder-clear divergence in thread._fallback_stream already had
a full multi-line explanation; leaving that alone.

https://claude.ai/code/session_01XE1bMoQ5BjCvgi1iLGKKhG
@patrick-chinchill patrick-chinchill changed the title claude/port-upstream-4.26-b5zzY sync: upstream Vercel Chat v4.26.0 Apr 17, 2026
@patrick-chinchill patrick-chinchill marked this pull request as ready for review April 18, 2026 01:02
Copy link
Copy Markdown

@chatgpt-codex-connector chatgpt-codex-connector Bot left a comment

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

💡 Codex Review

Here are some automated review suggestions for this pull request.

Reviewed commit: 6168961f41

ℹ️ About Codex in GitHub

Your team has set up Codex to review pull requests in this repo. Reviews are triggered when you

  • Open a pull request for review
  • Mark a draft as ready
  • Comment "@codex review".

If Codex has suggestions, it will comment; otherwise it will react with 👍.

Codex can also answer questions or update the PR. Try commenting "@codex address that feedback".

Comment thread src/chat_sdk/thread.py
Comment thread src/chat_sdk/channel.py
claude added 2 commits April 18, 2026 01:15
Codex found another variant of the cache/state rebinding class of
bugs (the chat= path this time). If an existing ThreadImpl or
ChannelImpl was constructed directly by a Chat instance, its
`_adapter_name` is None — only `_adapter` is set. The idempotent
chat-rebind path keyed the new-adapter lookup off `_adapter_name`:

    if thread._adapter_name:
        resolved = chat.get_adapter(thread._adapter_name)
        ...
    thread._state_adapter_instance = chat.get_state()

With `_adapter_name == None`, the lookup was skipped but state was
still reassigned to the new chat's backend. Result: post()/edit()
continued routing through the OLD adapter while get_state()/set_state()
hit the NEW chat — messages and state writes landing in different
Chat contexts (and potentially different platforms or environments).

Fix: in both ThreadImpl.from_json and ChannelImpl.from_json, fall
back to the currently-bound adapter's `.name` when `_adapter_name`
is empty, and record the name back onto the instance so future
serialization/rebinds see it.

2 new regression tests exercise the direct-construction + chat-
rebind path end-to-end (build two Chat instances each with a
different `slack` adapter, construct a thread/channel through
chat_a, rebind via from_json(chat=chat_b), assert both adapter AND
state now point at chat_b's objects).

3461 tests pass.

https://claude.ai/code/session_01XE1bMoQ5BjCvgi1iLGKKhG
Three Codex findings in a row landed on the same pattern: an idempotent
factory path (from_json with an existing instance) that updated one
binding but left stale caches or lookup keys from the previous binding.
The symptom is always "split routing" — messages go to the old context,
state to the new.

This wasn't covered by any of the six existing principles (input sweeps,
emit/parse symmetry, pass-interaction, sentinels, divergence budget,
pre-ship). Pass-interaction comes closest but is about *pipeline*
ordering, not *instance attribute* coherence across rebinds. Adding
it as a distinct check means future idempotent APIs get a dedicated
prompt: "walk every attribute and ask whether it came from the old
binding."

Also updates the CLAUDE.md summary to list the new check.

https://claude.ai/code/session_01XE1bMoQ5BjCvgi1iLGKKhG
@patrick-chinchill patrick-chinchill marked this pull request as draft April 18, 2026 01:21
@patrick-chinchill patrick-chinchill marked this pull request as ready for review April 18, 2026 01:21
Copy link
Copy Markdown

@chatgpt-codex-connector chatgpt-codex-connector Bot left a comment

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

💡 Codex Review

Here are some automated review suggestions for this pull request.

Reviewed commit: 5daeb324aa

ℹ️ About Codex in GitHub

Your team has set up Codex to review pull requests in this repo. Reviews are triggered when you

  • Open a pull request for review
  • Mark a draft as ready
  • Comment "@codex review".

If Codex has suggestions, it will comment; otherwise it will react with 👍.

Codex can also answer questions or update the PR. Try commenting "@codex address that feedback".

Comment thread src/chat_sdk/adapters/google_chat/format_converter.py Outdated
claude added 2 commits April 18, 2026 01:33
…-run

Ran the full 7-principle discipline (docs/SELF_REVIEW.md) via 3
subagents on the novel code in this PR after adding principle #7.
Caught 4 issues the previous rounds hadn't — the discipline is still
paying off.

[P2] Rebind leaks `_recent_messages` + `_message_history`
  (from_json subagent, principle #7)
  The last rebind-cache fix only cleared `_state_adapter_instance`
  and `_channel_cache`. `_recent_messages` (populated from the OLD
  adapter's fetches) and `_message_history` (the OLD chat's cache)
  were missed — both would keep routing to the previous context
  after a rebind. Adding them to the invalidation set on ThreadImpl
  (and `_message_history` on ChannelImpl).

[P2] adapter= and chat= were not orthogonal
  (from_json subagent, principle #3)
  `from_json(t, adapter=A, chat=chat_b)` hit the `if adapter:` branch
  and the `elif chat:` branch was silently skipped — so `_adapter=A`
  was applied but `_state_adapter_instance` was left at None
  (invalidated by the cache clear) and lazy-resolved via the
  singleton on next access, NOT chat_b. Split routing.

  Made the two arguments orthogonal: the `chat` block always applies
  its state, and resolves an adapter via chat ONLY when `adapter`
  wasn't passed. Singleton fallback only fires when neither is
  passed.

[P2] URLs containing `|` / `>` produce malformed `<url|text>` emit
  (Codex review of c381a63 — matches a P3 the emit/parse symmetry
  subagent had flagged earlier)
  `foo:bar|baz` emitted as `<foo:bar|baz|Label>` and parsed back with
  url=`foo:bar`. Added URL-delimiter check to the emit fallback set
  alongside the existing label check.

[P3 — defensive] Orthogonal `adapter`/`chat` pair previously fell
  through an `elif adapter is None and has_chat_singleton() ...`
  singleton-fallback branch that was unreachable before; same branch
  now correctly gated so it only fires when neither arg was passed.

3 new regression tests (one per P2):
- test_should_invalidate_recent_messages_and_message_history_on_rebind
- test_should_set_state_from_chat_when_both_adapter_and_chat_passed
- test_falls_back_to_parenthesized_form_for_urls_with_reserved_delimiters

Deferred from the review (not fixed here):
- Async generator hygiene when placeholder post_message raises
  before the try block — lazy generators don't actually leak.
- Asymmetric try/except on final-content edit vs placeholder-clear
  edit — the two branches handle different content shapes (user
  stream content vs forced whitespace). Would consider if an
  adapter issue arose.
- `_name` round-trip (upstream parity), `_is_subscribed_context`
  rebind (niche), KeyError on malformed dict (P3).

3464 tests pass.

https://claude.ai/code/session_01XE1bMoQ5BjCvgi1iLGKKhG
Ran the 7-principle discipline AGAIN with explicit upstream-TS drift
checks. Three subagents, each scoped to one file, returned 9 findings
between them. All addressed.

[P1] thread._fallback_stream used `get_committable_text()` instead
  of `render()` in the edit loop and first-post path.
  `get_committable_text` is for append-only native-streaming surfaces
  (Slack native) — it holds back unclosed lines and wraps tables in
  code fences. The fallback path uses full-message replacement edits,
  so the correct method is `render()` (remend'd markdown, no append-
  only buffering). Upstream packages/chat/src/thread.ts:697 uses
  `render()`; we diverged silently.

  Two existing tests needed updating (the test for `["H", "i"]` with
  placeholder disabled previously asserted "one post of 'Hi', no
  edits" — a byproduct of the get_committable_text holdback. The
  upstream-correct behavior is "post 'H' immediately, edit to 'Hi'".)

[P3] `_is_subscribed_context` not reset on idempotent rebind.
  A thread constructed inside a subscribed-context handler carries
  the flag; on rebind to a new adapter/chat, the new context has no
  active subscription, but `is_subscribed()` would still short-
  circuit to True. Added to the cache-invalidation list alongside
  the other stale-context attributes.

[Docs] Five divergence rows added / extended in UPSTREAM_SYNC.md
  non-parity table (with matching code-site breadcrumbs):
  - Heading rendering as `*text*` (upstream plain-text)
  - Image rendering as `{alt} ({url})` (upstream drops URL)
  - Fallback streaming stream-exception capture + flush
  - Fallback streaming final `final_content` (remend'd) vs
    upstream `accumulated` (raw)
  - Extended existing `<url|text>` label-fallback row to cover
    empty labels, schemeless URLs, and URLs containing `|` / `>`
    (all previously fixed but described in separate rows)

Kept from round-2 reports as "debatable, defer":
  - Symmetry between to_ast (guards adjacent markers) and
    extract_plain_text (doesn't) — stripping is a best-effort
    fallback, not a round-trip contract.
  - Stand-alone reviver signature adaptation is already documented
    in reviver.py docstring.

1 new regression test for `_is_subscribed_context` rebind clearing.
2 existing tests updated to match upstream-correct render() behavior.

3465 tests pass, 0 pyrefly errors, ruff clean.

https://claude.ai/code/session_01XE1bMoQ5BjCvgi1iLGKKhG
@patrick-chinchill patrick-chinchill marked this pull request as draft April 18, 2026 02:01
@patrick-chinchill patrick-chinchill marked this pull request as ready for review April 18, 2026 02:01
Copy link
Copy Markdown

@chatgpt-codex-connector chatgpt-codex-connector Bot left a comment

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

💡 Codex Review

Here are some automated review suggestions for this pull request.

Reviewed commit: 2e141331e9

ℹ️ About Codex in GitHub

Your team has set up Codex to review pull requests in this repo. Reviews are triggered when you

  • Open a pull request for review
  • Mark a draft as ready
  • Comment "@codex review".

If Codex has suggestions, it will comment; otherwise it will react with 👍.

Codex can also answer questions or update the PR. Try commenting "@codex address that feedback".

Comment thread src/chat_sdk/thread.py
…content

Codex P2 on 2e14133: `_create_sent_message` was always invoked with
`final_content` (from `renderer.finish()`) regardless of what
actually ended up on the platform. Most visible in two cases:

1. Strict adapter (Telegram) rejects the placeholder-clear " " edit
   → platform still shows "..." → sent.text was "" (mismatch).
2. No-placeholder path posts `accumulated if accumulated.strip()
   else " "` → platform shows " " for whitespace streams → but
   `last_edit_content = accumulated` (possibly empty) didn't match
   the posted value either.

Fix: track actual platform state via `last_edit_content` at every
post/edit point, and thread that into the SentMessage instead of
`final_content`. Concretely:

- In the `msg is None` branch, set `last_edit_content = markdown`
  (the exact value posted), not `accumulated`.
- After a successful final edit, set `last_edit_content = final_content`.
- Use `last_edit_content` (not `final_content`) when constructing
  the SentMessage.

Regression test pins the strict-adapter case — stranded placeholder
with sent.text matching "...". The whitespace-only happy path still
produces `sent.text == ""` because the markdown parser normalizes
a lone space to empty text (unchanged from before; fixing that
would require special-casing the parser and is a separate concern).

3466 tests pass, 0 pyrefly errors.

https://claude.ai/code/session_01XE1bMoQ5BjCvgi1iLGKKhG
@patrick-chinchill patrick-chinchill marked this pull request as draft April 18, 2026 02:16
@patrick-chinchill patrick-chinchill marked this pull request as ready for review April 18, 2026 02:16
Copy link
Copy Markdown

@chatgpt-codex-connector chatgpt-codex-connector Bot left a comment

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

💡 Codex Review

Here are some automated review suggestions for this pull request.

Reviewed commit: c345ac3d82

ℹ️ About Codex in GitHub

Your team has set up Codex to review pull requests in this repo. Reviews are triggered when you

  • Open a pull request for review
  • Mark a draft as ready
  • Comment "@codex review".

If Codex has suggestions, it will comment; otherwise it will react with 👍.

Codex can also answer questions or update the PR. Try commenting "@codex address that feedback".

Comment thread src/chat_sdk/thread.py
Comment thread src/chat_sdk/channel.py
claude added 4 commits April 18, 2026 02:28
…ilure

Codex P2 (both ThreadImpl and ChannelImpl): `from_json(existing_inst,
chat=Y)` was invalidating caches BEFORE calling `chat.get_adapter(name)`.
If the lookup failed and raised `RuntimeError`, the caller catching the
exception was left with a partially-mutated instance — `_state_adapter_
instance`, `_channel_cache`, `_message_history`, `_recent_messages`,
`_is_subscribed_context` all silently wiped, but `_adapter` unchanged.
Subsequent operations on the thread would re-resolve state/channel via
the singleton instead of the previously-bound chat.

Fix: pre-flight the adapter lookup BEFORE invalidating caches. If the
name doesn't resolve in the new chat, raise immediately with all
original state intact. The binding block below repeats the lookup
when it actually applies the result; that's a cheap duplicate in
exchange for transactional semantics.

Same fix in both `ThreadImpl.from_json` and `ChannelImpl.from_json`.

2 regression tests, each captures a snapshot of every cache-bearing
attribute before the rebind, calls `from_json(instance, chat=bad_chat)`
expecting RuntimeError, then asserts every snapshotted attribute is
byte-identical on the catch side:

- test_should_leave_thread_unchanged_when_chat_rebind_lookup_fails
- test_should_leave_channel_unchanged_when_chat_rebind_lookup_fails

3468 tests pass.

https://claude.ai/code/session_01XE1bMoQ5BjCvgi1iLGKKhG
Two things in one commit since they're both cleanup from the
transactional-rebind fix in 30f6ce5:

- ruff format: 30f6ce5's manual edits to thread.py / channel.py
  introduced formatting drift that CI caught. No semantic change.
- docs/SELF_REVIEW.md §6 (Rebind / idempotent-path state coherence)
  now explicitly covers error paths: "order the steps so any raise
  leaves the object in a well-defined state." Codex found a variant
  of principle #7 that the round-3 subagent's attribute walk had
  missed — the subagent checked WHICH attributes are invalidated
  on rebind, not WHEN they are invalidated relative to raises.
  The §6 note names this subtlety so future reviews catch it.

https://claude.ai/code/session_01XE1bMoQ5BjCvgi1iLGKKhG
…lder

Round-4 subagent P2: the sibling placeholder-clear edit was wrapped
defensively in try/except (to handle Telegram-style whitespace
rejection), but the primary final-content edit wasn't. If the final
edit raises (rate-limit, transient 5xx, content-policy reject), the
exception propagates out of `_fallback_stream` and the caller never
gets a SentMessage — even though the placeholder message is already
sitting on the platform. The caller catching the exception has no
handle to edit/delete it, and the message is orphaned.

Symmetric fix with the placeholder-clear branch: try the edit, log
on failure, leave `last_edit_content` alone. The SentMessage returned
to the caller reflects whatever was last successfully on the platform
(via `last_edit_content`) and carries a valid message ID the caller
can still act on.

Regression test injects an always-failing `edit_message`, asserts
(a) thread.post() doesn't raise, (b) sent.text matches the
placeholder "..." that's actually on the platform, (c) the expected
warn log fires.

3469 tests pass.

https://claude.ai/code/session_01XE1bMoQ5BjCvgi1iLGKKhG
…ty table

Round-5 subagent found the one remaining gap: `docs/SELF_REVIEW.md` §5
requires a CHANGELOG entry for every divergence, and the non-parity
table has 8 Python-specific rows while the CHANGELOG had only 4
bullets. Backfill the missing four and broaden one existing bullet:

- Extended the `<url|text>` reserved-delimiters row to cover `]`,
  newlines, empty labels, schemeless URLs, and URLs containing
  `|`/`>` — all handled by the same emit-side fallback check, all
  accumulated over several commits during review iteration but only
  partially reflected in the CHANGELOG.
- Added dedicated bullets for Google Chat heading → bold rendering
  (upstream loses visual hierarchy by falling through to plain text).
- Added dedicated bullet for Google Chat image rendering (upstream
  silently drops the URL).
- Added bullet for fallback streaming's stream-exception capture +
  flush before re-raise (upstream propagates immediately, orphaning
  pendingEdit and stranding the placeholder).
- Added bullet for fallback streaming's `remend`'d final SentMessage
  content vs upstream's raw `accumulated`.

No code changes. Round-5 verdict on code: clean across all 7
principles. Only doc bookkeeping was missing.

https://claude.ai/code/session_01XE1bMoQ5BjCvgi1iLGKKhG
@patrick-chinchill patrick-chinchill marked this pull request as draft April 18, 2026 05:16
@patrick-chinchill patrick-chinchill marked this pull request as ready for review April 18, 2026 05:16
@patrick-chinchill patrick-chinchill marked this pull request as draft April 18, 2026 07:17
@patrick-chinchill patrick-chinchill marked this pull request as ready for review April 18, 2026 07:17
@patrick-chinchill patrick-chinchill marked this pull request as draft April 23, 2026 02:10
@patrick-chinchill patrick-chinchill marked this pull request as ready for review April 23, 2026 02:10
@patrick-chinchill patrick-chinchill merged commit b1a9562 into main Apr 23, 2026
13 checks passed
@patrick-chinchill patrick-chinchill deleted the claude/port-upstream-4.26-b5zzY branch April 23, 2026 04:02
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

3 participants