Skip to content

feat(linear): agent-session thread-id encode/decode (chat@4.31/#151 — L2/5)#170

Merged
patrick-chinchill merged 1 commit into
mainfrom
feat/4.31-linear-agent-sessions-l2-threadid
Jun 20, 2026
Merged

feat(linear): agent-session thread-id encode/decode (chat@4.31/#151 — L2/5)#170
patrick-chinchill merged 1 commit into
mainfrom
feat/4.31-linear-agent-sessions-l2-threadid

Conversation

@patrick-chinchill

Copy link
Copy Markdown
Collaborator

L2/5 — Linear agent-session THREAD-ID encode/decode

Second PR of Wave D (#151, tracked under #152). Builds on L1's types (LinearAgentSessionThreadId, LinearThreadId.agent_session_id). Faithful port of the agent-session thread-id forms from upstream adapter-linear/src/index.ts + utils.ts (vercel/chat 4.31). The adapter-linear thread-id source is byte-identical between upstream 4.30 and 4.31; this lands it in the 4.31 wave.

Thread-id forms

form wire string
issue-level (existing) linear:{issueId}
comment-thread (existing) linear:{issueId}:c:{commentId}
agent-session issue (new) linear:{issueId}:s:{sessionId}
agent-session comment (new) linear:{issueId}:c:{commentId}:s:{sessionId}

The 3 anchored regexes (exact upstream ports)

COMMENT_SESSION_THREAD_PATTERN = ^([^:]+):c:([^:]+):s:([^:]+)$
COMMENT_THREAD_PATTERN         = ^([^:]+):c:([^:]+)$
ISSUE_SESSION_THREAD_PATTERN   = ^([^:]+):s:([^:]+)$

All explicitly anchored with ^...$ so .match() is a full match.

Decode order (load-bearing, exact upstream)

COMMENT_SESSIONISSUE_SESSIONCOMMENT → bare-issue (most-specific first).

encode

When agent_session_id is present, emits the :s: session form (comment-session if a comment_id is also set, else issue-session); otherwise unchanged.

assert_agent_session_thread

Ported from utils.ts. Raises ValidationError("linear", "Expected a Linear agent session thread") (byte-for-byte upstream message) when agent_session_id is absent; returns the thread re-typed as LinearAgentSessionThreadId.

CRITICAL — cross-SDK state compat

The issue-level and comment-thread encode outputs are persisted (Redis/Postgres) and shared with the TS SDK; they stay byte-identical. Verified by a round-trip and an explicit byte-identical guard test. The :s: forms are new (no existing persisted data). The malformed linear:x:s: input falls through to bare-issue x:s: — byte-parity with upstream confirmed via the TS regexes.

Scope (strict)

Thread-id encode/decode + the 3 anchored regexes + assert_agent_session_thread + tests ONLY. No agent-session webhook routing (L3), emit (L4), or fetch (L5). CHANGELOG untouched.

What downstream waves consume

  • L3 (routing): decode_thread_id + assert_agent_session_thread to route AgentSessionEvent webhooks to the session thread.
  • L4 (emit): encode_thread_id to address agent-activity posts back to the session.
  • L5 (fetch): the decoded agent_session_id for session-scoped fetch.

Tests + mutation proof

All four forms (encode/decode + an encode→decode→encode identity property test across all forms), decode-order guards (a comment-session id is NOT mis-decoded as comment; an issue-session id is NOT mis-decoded as bare-issue), the malformed linear:x:s: edge, and assert_agent_session_thread accept/raise. Mutation-checked locally: dropping the COMMENT_SESSION decode branch, dropping the session encode branch, and un-anchoring + reordering COMMENT each fail the suite.

Gauntlet

  • ruff check ✅ · ruff format --check
  • audit_test_quality.py ✅ (0 hard failures; no new duplicates)
  • pyrefly check src/ → 0 errors · test file → 0 errors
  • verify_test_fidelity.py → 732/732 ✅
  • pytest tests/ → 5125 passed, 4 skipped (Linear: 185 passed)

Refs #151 #152

🤖 Generated with Claude Code

… L2/5)

Port the Linear agent-session thread-id forms from upstream
adapter-linear/src/index.ts (4.31). Adds two new thread-id shapes
alongside the existing issue-level and comment-thread forms:

- Agent-session issue:   linear:{issueId}:s:{sessionId}
- Agent-session comment: linear:{issueId}:c:{commentId}:s:{sessionId}

Three anchored patterns (faithful ^...$ ports) are tried most-specific
first in decode — COMMENT_SESSION -> ISSUE_SESSION -> COMMENT -> bare —
matching upstream order exactly. encode emits the :s: session form when
agent_session_id is present. assert_agent_session_thread (ported from
utils.ts) raises ValidationError("linear", "Expected a Linear agent
session thread") on non-session threads and returns the narrowed type.

CRITICAL cross-SDK state compat: the issue-level and comment-thread
encode outputs are persisted (Redis/Postgres) shared with the TS SDK and
stay byte-identical — verified by a round-trip and an explicit
byte-identical guard test. The :s: forms are new (no existing data).

Tests: encode/decode for all four forms, an all-forms encode->decode->
encode identity property test, decode-order guards (a comment-session id
is NOT mis-decoded as comment; an issue-session id is NOT mis-decoded as
bare issue), the malformed `linear:x:s:` edge (falls to bare issue, byte
parity with upstream), and assert_agent_session_thread accept/raise.
Mutation-checked: dropping the COMMENT_SESSION decode branch, dropping
the session encode branch, and un-anchoring COMMENT each fail the suite.

Scope is thread-id only — webhook routing (L3), emit (L4), and fetch (L5)
are out of scope. L3 consumes decode_thread_id + assert_agent_session_
thread to route AgentSessionEvent webhooks; L4 consumes encode_thread_id
to address activity posts back to the session; L5 consumes the decoded
agent_session_id for session-scoped fetch.

Refs #151 #152
@coderabbitai

coderabbitai Bot commented Jun 20, 2026

Copy link
Copy Markdown

Warning

Review limit reached

@patrick-chinchill, we couldn't start this review because you've reached your PR review rate limit.

More reviews will be available in 21 minutes and 49 seconds. Learn how PR review limits work.

Your organization has used up its prepaid credits, and credit purchases are no longer available. Enable the review add-on in the billing tab to keep reviews running — you're only billed for reviews past your plan's rate limits ($0.25/file).

⌛ How to resolve this issue?

After more reviews become available, a review can be triggered using the @coderabbitai review command as a PR comment. Alternatively, push new commits to this PR.

To avoid repeated limits, reduce automatic review volume by pausing incremental auto-reviews earlier, using label-based review opt-in, excluding WIP or generated PR titles, or requesting reviews manually when the PR is ready. If your team needs uninterrupted high-volume reviews, an organization admin can enable usage-based credits.

🚦 How do rate limits work?

CodeRabbit enforces per-developer PR review limits for each organization. Most developers receive the normal plan refill rate.

For paid Pro and Pro+ PR reviews, CodeRabbit uses adaptive limits for sustained high-volume activity. When a developer's recent PR review activity reaches the 95th percentile or higher among CodeRabbit users, the refill rate gradually slows as usage increases. The highest same-day bursts are limited more strictly.

Please see our Fair Usage Limits Policy for further information.

ℹ️ Review info
⚙️ Run configuration

Configuration used: Repository UI

Review profile: CHILL

Plan: Pro Plus

Run ID: a70fc76f-fc06-498a-b25f-385d2f9af4d9

📥 Commits

Reviewing files that changed from the base of the PR and between 8185f57 and 9b6809a.

📒 Files selected for processing (3)
  • src/chat_sdk/adapters/linear/__init__.py
  • src/chat_sdk/adapters/linear/adapter.py
  • tests/test_linear_adapter.py

Thanks for using CodeRabbit! It's free for OSS, and your support helps us grow. If you like it, consider giving us a shout-out.

❤️ Share

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

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

Copy link
Copy Markdown
Contributor

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 introduces support for agent-session thread IDs in the Linear adapter, adding new encoding/decoding patterns and the assert_agent_session_thread utility along with comprehensive tests. The feedback suggests improving runtime type safety in assert_agent_session_thread by instantiating a real LinearAgentSessionThreadId object instead of using typing.cast, explicitly checking for None instead of using a truthiness check, and updating the corresponding test assertions to verify the runtime type.

Important

The consumer version of Gemini Code Assist on GitHub is being sunset. Starting June 18, 2026, new organization installations will be blocked, and all code review activity will officially cease on July 17, 2026.
For more details on the timeline and next steps, please review the Help Documentation.

Comment on lines +129 to +131
if not thread.agent_session_id:
raise ValidationError("linear", "Expected a Linear agent session thread")
return cast("LinearAgentSessionThreadId", thread)

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

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

medium

In Python, typing.cast is a static analysis helper and does not perform any runtime type conversion. Since LinearAgentSessionThreadId is a distinct subclass of LinearThreadId, using cast means the returned object is still a LinearThreadId instance at runtime. Any subsequent runtime checks (such as isinstance(thread, LinearAgentSessionThreadId)) will fail. To ensure runtime type safety and correct behavior, we should instantiate and return a real LinearAgentSessionThreadId object. Additionally, ensure we check if agent_session_id is None explicitly rather than using a truthiness check, to avoid issues if an empty string is a valid ID.

    if thread.agent_session_id is None:
        raise ValidationError("linear", "Expected a Linear agent session thread")
    return LinearAgentSessionThreadId(
        issue_id=thread.issue_id,
        comment_id=thread.comment_id,
        agent_session_id=thread.agent_session_id,
    )
References
  1. When checking for optional values that can be falsy but valid (e.g., 0, empty string, empty list), use is not None instead of a truthiness check to avoid silently ignoring them.

Comment on lines +391 to +397
def test_returns_narrowed_thread_for_session_id(self):
# A thread carrying agent_session_id is accepted and returned re-typed.
thread = LinearThreadId(issue_id="issue-1", agent_session_id="sess-1")
narrowed = assert_agent_session_thread(thread)
assert isinstance(narrowed, LinearThreadId)
assert narrowed.agent_session_id == "sess-1"
assert narrowed.issue_id == "issue-1"

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

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

medium

Update the test assertion to verify that the returned object is indeed an instance of LinearAgentSessionThreadId at runtime, aligning with the improved implementation of assert_agent_session_thread.

Suggested change
def test_returns_narrowed_thread_for_session_id(self):
# A thread carrying agent_session_id is accepted and returned re-typed.
thread = LinearThreadId(issue_id="issue-1", agent_session_id="sess-1")
narrowed = assert_agent_session_thread(thread)
assert isinstance(narrowed, LinearThreadId)
assert narrowed.agent_session_id == "sess-1"
assert narrowed.issue_id == "issue-1"
def test_returns_narrowed_thread_for_session_id(self):
# A thread carrying agent_session_id is accepted and returned re-typed.
thread = LinearThreadId(issue_id="issue-1", agent_session_id="sess-1")
narrowed = assert_agent_session_thread(thread)
assert isinstance(narrowed, LinearAgentSessionThreadId)
assert narrowed.agent_session_id == "sess-1"
assert narrowed.issue_id == "issue-1"

@patrick-chinchill patrick-chinchill marked this pull request as ready for review June 20, 2026 06:16
@patrick-chinchill patrick-chinchill merged commit 8a819f4 into main Jun 20, 2026
9 checks passed

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

Copy link
Copy Markdown

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: 9b6809afa1

ℹ️ 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".

they MUST stay byte-identical to the prior forms. The ``:s:`` session
forms are new (no existing persisted data).
"""
if platform_data.agent_session_id:

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 Prevent session IDs from falling through to comment APIs

With this new branch, any caller can obtain a linear:{issue}:s:{session} or linear:{issue}:c:{comment}:s:{session} thread ID, but the existing public methods that immediately call decode_thread_id are not session-aware: post_message still sends commentCreate with only decoded.issue_id/decoded.comment_id, and fetch_messages falls back to issue/comment fetches. In agent-session mode, a bot reply or fetch against the newly encoded ID will therefore hit the visible issue comments instead of the Linear agent session. Either keep these IDs rejected by those APIs until L4/L5 lands, or add the session-specific handling at the same time.

Useful? React with 👍 / 👎.

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.

1 participant