From 1c236da3d5cb3263bbce6b91e206fd5347578572 Mon Sep 17 00:00:00 2001 From: Khaliq Date: Thu, 15 Oct 2026 10:16:11 -0700 Subject: [PATCH 1/4] fix(devin): skip review-category sessions in remote capture Devin Review and push-review automations run inside the org as automation-origin sessions, so every code review Cognition runs lands in the catalog as if a person drove it. The listing's `category` field is the only marker that names the work; sessions classified code_review or code_review_and_analysis no longer map. Generated with [Devin](https://devin.ai) Co-Authored-By: Devin <158243242+devin-ai-integration[bot]@users.noreply.github.com> --- CHANGELOG.md | 1 + .../rust/src/remote/devin_cloud.rs | 16 +++++++++- .../provider-sources/rust/src/remote/tests.rs | 29 +++++++++++++++++++ 3 files changed, 45 insertions(+), 1 deletion(-) diff --git a/CHANGELOG.md b/CHANGELOG.md index 79b3ce41..fc8dd87d 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -33,6 +33,7 @@ This project follows [Semantic Versioning](https://semver.org/spec/v2.0.0.html). ### Fixed +- Devin remote capture skips sessions the v3 listing classifies as `code_review` or `code_review_and_analysis` — Cognition's Devin Review / push-review automations — so org review traffic no longer fills the catalog. - Two simultaneous first hydrations of one local session both succeed; the slower one no longer fails `CONNECTOR_NOT_CONFIGURED` when the other records the session's local observation mid-lookup. - Codex rollouts that Codex has compressed to `rollout-*.jsonl.zst` are discovered, synced, hydrated and fed to the change feed as evidence under their `.jsonl` locator; a rollout compressed after it was indexed keeps its session, locator and cursor and is not ingested again. - The first sync of a new store records each Codex rollout's cursor, so the rollout's next append resumes instead of re-reading it from the start. diff --git a/plugins/provider-sources/rust/src/remote/devin_cloud.rs b/plugins/provider-sources/rust/src/remote/devin_cloud.rs index 109d0126..62714063 100644 --- a/plugins/provider-sources/rust/src/remote/devin_cloud.rs +++ b/plugins/provider-sources/rust/src/remote/devin_cloud.rs @@ -168,10 +168,24 @@ fn ensure_devin_status(what: &str, scope: &str, status: u16, body: &str) -> Resu } } +/// `category` values the v3 listing assigns to code-review work. Cognition's +/// review sessions — Devin Review and push-review automations — run inside the +/// org as `automation`-origin sessions, so `origin` cannot tell them apart from +/// other automations; the classifier's category is the listing's only field +/// that names the work itself. +const REVIEW_CATEGORIES: [&str; 2] = ["code_review", "code_review_and_analysis"]; + /// Map one session object from `GET /v3/organizations/{org}/sessions` into a /// catalog row. The listing's `title` is the only human-readable identifier -/// the endpoint offers; `None` for a malformed id. +/// the endpoint offers; `None` for a malformed id or a review session, which +/// is an automation artifact rather than work a person directed. pub(super) fn map_devin_session(value: &Value) -> Option<(Candidate, ShallowSession)> { + if REVIEW_CATEGORIES + .iter() + .any(|category| string_field(value, "category").as_deref() == Some(category)) + { + return None; + } let id = string_field(value, "session_id")?; let id = devin_session_hex(&id)?.to_string(); let created_ms = value diff --git a/plugins/provider-sources/rust/src/remote/tests.rs b/plugins/provider-sources/rust/src/remote/tests.rs index 899cf0ab..eaed2ac5 100644 --- a/plugins/provider-sources/rust/src/remote/tests.rs +++ b/plugins/provider-sources/rust/src/remote/tests.rs @@ -1019,6 +1019,35 @@ fn devin_mapping_stamp_tracks_the_mapped_listing_fields() { assert_eq!(base.0.stamp, identical.0.stamp); } +#[test] +fn devin_mapping_skips_review_sessions_and_keeps_uncategorized_work() { + // Devin Review and push-review automations are `automation`-origin + // sessions the classifier files under the review categories; they carry + // no conversational history worth keeping. + for category in ["code_review", "code_review_and_analysis"] { + let mut entry = devin_session(DEVIN_SESSION_ID, "Review this push"); + entry["category"] = serde_json::json!(category); + assert!( + map_devin_session(&entry).is_none(), + "{category} must not map" + ); + } + // Uncategorized and non-review sessions still map: `category` is only + // populated once Cognition's classifier has run. + for category in [ + serde_json::Value::Null, + serde_json::json!("bug_fixing"), + serde_json::json!("other"), + ] { + let mut entry = devin_session(DEVIN_SESSION_ID, "t"); + entry["category"] = category.clone(); + assert!( + map_devin_session(&entry).is_some(), + "{category} must still map" + ); + } +} + #[test] fn devin_mapping_skips_malformed_ids_and_normalizes_the_prefixed_form() { for bad_id in [ From f4dd9c9796aa709e2051ca784640953b66088cdb Mon Sep 17 00:00:00 2001 From: Khaliq Date: Thu, 15 Oct 2026 21:59:44 -0700 Subject: [PATCH 2/4] fix(devin): scope the review filter to automation sessions Two review findings: a person-started session the classifier files as code_review must still catalog, so the skip now requires origin=="automation" alongside the category; and pages where every item was filtered still consumed the 100-page bound, starving limited discovery in review-dense orgs, so the bound now counts only pages that yielded rows while a larger absolute fetch cap keeps enumeration terminating. Generated with [Devin](https://devin.ai) Co-Authored-By: Devin <158243242+devin-ai-integration[bot]@users.noreply.github.com> --- .agentworkforce/relay/broker-kjg-lap.lock | 0 .agentworkforce/relay/pending-cleanups.json | 1 + .../relay/pending-cleanups.json.lock | 1 + .../rust/src/remote/devin_cloud.rs | 27 +++++++--- .../provider-sources/rust/src/remote/tests.rs | 50 +++++++++++++++++++ 5 files changed, 71 insertions(+), 8 deletions(-) create mode 100644 .agentworkforce/relay/broker-kjg-lap.lock create mode 100644 .agentworkforce/relay/pending-cleanups.json create mode 100644 .agentworkforce/relay/pending-cleanups.json.lock diff --git a/.agentworkforce/relay/broker-kjg-lap.lock b/.agentworkforce/relay/broker-kjg-lap.lock new file mode 100644 index 00000000..e69de29b diff --git a/.agentworkforce/relay/pending-cleanups.json b/.agentworkforce/relay/pending-cleanups.json new file mode 100644 index 00000000..fe51488c --- /dev/null +++ b/.agentworkforce/relay/pending-cleanups.json @@ -0,0 +1 @@ +[] diff --git a/.agentworkforce/relay/pending-cleanups.json.lock b/.agentworkforce/relay/pending-cleanups.json.lock new file mode 100644 index 00000000..67c2e8e5 --- /dev/null +++ b/.agentworkforce/relay/pending-cleanups.json.lock @@ -0,0 +1 @@ +agent-relay-cleanup-lock v2 diff --git a/plugins/provider-sources/rust/src/remote/devin_cloud.rs b/plugins/provider-sources/rust/src/remote/devin_cloud.rs index 62714063..e2c7021b 100644 --- a/plugins/provider-sources/rust/src/remote/devin_cloud.rs +++ b/plugins/provider-sources/rust/src/remote/devin_cloud.rs @@ -170,19 +170,26 @@ fn ensure_devin_status(what: &str, scope: &str, status: u16, body: &str) -> Resu /// `category` values the v3 listing assigns to code-review work. Cognition's /// review sessions — Devin Review and push-review automations — run inside the -/// org as `automation`-origin sessions, so `origin` cannot tell them apart from -/// other automations; the classifier's category is the listing's only field -/// that names the work itself. +/// org as `automation`-origin sessions, so a review category only skips a row +/// when automation spawned it: a person-directed session whose work Cognition +/// classifies as review still belongs in the catalog. const REVIEW_CATEGORIES: [&str; 2] = ["code_review", "code_review_and_analysis"]; +/// The absolute fetch bound on one session-list enumeration. Pages where +/// every item is filtered out — review automations — still count toward this +/// backstop, so the walk always terminates; only yielding pages count toward +/// `MAX_LIST_PAGES`, the shared bound that bounds catalogable results. +const MAX_LIST_FETCHES: usize = MAX_LIST_PAGES * 4; + /// Map one session object from `GET /v3/organizations/{org}/sessions` into a /// catalog row. The listing's `title` is the only human-readable identifier /// the endpoint offers; `None` for a malformed id or a review session, which /// is an automation artifact rather than work a person directed. pub(super) fn map_devin_session(value: &Value) -> Option<(Candidate, ShallowSession)> { - if REVIEW_CATEGORIES - .iter() - .any(|category| string_field(value, "category").as_deref() == Some(category)) + if string_field(value, "origin").as_deref() == Some("automation") + && REVIEW_CATEGORIES + .iter() + .any(|category| string_field(value, "category").as_deref() == Some(category)) { return None; } @@ -481,7 +488,8 @@ impl ShallowSessionProvider for DevinCloudProvider { let mut candidates = Vec::new(); let mut rows = BTreeMap::new(); let mut cursor: Option = None; - for _ in 0..MAX_LIST_PAGES { + let mut yielding_pages = 0usize; + for _ in 0..MAX_LIST_FETCHES { let page_limit = match self.limit { Some(limit) => limit .saturating_sub(candidates.len()) @@ -497,15 +505,18 @@ impl ShallowSessionProvider for DevinCloudProvider { &response.body, )?; let page = parse_devin_page(&response.body, "session list")?; + let before = candidates.len(); for item in &page.items { if let Some((candidate, session)) = map_devin_session(item) { rows.insert(candidate.locator.clone(), session); candidates.push(candidate); } } + yielding_pages += usize::from(candidates.len() > before); cursor = page.end_cursor.filter(|_| page.has_next_page); let done = cursor.is_none() || page.items.is_empty() + || yielding_pages >= MAX_LIST_PAGES || self.limit.is_some_and(|limit| candidates.len() >= limit); if done { break; @@ -516,7 +527,7 @@ impl ShallowSessionProvider for DevinCloudProvider { // caller-specified limit that was reached is not truncation. anyhow::ensure!( cursor.is_none() || self.limit.is_some_and(|limit| candidates.len() >= limit), - "EVIDENCE_PARTIAL: Devin session list exceeded the 100-page bound" + "EVIDENCE_PARTIAL: Devin session list exceeded the page bound" ); *self.fetched.lock().expect("remote connector row cache") = rows; Ok(candidates) diff --git a/plugins/provider-sources/rust/src/remote/tests.rs b/plugins/provider-sources/rust/src/remote/tests.rs index eaed2ac5..73e28814 100644 --- a/plugins/provider-sources/rust/src/remote/tests.rs +++ b/plugins/provider-sources/rust/src/remote/tests.rs @@ -1046,6 +1046,18 @@ fn devin_mapping_skips_review_sessions_and_keeps_uncategorized_work() { "{category} must still map" ); } + // The review filter is scoped to automation-spawned sessions: a + // person-directed session whose work the classifier files as review + // still belongs in the catalog. + for origin in ["webapp", "api", "desktop", "cli"] { + let mut entry = devin_session(DEVIN_SESSION_ID, "Review my PR"); + entry["origin"] = serde_json::json!(origin); + entry["category"] = serde_json::json!("code_review"); + assert!( + map_devin_session(&entry).is_some(), + "{origin} review work must still map" + ); + } } #[test] @@ -1103,6 +1115,44 @@ fn devin_credentials_parse_the_flat_toml() { assert!(devin_org_id(&config).is_none()); } +#[test] +fn devin_enumeration_keeps_walking_past_review_only_pages() { + // An org whose recent history is dense with Devin Review automations + // must not exhaust the page bound on filtered sessions: pages that yield + // nothing do not count toward it, while an absolute fetch bound still + // terminates the walk. + let _cleared = without_credentials_override(); + let home = tempfile::tempdir().unwrap(); + let mut review = devin_session(DEVIN_SESSION_ID, "Review this push"); + review["category"] = serde_json::json!("code_review"); + let mut script: Vec<(u16, String)> = (0..MAX_LIST_PAGES + 20) + .map(|page| { + ( + 200, + devin_page(&[review.clone()], Some(&format!("cursor+{page}")), true), + ) + }) + .collect(); + script.push(( + 200, + devin_page( + &[devin_session("aaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaa", "real")], + None, + false, + ), + )); + let transport = Arc::new(ScriptedDevinTransport::new(script)); + let provider = devin_provider(home.path(), &transport, None); + let conn = catalog(); + let env = env_at(&conn, home.path()); + let candidates = provider.enumerate(&env, None).unwrap(); + assert_eq!(candidates.len(), 1); + assert_eq!( + candidates[0].session_id.as_deref(), + Some("aaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaa") + ); +} + #[test] fn devin_enumeration_pages_with_first_and_after() { let _cleared = without_credentials_override(); From 3c5cbb228dd66bc2868e22c1826775556ffd9756 Mon Sep 17 00:00:00 2001 From: Khaliq Date: Thu, 15 Oct 2026 21:59:59 -0700 Subject: [PATCH 3/4] chore: drop stray untracked files swept into the last commit Generated with [Devin](https://devin.ai) Co-Authored-By: Devin <158243242+devin-ai-integration[bot]@users.noreply.github.com> --- .agentworkforce/relay/broker-kjg-lap.lock | 0 .agentworkforce/relay/pending-cleanups.json | 1 - .agentworkforce/relay/pending-cleanups.json.lock | 1 - 3 files changed, 2 deletions(-) delete mode 100644 .agentworkforce/relay/broker-kjg-lap.lock delete mode 100644 .agentworkforce/relay/pending-cleanups.json delete mode 100644 .agentworkforce/relay/pending-cleanups.json.lock diff --git a/.agentworkforce/relay/broker-kjg-lap.lock b/.agentworkforce/relay/broker-kjg-lap.lock deleted file mode 100644 index e69de29b..00000000 diff --git a/.agentworkforce/relay/pending-cleanups.json b/.agentworkforce/relay/pending-cleanups.json deleted file mode 100644 index fe51488c..00000000 --- a/.agentworkforce/relay/pending-cleanups.json +++ /dev/null @@ -1 +0,0 @@ -[] diff --git a/.agentworkforce/relay/pending-cleanups.json.lock b/.agentworkforce/relay/pending-cleanups.json.lock deleted file mode 100644 index 67c2e8e5..00000000 --- a/.agentworkforce/relay/pending-cleanups.json.lock +++ /dev/null @@ -1 +0,0 @@ -agent-relay-cleanup-lock v2 From 218941138956d30d239a12da03cf1d352183b20e Mon Sep 17 00:00:00 2001 From: kjgbot Date: Sat, 17 Oct 2026 16:21:47 -0700 Subject: [PATCH 4/4] fix(devin): clarify automation-only release note and coverage --- CHANGELOG.md | 7 +++-- .../provider-sources/rust/src/remote/tests.rs | 26 +++++++++++++------ 2 files changed, 23 insertions(+), 10 deletions(-) diff --git a/CHANGELOG.md b/CHANGELOG.md index fc8dd87d..824813d6 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -4,7 +4,11 @@ User-facing release notes for RelayHistory. Every public package — the `ai-his This project follows [Semantic Versioning](https://semver.org/spec/v2.0.0.html). Before 1.0, a breaking change is a minor release. -## [Unreleased] +## [Unreleased - Patch] + +### Fixed + +- Devin remote capture skips provider-classified `code_review` and `code_review_and_analysis` sessions only when their origin is `automation`; person-started reviews and other work remain discoverable. ## [0.45.2] - 2026-10-17 @@ -33,7 +37,6 @@ This project follows [Semantic Versioning](https://semver.org/spec/v2.0.0.html). ### Fixed -- Devin remote capture skips sessions the v3 listing classifies as `code_review` or `code_review_and_analysis` — Cognition's Devin Review / push-review automations — so org review traffic no longer fills the catalog. - Two simultaneous first hydrations of one local session both succeed; the slower one no longer fails `CONNECTOR_NOT_CONFIGURED` when the other records the session's local observation mid-lookup. - Codex rollouts that Codex has compressed to `rollout-*.jsonl.zst` are discovered, synced, hydrated and fed to the change feed as evidence under their `.jsonl` locator; a rollout compressed after it was indexed keeps its session, locator and cursor and is not ingested again. - The first sync of a new store records each Codex rollout's cursor, so the rollout's next append resumes instead of re-reading it from the start. diff --git a/plugins/provider-sources/rust/src/remote/tests.rs b/plugins/provider-sources/rust/src/remote/tests.rs index 73e28814..8029fb79 100644 --- a/plugins/provider-sources/rust/src/remote/tests.rs +++ b/plugins/provider-sources/rust/src/remote/tests.rs @@ -1049,14 +1049,24 @@ fn devin_mapping_skips_review_sessions_and_keeps_uncategorized_work() { // The review filter is scoped to automation-spawned sessions: a // person-directed session whose work the classifier files as review // still belongs in the catalog. - for origin in ["webapp", "api", "desktop", "cli"] { - let mut entry = devin_session(DEVIN_SESSION_ID, "Review my PR"); - entry["origin"] = serde_json::json!(origin); - entry["category"] = serde_json::json!("code_review"); - assert!( - map_devin_session(&entry).is_some(), - "{origin} review work must still map" - ); + for category in ["code_review", "code_review_and_analysis"] { + for origin in [ + None, + Some("webapp"), + Some("slack"), + Some("teams"), + Some("api"), + Some("desktop"), + Some("cli"), + ] { + let mut entry = devin_session(DEVIN_SESSION_ID, "Review my PR"); + entry["origin"] = serde_json::json!(origin); + entry["category"] = serde_json::json!(category); + assert!( + map_devin_session(&entry).is_some(), + "{origin:?} {category} work must still map" + ); + } } }