fix(orchestration): key inbound DMs on the shared per-pair session id#4582
Conversation
`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.
|
No actionable comments were generated in the recent review. 🎉 ℹ️ Recent review info⚙️ Run configurationConfiguration used: Organization UI Review profile: CHILL Plan: Pro Run ID: 📒 Files selected for processing (1)
📝 WalkthroughWalkthroughSession routing now uses ChangesSession keying and persistence guard
PR quality workflow link exclusions
Estimated code review effort: 3 (Moderate) | ~20 minutes Possibly related issues
Possibly related PRs
Suggested labels: Suggested reviewers: Poem
🚥 Pre-merge checks | ✅ 5✅ Passed checks (5 passed)
Comment |
There was a problem hiding this comment.
💡 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, |
There was a problem hiding this comment.
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 👍 / 👎.
There was a problem hiding this comment.
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
left a comment
There was a problem hiding this comment.
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:
- 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. - 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:116—session_key()clones even when caller owns it immediately; negligible.
Looks good
- Borrow-before-move handled + commented (key computed while
envintact). - Tests cover both branches (
w1present /h-onlylegacy) + 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.
|
Thanks @oxoxDev — both asks done in 1. Tracked issue: #4583 — "wake cursor keyed on 2. In-scope guard: One reinforcing detail for #4583: the tiny.place plugin's MCP Nitpick ( Re-requesting review. |
…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.
oxoxDev
left a comment
There was a problem hiding this comment.
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_seqwarn-log (gated toChatKind::Session, via the newsession_last_seqhelper) 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.)
… 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.
|
Re-approval needed: commit |
Summary
scope.wrapper_session_id) instead ofharness_session_id, via a newSessionEnvelopeV1::session_key().Problem
classify_messagebucketed inbound harness DMs onharness_session_id. The plugin now emits one shared conversation id per thread inwrapper_session_id(and reuses it on reply), so keying onharness_session_idmeans an OpenHuman-initiated conversation never threads the peer's reply back into the originating session.Solution
SessionEnvelopeV1::session_key()→ returnswrapper_session_id(the shared id), falling back toharness_session_idonly for a legacy envelope with no per-pair id.classify_messageuses it (computed before field moves, since it borrows&env).S→ the claude session's reply reusedS→ OpenHuman ingested it back underSand ran its orchestration graph on that session.Submission Checklist
session_key_is_the_shared_wrapper_id_then_harness_fallback(types.rs) + updated ingestclassifies_harness_envelope_as_session/persist_message_is_idempotent_and_buckets_by_sessionto assert the shared id (w1) and its failure/fallback path.session_key+ the classify assignment) is exercised by the new/updated unit tests.Impact
wrapper_session_idstill falls back toharness_session_id, so there is no regression for older peers.Related
AI Authored PR Metadata (required for Codex/Linear PRs)
Linear Issue
Commit & Branch
fix/orchestration-shared-session-keye966b7667Validation Run
pnpm --filter openhuman-app format:check— no frontend/TS changespnpm typecheck— no frontend/TS changescargo test --lib openhuman::orchestration::passes (54 tests) in a sibling worktree carrying the byte-identical change (types + ingest)cargo fmt --checkclean on both files; fullcargo checkblocked in a cleanupstream/mainworktree (see Validation Blocked) — verified via sibling worktree + CIValidation Blocked
command:cargo check/cargo testin a fresh worktree offupstream/mainerror:missing vendored submodules (vendor/tinyagents,vendor/tinyplace) — a clean worktree can't complete the build locallyimpact: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
Summary by CodeRabbit