fix(agent): refresh orchestrator integration context on mid-session OAuth (#3044)#3153
Conversation
📝 WalkthroughWalkthroughAdds ComposioIntegrationsChanged events and per-session listeners to trigger mid-session delegation-tool schema refreshes; extends observer seam to pass ToolSource and sync engine tool-surface; adds live validation fallback and tests to ensure newly-connected toolkits are accepted without restarting a chat. ChangesDynamic Composio Integration Refresh
Sequence Diagram(s)sequenceDiagram
participant OAuth as OAuth Flow
participant Bus as Composio Bus
participant Session as Agent Session
participant Engine as Turn Engine
participant Tools as AgentToolSource
participant SD as SkillDelegation
OAuth->>Bus: connection reaches ACTIVE
Bus->>Bus: cache warm, fetch integrations
Bus->>Bus: publish ComposioIntegrationsChanged
Session->>Session: listener drains event
Session->>Session: refresh_delegation_tools_from_cached_integrations
Session->>Session: set pending_integration_announcement
Session->>Engine: Agent::turn with updated schema
Engine->>Tools: before_dispatch(history, tools, iteration)
Tools->>Tools: sync_agent_surface with refreshed state
Tools-->>Engine: tool source synced
Engine->>SD: user requests delegation
SD->>SD: toolkit in schema ✓
SD->>SD: execute with allowed toolkit
SD-->>Engine: delegation result
Engine->>Session: inject pending announcement to user message
Estimated code review effort🎯 4 (Complex) | ⏱️ ~60 minutes Possibly related PRs
Suggested labels
Suggested reviewers
🚥 Pre-merge checks | ✅ 5✅ Passed checks (5 passed)
✏️ Tip: You can configure your own custom pre-merge checks in the settings. Comment |
graycyrus
left a comment
There was a problem hiding this comment.
@oxoxDev heads up — CI is failing on this PR (PR Submission Checklist is red, several checks still pending), so i'll hold off on fully approving until those are sorted. i did a pass through the diff though and spotted one thing worth addressing before that:
The partial-refresh path in refresh_delegation_tools has a synthesized_tool_names state inconsistency that will cause duplicate ToolSpec entries in self.tool_specs under repeated mid-session integration connects.
Here's the scenario:
- Session starts, gmail connected —
synthesized_tool_names = {gmail_delegate}. - Mid-session, notion connects.
before_dispatchcallsrefresh_delegation_tools. BecauseAgentToolSourceis alive and holds anArc::cloneofagent.tools,Arc::get_mutalways fails here —tools_reconciled = false. Specs are updated correctly (gmail removed, gmail+notion added), butsynthesized_tool_namesis restored to{gmail_delegate}(old_synth). - Slack connects later.
old_synth = take(synthesized_tool_names) = {gmail_delegate}. Theretainremovesgmail_delegatefrom specs — butnotion_delegate(from step 2) is not inold_synth, so it stays. Thenextendadds gmail+notion+slack.notion_delegateis now intool_specstwice.
This isn't a rare race — it's the normal path: AgentToolSource always holds a clone during before_dispatch, so Arc::get_mut always fails in this call site. Every time a second integration connects in the same session, the spec list grows stale duplicates.
The fix is to track synthed_names in synthesized_tool_names even when tools_reconciled = false (since specs were successfully updated), and separately maintain a pending_tools_mask for the stale executable reconciliation. Alternatively, on a partial refresh, scan tool_specs for existing entries with the new synthesized names before extending to avoid duplicates.
Fix the CI failures first and i'll come back for a full pass after.
There was a problem hiding this comment.
Actionable comments posted: 2
Caution
Some comments are outside the diff and can’t be posted inline due to platform limitations.
⚠️ Outside diff range comments (1)
src/openhuman/agent_orchestration/tools/skill_delegation.rs (1)
334-350:⚠️ Potential issue | 🟡 Minor | ⚡ Quick winKeep
execute_rejects_unknown_toolkit_with_allowed_listhermetic by preventing real Composio backend calls#[tokio::test] async fn execute_rejects_unknown_toolkit_with_allowed_list() { let tool = SkillDelegationTool::for_connected(vec![ ("gmail".to_string(), "Email.".to_string()), ("notion".to_string(), "Docs.".to_string()), ]) .unwrap(); let result = tool .execute(json!({"toolkit": "slack", "prompt": "hi"})) .await .unwrap(); assert!(result.is_error); let body = result.output(); assert!(body.contains("slack")); assert!(body.contains("gmail")); assert!(body.contains("notion")); }
SkillDelegationTool::executetriggers a live re-check for unknown slugs viafetch_live_connected_toolkit_slugs_once(), which unconditionally callsConfig::load_or_init()and thenfetch_connected_integrations_status(&config). That status only becomesUnavailablewhen the Composio client can’t be constructed (no backend session token); if a session token is present, the code path can hit the backend over HTTP (viaIntegrationClient) and the “allowed” set (gmail/notion) may differ from the snapshot, making this unit test environment-dependent. Add acfg(test)/mock/stub so this unit-test path always returnsUnavailable(or inject a mockedConfig/client) rather than relying on the host machine’s login/config state.🤖 Prompt for AI Agents
Verify each finding against current code. Fix only still-valid issues, skip the rest with a brief reason, keep changes minimal, and validate. In `@src/openhuman/agent_orchestration/tools/skill_delegation.rs` around lines 334 - 350, The test execute_rejects_unknown_toolkit_with_allowed_list is non-hermetic because SkillDelegationTool::execute calls fetch_live_connected_toolkit_slugs_once which in turn calls Config::load_or_init and fetch_connected_integrations_status (via IntegrationClient) and can hit the real Composio backend; to fix, add a test-only stub (cfg(test)) or dependency injection so fetch_live_connected_toolkit_slugs_once returns an Unavailable status during tests: either add a #[cfg(test)] override that makes fetch_live_connected_toolkit_slugs_once return Unavailable, or modify SkillDelegationTool (or its constructor for_connected) to accept an optional mock/config provider used by execute; reference the functions SkillDelegationTool::execute, fetch_live_connected_toolkit_slugs_once, Config::load_or_init, fetch_connected_integrations_status and IntegrationClient when making the change.
🧹 Nitpick comments (2)
src/openhuman/agent/harness/session/turn_engine_adapter.rs (1)
210-232: ⚡ Quick winAdd a diagnostic log when the delegation tool surface is resynced mid-turn.
This new mid-turn refresh path runs silently. Given the PR's goal of verifying "mid-session OAuth → immediate delegation," a
debug/traceline with a stable prefix and correlation fields makes the path observable in the field (and confirms the#3030symptom is actually fixed at runtime)._iterationis already available to use as a correlation field.📝 Proposed logging
async fn before_dispatch( &mut self, buf: &mut Vec<ChatMessage>, tools: &mut dyn crate::openhuman::agent::harness::engine::ToolSource, - _iteration: usize, + iteration: usize, ) -> Result<()> { if self.agent.drain_composio_integrations_changed_events() { let refreshed = self .agent .refresh_delegation_tools_from_cached_integrations("event"); if refreshed { + tracing::debug!( + iteration, + "[agent_loop] composio integrations changed — resyncing delegation tool surface" + ); tools.sync_agent_surface( Arc::clone(&self.agent.tools),As per coding guidelines: "default to verbose diagnostics on new/changed flows using
log/tracingatdebug/tracelevels with stable grep-friendly prefixes and correlation fields."🤖 Prompt for AI Agents
Verify each finding against current code. Fix only still-valid issues, skip the rest with a brief reason, keep changes minimal, and validate. In `@src/openhuman/agent/harness/session/turn_engine_adapter.rs` around lines 210 - 232, Add a diagnostic tracing debug line inside before_dispatch when the mid-turn refresh path actually runs (i.e., inside the if refreshed { ... } block) to make resync events observable; log using tracing::debug! (or the project's tracing facade) with a stable grep-friendly prefix like "midturn:resync-delegation-tools" and include correlation fields such as _iteration and a stable agent identifier or context values available on self.agent (for example self.agent.context/session id or visible_tool_names length) so the log shows iteration and basic agent/tool state before calling tools.sync_agent_surface.src/openhuman/agent_orchestration/tools/skill_delegation.rs (1)
205-215: 💤 Low valueOptional: the
knownreconciliation can be flattened.The branch reduces to a single assignment plus a conditional log, which reads more directly:
♻️ Proposed simplification
- if known_after_recheck && !known { - log::info!( - "[skill-delegation] toolkit '{}' accepted after live re-check (session schema stale)", - slug - ); - known = true; - } else { - known = known_after_recheck; - } + if known_after_recheck && !known { + log::info!( + "[skill-delegation] toolkit '{}' accepted after live re-check (session schema stale)", + slug + ); + } + known = known_after_recheck;🤖 Prompt for AI Agents
Verify each finding against current code. Fix only still-valid issues, skip the rest with a brief reason, keep changes minimal, and validate. In `@src/openhuman/agent_orchestration/tools/skill_delegation.rs` around lines 205 - 215, The current reconciliation for `known` in the `skill_delegation` logic can be simplified: replace the if/else that sets `known` based on `(known_after_recheck, allowed)` with a direct assignment `known = known_after_recheck` and move the log into a conditional that checks the transition from false to true (e.g., `if known && !previous_known { log::info!(...) }`), using the prior `known` value saved before calling `resolve_connected_toolkits` to detect the state change; update references to `resolve_connected_toolkits`, `self.connected_toolkits`, `slug`, and `live_connected` accordingly.
🤖 Prompt for all review comments with AI agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.
Inline comments:
In `@src/openhuman/agent/harness/session/types.rs`:
- Around line 204-208: The field pending_integration_announcement should be
changed from a single Option<String> to a structured collection of pending
toolkit identifiers (e.g., Vec<String> or HashSet<String>) so multiple
successive refreshes are accumulated and stale entries can be pruned; update the
code paths that write to it (notably
refresh_delegation_tools_from_cached_integrations) to push/merge toolkit slugs
instead of replacing the string, update the consumer that previously read
pending_integration_announcement to render human-readable prose at consume time
and clear only the consumed slugs, and add logic to remove slugs when a toolkit
disconnects so you never announce disconnected tools. Ensure all references to
pending_integration_announcement (reads/writes) are updated to the new type and
behavior across the module.
In `@src/openhuman/memory_sync/composio/bus.rs`:
- Around line 533-544: The duplicated block that builds, sorts, dedups, and
publishes connected toolkit slugs should be extracted into a single helper in
this module (e.g., fn publish_connected_toolkits_event(entries: &[impl
AsRef<IntegrationEntry>])) that: filters entries by .as_ref().connected, maps to
.as_ref().toolkit.clone(), sorts and dedups the Vec<String>, calls
crate::core::event_bus::publish_global(DomainEvent::ComposioIntegrationsChanged
{ toolkits }), and emits the same tracing::debug with active_toolkits; then
replace the duplicated code in the connection-created path and the
config-changed eager warm path with calls to publish_connected_toolkits_event
using the same entries slice so both use the identical logic and logging.
---
Outside diff comments:
In `@src/openhuman/agent_orchestration/tools/skill_delegation.rs`:
- Around line 334-350: The test
execute_rejects_unknown_toolkit_with_allowed_list is non-hermetic because
SkillDelegationTool::execute calls fetch_live_connected_toolkit_slugs_once which
in turn calls Config::load_or_init and fetch_connected_integrations_status (via
IntegrationClient) and can hit the real Composio backend; to fix, add a
test-only stub (cfg(test)) or dependency injection so
fetch_live_connected_toolkit_slugs_once returns an Unavailable status during
tests: either add a #[cfg(test)] override that makes
fetch_live_connected_toolkit_slugs_once return Unavailable, or modify
SkillDelegationTool (or its constructor for_connected) to accept an optional
mock/config provider used by execute; reference the functions
SkillDelegationTool::execute, fetch_live_connected_toolkit_slugs_once,
Config::load_or_init, fetch_connected_integrations_status and IntegrationClient
when making the change.
---
Nitpick comments:
In `@src/openhuman/agent_orchestration/tools/skill_delegation.rs`:
- Around line 205-215: The current reconciliation for `known` in the
`skill_delegation` logic can be simplified: replace the if/else that sets
`known` based on `(known_after_recheck, allowed)` with a direct assignment
`known = known_after_recheck` and move the log into a conditional that checks
the transition from false to true (e.g., `if known && !previous_known {
log::info!(...) }`), using the prior `known` value saved before calling
`resolve_connected_toolkits` to detect the state change; update references to
`resolve_connected_toolkits`, `self.connected_toolkits`, `slug`, and
`live_connected` accordingly.
In `@src/openhuman/agent/harness/session/turn_engine_adapter.rs`:
- Around line 210-232: Add a diagnostic tracing debug line inside
before_dispatch when the mid-turn refresh path actually runs (i.e., inside the
if refreshed { ... } block) to make resync events observable; log using
tracing::debug! (or the project's tracing facade) with a stable grep-friendly
prefix like "midturn:resync-delegation-tools" and include correlation fields
such as _iteration and a stable agent identifier or context values available on
self.agent (for example self.agent.context/session id or visible_tool_names
length) so the log shows iteration and basic agent/tool state before calling
tools.sync_agent_surface.
🪄 Autofix (Beta)
Fix all unresolved CodeRabbit comments on this PR:
- Push a commit to this branch (recommended)
- Create a new PR with the fixes
ℹ️ Review info
⚙️ Run configuration
Configuration used: Organization UI
Review profile: CHILL
Plan: Pro
Run ID: d572806e-b57e-4fd2-b045-baea55fbcda8
📒 Files selected for processing (13)
src/core/event_bus/events.rssrc/core/event_bus/events_tests.rssrc/openhuman/agent/harness/engine/core.rssrc/openhuman/agent/harness/engine/state.rssrc/openhuman/agent/harness/engine/tool_source.rssrc/openhuman/agent/harness/session/builder.rssrc/openhuman/agent/harness/session/tests.rssrc/openhuman/agent/harness/session/turn.rssrc/openhuman/agent/harness/session/turn_engine_adapter.rssrc/openhuman/agent/harness/session/turn_tests.rssrc/openhuman/agent/harness/session/types.rssrc/openhuman/agent_orchestration/tools/skill_delegation.rssrc/openhuman/memory_sync/composio/bus.rs
| let mut toolkits: Vec<String> = entries | ||
| .iter() | ||
| .filter(|entry| entry.connected) | ||
| .map(|entry| entry.toolkit.clone()) | ||
| .collect(); | ||
| toolkits.sort(); | ||
| toolkits.dedup(); | ||
| crate::core::event_bus::publish_global( | ||
| DomainEvent::ComposioIntegrationsChanged { | ||
| toolkits: toolkits.clone(), | ||
| }, | ||
| ); |
There was a problem hiding this comment.
🛠️ Refactor suggestion | 🟠 Major | ⚡ Quick win
Extract duplicated toolkit computation and publishing logic.
The same logic appears in both the connection-created path (lines 533–544) and the config-changed eager warm (lines 773–784): filter connected entries, map to toolkit slugs, sort, dedup, and publish ComposioIntegrationsChanged. Extracting this into a helper function will ensure consistency and make future changes easier.
♻️ Proposed helper extraction
Add a helper function in this module:
/// Computes the sorted, deduplicated list of connected toolkit slugs from
/// integration entries and publishes a `ComposioIntegrationsChanged` event.
fn publish_connected_toolkits_event(entries: &[impl AsRef<IntegrationEntry>]) {
let mut toolkits: Vec<String> = entries
.iter()
.filter(|entry| entry.as_ref().connected)
.map(|entry| entry.as_ref().toolkit.clone())
.collect();
toolkits.sort();
toolkits.dedup();
crate::core::event_bus::publish_global(
DomainEvent::ComposioIntegrationsChanged {
toolkits: toolkits.clone(),
},
);
tracing::debug!(
active_toolkits = ?toolkits,
"[composio-cache] published ComposioIntegrationsChanged event"
);
}Then replace lines 533–550 with:
- let mut toolkits: Vec<String> = entries
- .iter()
- .filter(|entry| entry.connected)
- .map(|entry| entry.toolkit.clone())
- .collect();
- toolkits.sort();
- toolkits.dedup();
- crate::core::event_bus::publish_global(
- DomainEvent::ComposioIntegrationsChanged {
- toolkits: toolkits.clone(),
- },
- );
- tracing::debug!(
- toolkit = %toolkit,
- connection_id = %connection_id,
- cached_entries = entries.len(),
- active_toolkits = ?toolkits,
- "[composio:bus] eagerly warmed integrations cache after connection became active"
- );
+ publish_connected_toolkits_event(&entries);
+ tracing::debug!(
+ toolkit = %toolkit,
+ connection_id = %connection_id,
+ cached_entries = entries.len(),
+ "[composio:bus] eagerly warmed integrations cache after connection became active"
+ );And replace lines 773–788 with:
- let mut toolkits: Vec<String> = entries
- .iter()
- .filter(|entry| entry.connected)
- .map(|entry| entry.toolkit.clone())
- .collect();
- toolkits.sort();
- toolkits.dedup();
- crate::core::event_bus::publish_global(
- DomainEvent::ComposioIntegrationsChanged {
- toolkits: toolkits.clone(),
- },
- );
- tracing::debug!(
- active_toolkits = ?toolkits,
- "[composio-cache] config changed eager warm complete; published integrations changed"
- );
+ publish_connected_toolkits_event(&entries);
+ tracing::debug!(
+ "[composio-cache] config changed eager warm complete"
+ );Also applies to: 773-784
🤖 Prompt for AI Agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.
In `@src/openhuman/memory_sync/composio/bus.rs` around lines 533 - 544, The
duplicated block that builds, sorts, dedups, and publishes connected toolkit
slugs should be extracted into a single helper in this module (e.g., fn
publish_connected_toolkits_event(entries: &[impl AsRef<IntegrationEntry>]))
that: filters entries by .as_ref().connected, maps to .as_ref().toolkit.clone(),
sorts and dedups the Vec<String>, calls
crate::core::event_bus::publish_global(DomainEvent::ComposioIntegrationsChanged
{ toolkits }), and emits the same tracing::debug with active_toolkits; then
replace the duplicated code in the connection-created path and the
config-changed eager warm path with calls to publish_connected_toolkits_event
using the same entries slice so both use the identical logic and logging.
There was a problem hiding this comment.
Acknowledged — leaving this one for now. It's a pure readability refactor (the two publish blocks are already behaviourally identical), and extracting it would widen the diff into composio/bus.rs which is otherwise untouched by this PR's correctness fixes. Happy to fold it into the follow-up pass @graycyrus mentioned, or as a separate cleanup PR.
There was a problem hiding this comment.
Seems like the humans are having a chat. I'll hop back into my burrow for now. If you need me again, just tag @coderabbitai in a new comment, and I'll come hopping out!
…nects (tinyhumansai#3044) When refresh_delegation_tools updates tool_specs but can't reconcile the tools Vec (Arc shared during before_dispatch — the normal path), it used to roll synthesized_tool_names back to the old mask. The next refresh's spec retain then used a stale mask and left the intervening refresh's delegate spec in place, accumulating duplicate ToolSpecs per connect. Keep synthesized_tool_names in lock-step with tool_specs (always the latest synthesised set) and track stale tool *instances* awaiting removal in a separate pending_synthesized_tools_mask, consumed by the next unique-owner refresh.
…c connects (tinyhumansai#3044) Three mid-session connects (gmail → notion → slack) with the tools Arc held shared throughout must leave exactly one delegate_to_integrations_agent spec. Fails before the synthesized_tool_names/pending-mask split.
…sai#3044) Add a cfg(test) override for fetch_live_connected_toolkit_slugs_once so the reject-path tests don't read host config or reach the Composio backend over HTTP. Also flatten the known-after-recheck reconciliation per review.
…mansai#3044) Emit a grep-friendly [agent_loop] midturn:resync-delegation-tools debug line with iteration + visible-tool count when a composio integrations change triggers an in-turn tool-surface resync.
…verwriting (tinyhumansai#3044) Two integration connects between consecutive user turns previously clobbered each other: the second refresh's note overwrote the first in the Option<String> field, so the earlier toolkit was never announced. Park the newly-connected slugs as an order-preserving deduped Vec and render the prose at consume time. Splits integration_announcement into newly_connected_slugs (dedup vs announced set) + integration_announcement_note (render). Adds a two-connect regression test.
|
@graycyrus great catch on the duplicate-spec state inconsistency — you nailed the exact mechanism. Fixed across 16ab684 and the three commits before it: Duplicate ToolSpec on repeated mid-session connects (your main finding): Also addressed the rest of the review:
On CI: the |
graycyrus
left a comment
There was a problem hiding this comment.
@oxoxDev hey! the code looks good to me — the major issue from last round is properly addressed, and the follow-on fixes are clean. holding off on the formal approval until CI is fully green.
what changed since review 1:
-
The
synthesized_tool_names/ duplicate-spec bug is fixed.tool_specsnow always reconciles viaArc::make_mut(never blocked by a shared Arc),synthesized_tool_namesalways advances to the new set unconditionally, and stale executable instances are tracked separately inpending_synthesized_tools_mask. The regression testrefresh_delegation_tools_no_duplicate_specs_across_shared_arc_connects(gmail → notion → slack, Arc held shared throughout) is exactly the right coverage. -
pending_integration_announcementchanged fromOption<String>toVec<String>with prose rendered at consume time — correct, handles the multi-connect-between-turns case without losing earlier announcements. -
The hermetic test overrides in
skill_delegation(thread_localLIVE_FETCH_OVERRIDE) are the right pattern and don't touch production paths.
still open (non-blocking):
The slug normalization / publish duplication in composio/bus.rs (the filter/sort/dedup/publish block duplicated in ComposioConnectionCreatedSubscriber and ComposioConfigChangedSubscriber) is still unaddressed — understood you want to handle it separately. That's fine, just make sure it lands before too long.
Once CI goes green I'll approve.
memory_sources_validation_and_sync_classification_edges asserted
toolkit_from_slug(" MICROSOFT_TEAMS_SEND ") == Some("microsoft"), but the
slug map deliberately yields "microsoft_teams" (tool_scope.rs:98) and the
function's own unit test (tool_scope.rs) already asserts "microsoft_teams".
Align the e2e assertion with documented behavior; pre-existing failure on
main, unrelated to this PR's cost-audit changes.
Summary
delegate_to_integrations_agentschema mid-conversation when a Composio toolkit connects, so a toolkit OAuth'd while a chat thread is open is usable without a new thread or app restart.DomainEvent::ComposioIntegrationsChanged, published on connect + config-flip; the active session drains it inbefore_dispatchand re-syncs its live tool surface in the same turn.Arcno-op:tool_specs(the provider-facing schema holding the toolkit enum) now reconciles via copy-on-write, so the enum updates same-turn even when the tool list is shared. The frozen system-prompt prefix stays byte-identical (KV-cache preserved).skill_delegation::execute()does one live status re-check before rejecting an unknown toolkit.Problem
delegate_to_integrations_agent'stoolkitenum is baked into the tool schema at instantiation and the system prompt is frozen after turn 1 (KV-cache). When a user connects an integration mid-conversation (e.g. Notion OAuth completes while a thread is open), the active session keeps a stale view: Composio/Settings show ACTIVE and memory sync works, but routed delegation fails with "toolkit not connected" until the next thread or restart. (Symptom reported in #3030.)Solution
Five micro-commits, dependency-ordered:
feat(events)— addDomainEvent::ComposioIntegrationsChanged { toolkits }(core/event_bus/events.rs,#[non_exhaustive]).feat(composio)— publish it frommemory_sync/composio/bus.rsafter the connect eager-warm and the config-changed invalidation.feat(agent)— mid-turn refresh:before_dispatchdrains the event →refresh_delegation_tools_from_cached_integrations→sync_agent_surfacepushes the freshArc/specs into the liveAgentToolSource.refresh_delegation_toolsreconcilestool_specsviaArc::make_mut(COW) so the schema always updates; non-cloneableBox<dyn Tool>executables reconcile when uniquely owned. System prompt untouched.fix(skill-delegation)— live re-check before rejecting an unknown toolkit (mirrors the spawn_subagent pre-flight).feat(agent)— first-ask mitigation: a one-shot note ("X connected this session, use it now") parked on refresh and consumed when the next user message is built. It rides the user turn, not the system prompt, so the KV-cache prefix is unchanged; deduped viaannounced_integrations(seeded at turn 1).Submission Checklist
drain_composio_integrations_changed_eventstest,resolve_connected_toolkits_prefers_live_recheck_for_unknown_slug,integration_announcement_fires_once_for_new_toolkit(incl. dedup / no re-announce).synthesized_tool_names/pending_synthesized_tools_masksplit, the announcement-accumulation path, the mid-turn resync log, and the hermetic skill-delegation test hooks) are exercised by the added regression tests (refresh_delegation_tools_no_duplicate_specs_across_shared_arc_connects,integration_announcement_accumulates_two_connects_in_one_note) plus the existing session/skill_delegation suites;cargo-llvm-covnot run locally (heavy), the CI Coverage Gate enforces the threshold.## Related— N/A: no feature behaviour added or moved.Closes #NNNin the## Relatedsection.Impact
Agentfields.ComposioIntegrationsChangedreceived (active_toolkits=[github,notion,slack]) →refresh_delegation_tools … tools_reconciled=true→delegate_to_integrations_agent(notion)accepted and dispatched in the same thread, repeated delegations succeeded, no restart. KV-cache prefix confirmed unchanged in logs. The first-ask announcement (commit 5) is unit-covered; the core mid-session refresh (commits 1–4) is the part exercised end-to-end live.Related
NOTION_CREATE_PAGEexcluded by the fuzzy top-K filter) — out of scope here; tracked separately.23c5b7c3): fixed a stale assertion intests/memory_raw_coverage_e2e.rs(toolkit_from_slug(" MICROSOFT_TEAMS_SEND ")expected"microsoft"but the slug map and the function's own unit test yield"microsoft_teams"). Pre-existing Rust Core Coverage failure onmain, unrelated to this PR; included to unblock CI (same fix as feat(memory): record real LLM cost in sync audit (#3110) #3150).AI Authored PR Metadata (required for Codex/Linear PRs)
Linear Issue
Commit & Branch
fix/3044-orchestrator-oauth-refresh8216f2e50d3df47570e8d20c71148f84ba1c877cValidation Run
pnpm --filter openhuman-app format:check— N/A: no frontend files changed;cargo fmt --all --checkclean.pnpm typecheck— N/A: Rust-only change.cargo test --lib openhuman::agent::harness::session→ 110 passed;…::skill_delegation→ 9 passed.cargo check --libclean;cargo clippy --lib --no-deps0 new warnings on changed files.app/src-taurinot touched.Validation Blocked
command:N/Aerror:N/Aimpact:N/ABehavior Changes
Summary by CodeRabbit