Skip to content

fix(linear): stable agent-session thread ids, rootless sessions, undetermined comment mentions (#232) - #280

Merged
patrick-chinchill merged 4 commits into
mainfrom
sync/4.41-l1
Oct 1, 2026
Merged

patrick-chinchill merged 4 commits into
mainfrom
sync/4.41-l1

Conversation

@patrick-chinchill

@patrick-chinchill patrick-chinchill commented Oct 1, 2026 •

Copy link
Copy Markdown
Collaborator

Summary

Linear agent sessions now route every event to one stable thread, linear:{issueId}:s:{agentSessionId}. Before this change, each source comment produced its own …:c:{comment}:s:{session} thread.

  • Webhook, created: a session without a root comment is dispatched with a synthetic comment {id: "agent-session-{sessionId}", body: promptContext ?? ""}. It used to be dropped. A session with no creator is authored by {type: "bot", id: "linear-automation", displayName/fullName: "Linear automation"}. It used to be authored as the bot itself, which made is_me true, so core dropped it as a self-message.
  • Webhook, prompted: a prompt without sourceCommentId uses agentActivity.id as the message id. It used to be dropped with "Missing source comment ID".
  • Fetch: when a session has no root comment, _fetch_agent_session_messages now calls the new _fetch_agent_session_activities instead of raising AdapterError("… missing a root comment"). That method pages agentSession.activities (first/after forward, last/before otherwise, default 50), sorts by parsed createdAt and renders each activity with _render_activity. Prompts are authored as the user and every other activity as the bot. next_cursor is endCursor (forward + hasNextPage) or startCursor (backward + hasPreviousPage).
  • Emit: post_message / stream return the stable session thread id.
  • parse_message: the agent_session_comment kind delegates to _parse_agent_session_message (is_mention=True, stable thread). Ordinary comments leave is_mention unset (None), so core @mention text detection from [4.41/C2a] Core mentions & message model: tri-state is_mention, mention regex, Author.email/is_system, Message.reply_to #192 still runs on them.
  • decode_thread_id / encode_thread_id are unchanged. The old :c:{c}:s:{s} form still decodes, so posting to a stored old id still reaches its session.

Upstream commits mapped

Upstream Python
3d2cb22a fix(linear): stabilize agent session event threads (#885, chat@4.40.0) 1. parseMessage stable id → _parse_agent_session_message, emit _raw_message_from_source_comment. 2. sourceCommentId ?? activity.id → prompted branch. 3. Synthetic root comment → created branch. 4. linear-automation author → created branch. 5. fetchAgentSessionActivities + renderActivity → _fetch_agent_session_activities + _AGENT_SESSION_ACTIVITIES_QUERY + _render_activity.
fcdc1c9e fix(chat): honor adapter-reported non-mentions (#946, chat@4.41.0), Linear half parse_message: True for agent-session comments, unset for ordinary comments. The core tri-state landed in #192.

Verify-first: Linear schema (raw GraphQL)

I checked the activities query field by field against Linear's published schema.graphql, the same copy used to harden L4/L5 (#231):

  • AgentSession.activities(after: String, before: String, first: Int, last: Int, …): AgentActivityConnection!
  • AgentActivity has no scalar sourceCommentId or userId. Only the relations sourceComment: Comment and user: User! exist; the SDK derives the ids from them. The query therefore selects sourceComment { id } and user { id displayName name email avatarUrl } inline, in one query with no per-activity follow-ups.
  • content: AgentActivityContent! is a union of AgentActivity{Prompt,Response,Thought,Error,Elicitation}Content (each has type, body: String!) and AgentActivityActionContent (type, action: String!, parameter: String!, result: String). The query selects these with inline fragments. AgentActivityType values are lowercase (prompt, …).
  • PageInfo has hasNextPage, hasPreviousPage, endCursor and startCursor.
  • AgentSession.url: String was added to the session query; the activities path copies it onto each message, as upstream does.
  • The same live-tenant caveat as the existing L4/L5 rows applies: this was checked against the published schema, not against a live agent-session tenant.

Tests ported

packages/adapter-linear/src/index.test.ts is not fidelity-mapped (#78). Ported by name:

  • tests/test_linear_webhook.py: "routes prompted events to the same stable session thread", "routes prompted events without a source comment", "uses an automation author when a created session has no creator", "dispatches created sessions without a root comment". The upstream payload fixture's comment: null override and the activity sourceCommentId/user were mirrored.
  • tests/test_linear_agent_session_fetch.py: "should fetch activities for agent sessions without a root comment" ({id, last: 50}, texts User prompt / Searching: Chat SDK\nFound documentation / Agent response). Also the issue-public + comment: null case of the it.each "validates agent session ownership through the Linear SDK", which now resolves to messages=[], next_cursor=None through the activities query with two requests over the real _graphql_query.
  • tests/test_linear_adapter.py: "should mark an agent session comment as a mention", "should leave isMention undetermined for an ordinary comment".

Python-specific tests:

  • Forward and backward cursor forwarding (after/before), omission of an empty cursor, and the next cursor per direction (parametrized).
  • _render_activity: an action that is empty after trimming renders "Action"; a blank parameter or result is dropped; the JS .trim() set strips U+3000 and the BOM but keeps U+001C and U+0085.
  • Author/id fallbacks for activities with no user (prompt → "unknown", other → bot id and user_name), and sourceComment.id ?? activity.id.
  • An old-form linear:{i}:c:{c}:s:{s} thread id still posts to its session (emit tests).

Existing tests that asserted the per-comment session thread id were updated in place: the fetch happy path, the dispatch-ordering test, the webhook created/routed/nested-issue tests, and the emit post/stream tests. The tests for removed drop paths ("Missing source comment ID", "Missing comment for agent session", "missing a root comment") were replaced by the ported tests above.

15 targeted mutations of the new code were all caught by the suite (str.strip vs JS trim, the "Action" fallback, after↔before, an unforwarded cursor, no sort, inverted is_bot, the wrong next cursor per direction, the automation id, dropping prompted/created events again, no parse_message delegation, ignoring sourceComment, raising on rootless sessions, the display-name fallback).

Fidelity

TS_ROOT=…/vercel-chat-4.41.1 uv run python scripts/verify_test_fidelity.py --report-target:

Delta vs committed report (HEAD): missing 198 -> 198 (+0)

The count is unchanged because adapter-linear is not mapped (#78), so scripts/fidelity_target.json is unchanged.

Divergences

None new. The activities query sends only the variables upstream passes ({id, last: 50} by default), so unused bounds are omitted rather than sent as null. That matches the SDK call. The UPSTREAM_SYNC.md L4 and L5 rows are updated (stable ids, activities query), and so is the #192 mention section's Linear note.

Consumer impact

  • BREAKING for Linear agent-session users: session messages move from linear:{i}:c:{c}:s:{s} to linear:{i}:s:{s}. Subscriptions and state keyed by old ids will not match new events. Re-subscribe, or map stored ids by dropping the :c:{comment} segment. Posting to a stored old id still works. A side effect consumers will see: a thread subscribed on the created event now receives the follow-up prompted events through on_subscribed_message, because they share the thread.
  • Automation-created sessions (no creator) now reach handlers.
  • Ordinary Linear comments: is_mention stays None, so text detection applies. This matches the behavior before [4.41/C2a] Core mentions & message model: tri-state is_mention, mention regex, Author.email/is_system, Message.reply_to #192 for these messages.
  • None for Slack/Teams or the other adapters.

Validation

Full validation (ruff check, ruff format --check, audit_test_quality, verify_test_fidelity.py --check-docs, strict fidelity at 4.31.0 733/733, pytest) is green: 6636 passed, 24 skipped. pyrefly check: 0 errors.

Closes #232
Part of #184

Review

gpt-6-astra Codex review round 1 on 89b35dd: PASS ("No actionable regressions found ... the new GraphQL selections match Linear's published schema."). CI on the draft: test (3.12/3.13), CodeQL green; Lint & Type Check runs once the PR is out of draft (ruff, audit, fidelity --check-docs/--strict and pyrefly were run locally, all green).

Merge gate

Final HEAD: 13b7017. This is 6a26365, which adds the review test fixes, plus a merge of origin/main. The merge was clean and fidelity_target.json did not conflict.

Independent review (4 findings, 3 fixed and 1 handled as documentation):

  • Fixed (tests): nothing pinned is_mention is None on the paths core routes. Two assertions were added: one to the comment-mode webhook dispatch test (test_linear_webhook.py::test_comment_still_dispatched_in_comment_mode) and one to the comment-thread fetch test (test_linear_extended.py::test_validates_comment_ownership_through_the_transport). The mutation is_mention=False is now caught at both _build_message and _comment_node_to_message.
  • Fixed (tests): the activities actor ?? chains were not pinned. The ported activities test now asserts user_name/full_name == ("Alice", "Alice Smith") and the raw email. It also gives the prompt activity a distinct updatedAt and asserts edited == [True, False, False]. The mutations displayName↔name, updatedAt-from-createdAt and dropped-email are all caught.
  • Fixed (tests): the promptContext ?? "" fallback was not pinned. test_dispatches_created_sessions_without_a_root_comment is now parametrized with a no-promptContext case that asserts text == "", and the body: None mutation is caught.
  • Out of scope, documented and tracked: ordinary-comment parse_message returns thread_id="" and an "unknown" author. This gap predates the PR and is not on a routed path: core never calls parse_message. The docstring is narrowed so the parity claim covers only the agent-session branch. A known-divergence row was added to docs/UPSTREAM_SYNC.md (Platform-specific gaps), and follow-up Linear: parse_message for ordinary comments returns empty thread id and unknown author (upstream parity) #285 was opened. No sibling issue under [4.41] Tracking: sync upstream chat@4.31.0 → chat@4.41.1 (0.4.41 wave) #184 owned it.

gpt-6-astra: 1 round on 13b7017, verdict PASS ("No actionable regressions found. All 303 Linear tests passed, targeted lint checks passed, and the new GraphQL selections match Linear's published schema. Live-tenant behavior was not verified."). The earlier draft round on 89b35dd was also PASS.

Bots: CodeRabbit skipped the draft. A manual @coderabbitai review after ready-for-review came back as "Review rate limited", so there are no CodeRabbit or gemini review comments to address.

CI on 13b7017: test (3.12), test (3.13), CodeQL and Analyze (actions/python) are green. The PR-triggered Lint & Type Check was skipped because the push landed while the PR was still a draft (the ready_for_review run was cancelled by concurrency). A workflow_dispatch Lint & Type Check run on the same SHA was green: https://github.com/Chinchill-AI/chat-sdk-python/actions/runs/36820135415.

Local full validation on 13b7017: ruff check, ruff format --check, audit_test_quality (0 hard failures), verify_test_fidelity.py --check-docs, strict fidelity at 4.31.0 (all TS tests matched) and pytest (6689 passed, 24 skipped) all pass. pyrefly check reports 0 errors. --report-target at 4.41.1 gives Delta vs committed report (HEAD): missing 167 -> 167 (+0), so fidelity_target.json is unchanged.

…termined comment mentions (#232)

Ports vercel/chat 3d2cb22a (#885, chat@4.40.0) and the Linear half of
fcdc1c9e (#946, chat@4.41.0).

- Every agent-session message uses linear:{issue}:s:{session} (webhook
  created/prompted, fetch, emit). Old :c:{comment}:s: ids still decode.
- Prompts without a source comment use the activity id; sessions created
  without a root comment are dispatched with a synthetic comment built
  from the prompt context; creator-less sessions are authored by a
  distinct 'Linear automation' bot instead of the bot itself.
- fetch_messages on a rootless session reads agentSession.activities
  (first/after forward, last/before otherwise) via a schema-checked raw
  GraphQL query, rendering actions like upstream renderActivity.
- parse_message delegates the agent_session_comment kind; ordinary
  comments leave is_mention unset.
@coderabbitai

coderabbitai Bot commented Oct 1, 2026 •

Copy link
Copy Markdown

Warning

Review limit reached

You've used all free OSS reviews for now. Wait for the free limit to reset to keep reviewing this public repository.

Next included review available in 13 minutes.

Check out review usage here.

View limit details

Limit details: You’ve used the included review currently available.

Learn how review limits work.

Review configuration:

⚙️ Run configuration

Configuration used: Repository UI

Review profile: CHILL

Plan: Advanced

Run ID: 08c724cf-eb6d-4bf7-82bf-304871b6963f

📥 Commits

Reviewing files that changed from the base of the PR and between c75418f and 2efc1b5.

📒 Files selected for processing (8)
  • CHANGELOG.md
  • docs/UPSTREAM_SYNC.md
  • src/chat_sdk/adapters/linear/adapter.py
  • tests/test_linear_adapter.py
  • tests/test_linear_agent_session_emit.py
  • tests/test_linear_agent_session_fetch.py
  • tests/test_linear_extended.py
  • tests/test_linear_webhook.py
  • Autopilot · Keep fixing CodeRabbit findings and required CI, and resolving merge conflicts

Autopilot is currently an internal CodeRabbit preview.


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.

@patrick-chinchill
patrick-chinchill marked this pull request as ready for review October 1, 2026 05:28
@patrick-chinchill

Copy link
Copy Markdown
Collaborator Author

@coderabbitai review

@coderabbitai

coderabbitai Bot commented Oct 1, 2026 •

Copy link
Copy Markdown
⚠️ Action not completed

Review rate limited.

Note: CodeRabbit is an incremental review system and does not re-review already reviewed commits. This command is applicable only when automatic reviews are paused.

@patrick-chinchill

Copy link
Copy Markdown
Collaborator Author

Merge gate: CI green (test 3.12, test 3.13, Lint & Type Check, CodeQL, Analyze python/actions) on 2efc1b5; local Codex review (gpt-6-astra, xhigh, --base origin/main) on 13b7017: "No actionable regressions found. All 303 Linear tests passed, targeted lint checks passed, and the new GraphQL selections match Linear's published schema. Live-tenant behavior was not verified."; 2 astra rounds (both clean). 2efc1b5 is a clean, conflict-free merge of origin/main (#272/#229/#273/#281, none touching Linear); the PR's src/tests/scripts delta is line-identical to the reviewed 13b7017, so no re-review was needed. Local full validation green (6884 passed, strict fidelity, pyrefly 0 errors; fidelity target unchanged at 151 missing). CodeRabbit rate-limited (no review). Merging with --admin (Protect Main requires a code-owner approval).

@patrick-chinchill
patrick-chinchill merged commit 570c95d into main Oct 1, 2026
7 checks passed
@patrick-chinchill
patrick-chinchill deleted the sync/4.41-l1 branch October 1, 2026 05:54
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.

[4.41/L1] Linear: stable agent-session thread ids, undetermined mentions for ordinary comments

1 participant