Skip to content

fix(orchestration): key inbound DMs on the shared per-pair session id#4582

Merged
oxoxDev merged 5 commits into
tinyhumansai:mainfrom
sanil-23:fix/orchestration-shared-session-key
Jul 6, 2026
Merged

fix(orchestration): key inbound DMs on the shared per-pair session id#4582
oxoxDev merged 5 commits into
tinyhumansai:mainfrom
sanil-23:fix/orchestration-shared-session-key

Conversation

@sanil-23

@sanil-23 sanil-23 commented Jul 6, 2026

Copy link
Copy Markdown
Contributor

Summary

  • Key inbound harness DMs on the shared per-pair session id (scope.wrapper_session_id) instead of harness_session_id, via a new SessionEnvelopeV1::session_key().
  • Pairs with the tiny.place plugin change (fix(plugin): route inbound by the one common session id tiny.place#227) so a reply threads back into the session that originated the conversation — in both directions.

Problem

  • classify_message bucketed inbound harness DMs on harness_session_id. The plugin now emits one shared conversation id per thread in wrapper_session_id (and reuses it on reply), so keying on harness_session_id means an OpenHuman-initiated conversation never threads the peer's reply back into the originating session.

Solution

  • Add SessionEnvelopeV1::session_key() → returns wrapper_session_id (the shared id), falling back to harness_session_id only for a legacy envelope with no per-pair id.
  • classify_message uses it (computed before field moves, since it borrows &env).
  • Verified live: OpenHuman-initiated DM under a session id S → the claude session's reply reused S → OpenHuman ingested it back under S and ran its orchestration graph on that session.

Submission Checklist

  • Tests added or updated — session_key_is_the_shared_wrapper_id_then_harness_fallback (types.rs) + updated ingest classifies_harness_envelope_as_session / persist_message_is_idempotent_and_buckets_by_session to assert the shared id (w1) and its failure/fallback path.
  • Diff coverage ≥ 80% — every changed line (session_key + the classify assignment) is exercised by the new/updated unit tests.
  • N/A — Coverage matrix: behaviour-only change to an existing ingest path; no feature rows added/removed/renamed.
  • N/A — no matrix feature IDs affected (behaviour-only change).
  • No new external network dependencies introduced.
  • N/A — does not touch release-cut / manual-smoke surfaces (internal DM routing key only).
  • N/A — no linked issue; follow-up to the A2A shared-session-id work (plugin PR fix(plugin): route inbound by the one common session id tiny.place#227).

Impact

  • Desktop core only (Rust). Inbound harness DMs now bucket under the shared per-pair id; a legacy envelope with no wrapper_session_id still falls back to harness_session_id, so there is no regression for older peers.

Related


AI Authored PR Metadata (required for Codex/Linear PRs)

Linear Issue

  • Key: N/A
  • URL: N/A

Commit & Branch

  • Branch: fix/orchestration-shared-session-key
  • Commit SHA: e966b7667

Validation Run

  • N/A pnpm --filter openhuman-app format:check — no frontend/TS changes
  • N/A pnpm typecheck — no frontend/TS changes
  • Focused tests: cargo test --lib openhuman::orchestration:: passes (54 tests) in a sibling worktree carrying the byte-identical change (types + ingest)
  • Rust fmt/check: cargo fmt --check clean on both files; full cargo check blocked in a clean upstream/main worktree (see Validation Blocked) — verified via sibling worktree + CI
  • N/A Tauri fmt/check — no Tauri code changed

Validation Blocked

  • command: cargo check/cargo test in a fresh worktree off upstream/main
  • error: missing vendored submodules (vendor/tinyagents, vendor/tinyplace) — a clean worktree can't complete the build locally
  • impact: compilation + the full orchestration suite (54 tests) verified in a sibling worktree with the identical change; CI has the submodules and will validate authoritatively.

Behavior Changes

  • Intended behavior change: inbound harness DMs bucket on the shared per-pair session id.
  • User-visible effect: replies to OpenHuman-initiated agent conversations thread back into the same session.

Summary by CodeRabbit

  • New Features
    • Session messages now use a more precise per-session identifier for routing and persistence, improving consistency across related conversations.
    • Legacy session envelopes are still supported via automatic fallback to the older session identifier format.
  • Bug Fixes
    • Added a warning when messages arrive with non-monotonic sequence numbers for the same session, highlighting potential ordering/reuse issues while still saving data.
    • Lower-sequence messages continue to be saved to prevent unintended loss.
  • Chores
    • Updated CI link checking to exclude additional external URLs from validation.

`classify_message` bucketed inbound harness DMs on `harness_session_id`,
but the tiny.place plugin now sends one shared conversation id per thread in
`scope.wrapper_session_id` (and reuses it on reply). Key on that instead via
a new `SessionEnvelopeV1::session_key()` (wrapper id, `harness_session_id`
fallback for legacy envelopes), so a reply threads back into the session that
originated the conversation — in both directions.

Pairs with the plugin change (tinyhumansai/tiny.place#227). Verified live:
OpenHuman-initiated DM under session S -> claude reply reused S -> ingested
back under S and drove the orchestration graph.
@sanil-23 sanil-23 requested a review from a team July 6, 2026 12:42
@coderabbitai

coderabbitai Bot commented Jul 6, 2026

Copy link
Copy Markdown
Contributor

Review Change Stack

No actionable comments were generated in the recent review. 🎉

ℹ️ Recent review info
⚙️ Run configuration

Configuration used: Organization UI

Review profile: CHILL

Plan: Pro

Run ID: 39089863-b244-4f49-a2db-749d627b8016

📥 Commits

Reviewing files that changed from the base of the PR and between 46ed02e and 3dc305e.

📒 Files selected for processing (1)
  • .github/workflows/pr-quality.yml

📝 Walkthrough

Walkthrough

Session routing now uses SessionEnvelopeV1::session_key() for harness envelopes, persistence checks stored last_seq before warning on non-monotonic session messages, and the PR quality workflow ignores two extra external link targets.

Changes

Session keying and persistence guard

Layer / File(s) Summary
Session key contract
src/openhuman/orchestration/types.rs
Adds SessionEnvelopeV1::session_key() returning wrapper_session_id when present, else falling back to harness_session_id; tests cover both cases.
Message classification uses session_key
src/openhuman/orchestration/ingest.rs
classify_message now derives ClassifiedMessage.session_id from session_key() instead of scope.harness_session_id; tests updated to expect the wrapper id.
Last-seq lookup helper
src/openhuman/orchestration/store.rs
Adds session_last_seq to query the stored last_seq for an (agent_id, session_id) pair, returning None if absent.
Non-monotonic seq warning in persistence
src/openhuman/orchestration/ingest.rs
persist_message checks inbound seq against stored last_seq for ChatKind::Session and logs a warning if non-monotonic, without blocking persistence; new test confirms both messages still persist.

PR quality workflow link exclusions

Layer / File(s) Summary
Lychee exclusions
.github/workflows/pr-quality.yml
The link-check step adds two --exclude URL patterns for the repository stargazers page and the Star History API domain.

Estimated code review effort: 3 (Moderate) | ~20 minutes

Possibly related issues

Possibly related PRs

Suggested labels: bug, rust-core

Suggested reviewers: M3gA-Mind

Poem

I’m a rabbit with a session key,
Hopping where the wrapper id should be.
Seq gets checked, but messages stay,
A tiny warning puffs my way. 🐇

🚥 Pre-merge checks | ✅ 5
✅ Passed checks (5 passed)
Check name Status Explanation
Description Check ✅ Passed Check skipped - CodeRabbit’s high-level summary is enabled.
Title check ✅ Passed The title clearly matches the main change: inbound DMs are keyed on the shared per-pair session id.
Docstring Coverage ✅ Passed No functions found in the changed files to evaluate docstring coverage. Skipping docstring coverage check.
Linked Issues check ✅ Passed Check skipped because no linked issues were found for this pull request.
Out of Scope Changes check ✅ Passed Check skipped because no linked issues were found for this pull request.

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

@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: e966b76676

ℹ️ About Codex in GitHub

Codex has been enabled to automatically 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 👍.

When you sign up for Codex through ChatGPT, Codex can also answer questions or update the PR, like "@codex address that feedback".

// both peers put on every message for a thread), so a reply threads back
// into the same session. Falls back to `harness_session_id` for a legacy
// envelope with no per-pair id. See `SessionEnvelopeV1::session_key`.
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 Preserve monotonic wake cursors after rekeying sessions

When a shared thread later receives a message from a different peer harness session (for example after the peer restarts/reopens the harness but reuses the same wrapper_session_id), this now buckets both messages under the wrapper id while seq still comes from env.message.line, which is only monotonic within the emitting harness session. The wake path keys its cursor by (agent_id, session_id) and skips unless latest_seq > cursor, so after processing a higher line from one harness, a later lower/reset line in the same wrapper thread is persisted and acked but treated as idempotent and never reaches the orchestration graph. Please derive a monotonic per-wrapper sequence/cursor (or use timestamp/message id) when switching the session key to the shared wrapper id.

Useful? React with 👍 / 👎.

Copy link
Copy Markdown
Contributor Author

Choose a reason for hiding this comment

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

Good catch, and a fair concern. In practice the plugin side keeps a wrapper thread bound to a single emitting harness session: once a shared id (conversation uuid) is minted/adopted, an inbound message whose owning session is offline is held/reaped as closed rather than re-delivered to a different live session (see the routing change in tinyhumansai/tiny.place#227), so a given wrapper_session_id doesn't normally accumulate messages from two harness sessions with independent line counters.

That said, the underlying coupling — seq derived from env.message.line (monotonic only within an emitting harness) while the cursor now keys on the shared wrapper id — is real. A robust fix (a monotonic per-wrapper ingest counter, or keying the wake cursor on arrival order / message id / timestamp instead of line) lives in the wake/cursor logic (ops.rs/store.rs), not this minimal classify-keying change. Filing it as a follow-up so this PR stays scoped to the routing-key fix. Flagging for maintainer visibility.

@oxoxDev oxoxDev left a comment

Copy link
Copy Markdown
Collaborator

Choose a reason for hiding this comment

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

Review — key inbound DMs on shared per-pair session id

Clean, minimal, well-tested routing-key swap. One latent coupling this change introduces, so requesting changes only to get it tracked + guarded — the code fix itself is correctly out of scope.

Major — seq/key domain mismatch → silent message drop (src/openhuman/orchestration/ingest.rs:59)

seq = env.message.line is monotonic only within an emitting harness session. This PR moves the session key (and thus the wake cursor) to wrapper_session_id. upsert_session does last_seq = MAX(old, new) and the wake path fires only on last_seq > cursor (store.rs:169, ingest_cursor_lag store.rs:368). If two harness sessions ever share one wrapper_session_id (peer restart reusing the id, or a reset/lower line), the later message is persisted + acked but never reaches the orchestration graph — a silent drop.

Pre-PR this was impossible: key seq domain (harness_session_id). The key-swap is what introduces the mismatch. Same thing @chatgpt-codex-connector flagged (P2); the deferral to the tiny.place#227 1-harness:1-wrapper invariant is reasonable, but it's cross-repo, future-fragile, and the failure mode is silent.

Two asks before merge:

  1. File the follow-up as a tracked issue (not just this thread) — a robust fix (per-wrapper monotonic ingest counter, or cursor keyed on arrival/msg-id) lives in ops.rs/store.rs. Comment-driven deferrals rot; a dropped-message leak is the exact class worth a real issue.
  2. Cheap in-scope guard — log when a wrapper session receives seq <= last_seq, so the invariant breaking in prod surfaces instead of dropping quietly:
// persist_message, before upsert, Session chat_kind only:
if classified.seq <= existing_last_seq {
    log::warn!(target: LOG,
        "[orchestration] wrapper session {} got seq {} <= last_seq {} — possible cross-harness reuse; wake may skip",
        classified.session_id, classified.seq, existing_last_seq);
}

Nitpick

  • types.rs:116session_key() clones even when caller owns it immediately; negligible.

Looks good

  • Borrow-before-move handled + commented (key computed while env intact).
  • Tests cover both branches (w1 present / h-only legacy) + updated ingest asserts; diff-coverage credible.
  • CI green except Markdown Link Check (Lychee) — PR touches zero markdown, so external-link infra, not PR-caused.
  • BLOCKED = needs approval, not a CI failure.

…tinyhumansai#4583)

Address review on tinyhumansai#4582 (@oxoxDev): the wake cursor now keys on
`wrapper_session_id` while `seq = env.message.line` is monotonic only within
one emitting harness. `upsert_session` MAX-clamps `last_seq` and the wake fires
on `last_seq > cursor`, so a non-monotonic inbound seq (cross-harness reuse, or
a plugin-composed message whose line is 0) is persisted+acked but can silently
skip the graph.

- persist_message: for Session windows, warn when `seq <= last_seq` (via new
  `store::session_last_seq`) so the invariant break surfaces instead of dropping
  quietly. Guard only logs — never blocks persistence.
- Robust fix (per-wrapper monotonic ingest cursor) tracked in tinyhumansai#4583.
- Test: a lower-seq message on the same wrapper session still persists.
@sanil-23

sanil-23 commented Jul 6, 2026

Copy link
Copy Markdown
Contributor Author

Thanks @oxoxDev — both asks done in 2ad045b2:

1. Tracked issue: #4583 — "wake cursor keyed on wrapper_session_id but seq=env.message.line → silent message drop". Captures the domain mismatch, both trigger cases, the robust-fix options (per-wrapper monotonic ingest cursor, or cursor keyed on arrival/msg-id/timestamp in ops.rs/store.rs), and required tests.

2. In-scope guard: persist_message now warn!s when a Session window receives seq <= last_seq (via a new store::session_last_seq), so the break surfaces instead of dropping quietly. Guard only logs — never blocks persistence (covered by a new test: a lower-seq message on the same wrapper session still lands).

One reinforcing detail for #4583: the tiny.place plugin's MCP send/auto_reply path emits message.line = 0 on every message (verified via encodeEnvelope). If that's the primary A2A path, seq is effectively degenerate (always 0), so the cursor can't distinguish messages by seq even within one harness — widening the failure window beyond cross-harness reuse. Noted in the issue.

Nitpick (session_key() clone): left as-is — the caller (classify_message) moves it into ClassifiedMessage.session_id immediately, so it's a single owned String either way; negligible as you said.

Re-requesting review.

@sanil-23 sanil-23 requested a review from oxoxDev July 6, 2026 13:34
…ia ?)

Rust Quality clippy failed with E0308 — .optional() yields rusqlite::Error but
the module's Result is anyhow-based. Wrap as Ok(..optional()?) to convert, matching
the other query helpers in store.rs.
@coderabbitai coderabbitai Bot added agent Built-in agents, prompts, orchestration, and agent runtime in src/openhuman/agent/. bug rust-core Core Rust runtime in src/: CLI, core_server, shared infrastructure. labels Jul 6, 2026
@coderabbitai coderabbitai Bot removed the agent Built-in agents, prompts, orchestration, and agent runtime in src/openhuman/agent/. label Jul 6, 2026
coderabbitai[bot]
coderabbitai Bot previously approved these changes Jul 6, 2026
oxoxDev
oxoxDev previously approved these changes Jul 6, 2026

@oxoxDev oxoxDev left a comment

Copy link
Copy Markdown
Collaborator

Choose a reason for hiding this comment

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

Re-review ✅ — both asks addressed, approving

  • Tracked: follow-up filed as #4583 (the robust per-wrapper monotonic cursor), referenced from the guard comment — no longer a comment-only deferral.
  • Guard: the seq <= last_seq warn-log (gated to ChatKind::Session, via the new session_last_seq helper) surfaces a broken 1-harness:1-wrapper invariant in prod instead of dropping silently — exactly the detection this needed. The new test proves it warns without ever blocking persistence (lower-seq message still lands, count = 2). Error-type plumbing matches store.rs convention.

The silent-drop stays structurally possible until #4583, but it's now observable + tracked, which is the right scope for this routing-key PR. Nice turnaround.

Nit (unchanged, non-blocking): session_key() clones eagerly — ignore.

(Markdown Link Check red = external-link infra, PR touches zero markdown. Rust Core Coverage was still finishing at review time — new guard lines are covered by the added test.)

sanil-23 added 2 commits July 6, 2026 20:31
… a cold cache

No code change. The prior coverage run passed in ~12m; the last run rebuilt the
instrumented core crate (incl. ML deps) with no warm cache and exceeded
timeout-minutes: 50. Re-running with a warmer cache.
The soft Markdown Link Check fails on external services that return
bot/transient errors for legitimate links in the translated READMEs:
- github.com/tinyhumansai/openhuman/stargazers (404 to the checker)
- api.star-history.com/svg?... (503 Service Unavailable)

Add --exclude patterns matching the repo's existing approach (reddit,
homebrew, star-history/#...). continue-on-error check; no code change.
@sanil-23 sanil-23 dismissed stale reviews from coderabbitai[bot] and oxoxDev via 3dc305e July 6, 2026 15:47
@sanil-23 sanil-23 requested a review from oxoxDev July 6, 2026 15:49
@sanil-23

sanil-23 commented Jul 6, 2026

Copy link
Copy Markdown
Contributor Author

Re-approval needed: commit 3dc305e5 only adds two lychee --exclude patterns to .github/workflows/pr-quality.yml (for the flaky /stargazers 404 and api.star-history.com 503 external links) to green the non-blocking Markdown Link Check. No change to the orchestration code you already approved (ingest.rs/store.rs unchanged since 46ed02e5). The workflow-file edit tripped stale-review dismissal — sorry for the re-review ping.

@oxoxDev oxoxDev left a comment

Copy link
Copy Markdown
Collaborator

Choose a reason for hiding this comment

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

LGTM

@oxoxDev oxoxDev merged commit 057c3ff into tinyhumansai:main Jul 6, 2026
19 checks passed
@github-project-automation github-project-automation Bot moved this from Todo to Done in Team Openhuman Jul 6, 2026
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

bug rust-core Core Rust runtime in src/: CLI, core_server, shared infrastructure.

Projects

Status: Done

Development

Successfully merging this pull request may close these issues.

2 participants