Skip to content

fix(cloud): create sessions before legacy sends - #168

Merged
cnjack merged 2 commits into
mainfrom
jack-wang1-hpi-fix-mobile-conversation-contract
Jul 24, 2026
Merged

fix(cloud): create sessions before legacy sends#168
cnjack merged 2 commits into
mainfrom
jack-wang1-hpi-fix-mobile-conversation-contract

Conversation

@cnjack

@cnjack cnjack commented Jul 24, 2026

Copy link
Copy Markdown
Owner

Summary

  • Create a real session before a legacy cloud chat.send without a session ID, avoiding active-engine misrouting.
  • Return the exact created session ID in the command acknowledgement.
  • Publish canonical plaintext last_activity_at metadata for device-session mirror ordering while keeping session metadata encrypted.

Validation

  • make build-web && go test ./internal/cloud ./internal/web

Summary by CodeRabbit

  • New Features

    • Sessions can now be ordered by recent activity using a timestamp that prefers the latest update and falls back to the start time.
    • New sessions are created (and a session ID is returned) before sending the first message, so subsequent commands open the correct session.
  • Bug Fixes

    • Improved handling/validation of session IDs to avoid accepting mismatches or missing IDs.
    • If session creation or the subsequent chat step fails, the overall command no longer reports success.
  • Documentation

    • Updated guidance to describe activity-based ordering and the session creation/acknowledgment flow.

Co-authored-by: Copilot App <223556219+Copilot@users.noreply.github.com>
@coderabbitai

coderabbitai Bot commented Jul 24, 2026

Copy link
Copy Markdown

Review Change Stack

📝 Walkthrough

Walkthrough

The connector now explicitly creates or focuses local sessions before chat submission, validates returned session IDs, and propagates those IDs in acknowledgements. Cloud session upserts also include UTC-normalized activity timestamps for ordering without decrypting metadata.

Changes

Session synchronization

Layer / File(s) Summary
Explicit session creation and chat routing
internal/cloud/connector.go, internal/cloud/connector_test.go, internal/web/concurrency_test.go, internal/cloud/crypto_wiring_test.go
Chat commands resolve a concrete local session through a shared helper, validate chat response IDs, propagate created IDs in acknowledgements, and cover failure and engine-selection behavior.
Session activity metadata propagation
internal/cloud/relay.go, internal/cloud/sessions.go, internal/cloud/sync_filter_test.go, internal/cloud/connector_test.go, internal/cloud/crypto_wiring_test.go, docs/cloud.md, AGENTS.md
Session upserts add UTC-normalized last_activity_at, preferring updated_at and falling back to start_time; documentation, encryption-path tests, and relay guidance describe and verify the plaintext routing metadata contract.

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

Sequence Diagram(s)

sequenceDiagram
  participant CloudConnector
  participant LocalSessionsAPI
  participant LocalChatAPI
  CloudConnector->>LocalSessionsAPI: Create or focus session
  LocalSessionsAPI-->>CloudConnector: Return session_id
  CloudConnector->>LocalChatAPI: Submit chat with session_id
  LocalChatAPI-->>CloudConnector: Return matching session_id
  CloudConnector-->>CloudConnector: Acknowledge result.session_id
Loading

Possibly related PRs

  • cnjack/jcode#28: Coordinates the /api/sessions contract around optional session_id and session resume behavior.
🚥 Pre-merge checks | ✅ 4 | ❌ 1

❌ Failed checks (1 warning)

Check name Status Explanation Resolution
Docstring Coverage ⚠️ Warning Docstring coverage is 71.43% which is insufficient. The required threshold is 80.00%. Write docstrings for the functions missing them to satisfy the coverage threshold.
✅ Passed checks (4 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: legacy cloud sends now create a session first when none is provided.
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.
✨ Finishing Touches
🧪 Generate unit tests (beta)
  • Create PR with unit tests
  • Commit unit tests in branch jack-wang1-hpi-fix-mobile-conversation-contract

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.

@coderabbitai coderabbitai 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.

Actionable comments posted: 1

Caution

Some comments are outside the diff and can’t be posted inline due to platform limitations.

⚠️ Outside diff range comments (1)
internal/cloud/connector.go (1)

852-859: 🎯 Functional Correctness | 🟠 Major | ⚡ Quick win

Validate the compose chat response session ID before acknowledging success.

Unlike the legacy path, compose accepts any successful /api/chat response and returns sid without checking the returned session_id. A mismatched or missing response ID can acknowledge a session that did not receive the message. Parse and require a matching session_id here as well.

🤖 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 `@internal/cloud/connector.go` around lines 852 - 859, Update the compose
`/api/chat` success path in the surrounding connector method to parse the
response body and require a non-empty `session_id` matching `sid` before
returning success. Reuse the existing response-validation behavior or helper
from the legacy path if available, and return an error result for missing or
mismatched IDs instead of acknowledging the session.
🤖 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 `@internal/cloud/sessions.go`:
- Around line 53-58: Move LastActivityAt from the plaintext SessionUpsert fields
into the value sealed by sealUplink, preserving its use for ordering after
openDownlink. Update internal/cloud/relay.go lines 36-39,
internal/cloud/crypto_wiring_test.go lines 116-130, and docs/cloud.md lines
47-50 to reflect and verify that all session activity metadata is encrypted when
a CEK exists; these sites require corresponding contract, test, and
documentation updates.

---

Outside diff comments:
In `@internal/cloud/connector.go`:
- Around line 852-859: Update the compose `/api/chat` success path in the
surrounding connector method to parse the response body and require a non-empty
`session_id` matching `sid` before returning success. Reuse the existing
response-validation behavior or helper from the legacy path if available, and
return an error result for missing or mismatched IDs instead of acknowledging
the session.
🪄 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: defaults

Review profile: CHILL

Plan: Pro Plus

Run ID: b28be72e-d902-4fa4-8f15-7cc585df611c

📥 Commits

Reviewing files that changed from the base of the PR and between d784a45 and d3ad459.

📒 Files selected for processing (8)
  • docs/cloud.md
  • internal/cloud/connector.go
  • internal/cloud/connector_test.go
  • internal/cloud/crypto_wiring_test.go
  • internal/cloud/relay.go
  • internal/cloud/sessions.go
  • internal/cloud/sync_filter_test.go
  • internal/web/concurrency_test.go

Comment on lines 53 to 58
upserts = append(upserts, SessionUpsert{
SessionID: m.UUID,
Status: status,
Meta: c.sealUplink(metaJSON),
SessionID: m.UUID,
Status: status,
Meta: c.sealUplink(metaJSON),
LastActivityAt: sessionLastActivityAt(m),
})

Copy link
Copy Markdown

Choose a reason for hiding this comment

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

🔒 Security & Privacy | 🟠 Major | 🏗️ Heavy lift

🧩 Analysis chain

🏁 Script executed:

#!/bin/bash
set -euo pipefail

echo "== files =="
git ls-files | rg '(^|/)(sessions|relay|crypto_wiring_test)\.go$|docs/cloud\.md$' || true

echo "== sessions.go outline =="
ast-grep outline internal/cloud/sessions.go --view compact || true

echo "== sessions.go relevant sections =="
cat -n internal/cloud/sessions.go | sed -n '1,140p'

echo "== relay.go relevant sections =="
cat -n internal/cloud/relay.go | sed -n '1,120p'

echo "== crypto_wiring_test.go relevant sections =="
cat -n internal/cloud/crypto_wiring_test.go | sed -n '1,180p'

echo "== docs/cloud.md relevant sections =="
cat -n docs/cloud.md | sed -n '1,120p'

echo "== sealUplink/openDownlink definitions/usages =="
rg -n "func .*sealUplink|func .*openDownlink|sealUplink|openDownlink" internal/cloud -S

Repository: cnjack/jcode

Length of output: 23948


🏁 Script executed:

#!/bin/bash
set -euo pipefail

echo "== connector sealing implementation =="
cat -n internal/cloud/connector.go | sed -n '250,320p'

echo "== package comments for internal/cloud =="
cat -n internal/cloud/*.go | sed -n '1,25p'

echo "== deterministic read-only check: plaintext fields outside sealUplink calls =="
python3 - <<'PY'
from pathlib import Path
for path in [Path("internal/cloud/sessions.go"), Path("internal/cloud/relay.go"), Path("internal/cloud/crypto_wiring_test.go"), Path("docs/cloud.md")]:
    text = path.read_text()
    print(f"\n--- {path} ---")
    for needle in [
        "LastActivityAt",
        "last_activity_at",
        "intentionally plaintext for cloud-side ordering",
        "want plaintext UTC timestamp",
        "可.加密的\n元数据,以及可选的明文字段 `last_activity_at`",
    ]:
        idx = text.find(needle)
        print(f"{needle!r}: {'FOUND' if idx >= 0 else 'MISSING'}")
PY

Repository: cnjack/jcode

Length of output: 5020


Do not send session activity metadata outside the E2EE envelope.

LastActivityAt is sent in plaintext with an active CEK, and the cipher-enabled test documents that the cloud receives it unsealed. This violates the internal/cloud E2EE relay payload rule that relay payloads must use sealUplink/openDownlink once a CEK exists. Move the ordering field under the sealed payload or update the policy/code/tests to match an approved plaintext ordering contract.

  • internal/cloud/sessions.go#L53-L58
  • internal/cloud/relay.go#L36-L39
  • internal/cloud/crypto_wiring_test.go#L116-L130
  • docs/cloud.md#L47-L50
📍 Affects 4 files
  • internal/cloud/sessions.go#L53-L58 (this comment)
  • internal/cloud/relay.go#L36-L39
  • internal/cloud/crypto_wiring_test.go#L116-L130
  • docs/cloud.md#L47-L50
🤖 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 `@internal/cloud/sessions.go` around lines 53 - 58, Move LastActivityAt from
the plaintext SessionUpsert fields into the value sealed by sealUplink,
preserving its use for ordering after openDownlink. Update
internal/cloud/relay.go lines 36-39, internal/cloud/crypto_wiring_test.go lines
116-130, and docs/cloud.md lines 47-50 to reflect and verify that all session
activity metadata is encrypted when a CEK exists; these sites require
corresponding contract, test, and documentation updates.

Source: Coding guidelines

@coderabbitai coderabbitai 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.

Actionable comments posted: 1

🤖 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 `@AGENTS.md`:
- Around line 223-228: Update the uplink sealing flow around sealUplink so any
sealing failure rejects or drops the content-bearing upload instead of logging
the error and sending the original plaintext. Preserve plaintext transmission
only for the explicitly allowed pre-CEK or cloud.e2ee:false cases, and ensure
callers handle the rejected upload without bypassing E2EE.
🪄 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: defaults

Review profile: CHILL

Plan: Pro Plus

Run ID: 00fb34a1-2611-4f10-8339-0591199b7ba1

📥 Commits

Reviewing files that changed from the base of the PR and between d3ad459 and 4e243d4.

📒 Files selected for processing (1)
  • AGENTS.md

Comment thread AGENTS.md
Comment on lines +223 to +228
- **E2EE is the default.** Session content, metadata, command bodies/results,
and other content-bearing relay payloads are sealed once a CEK exists; gray
plaintext content is only for pre-CEK or `cloud.e2ee:false`. Plaintext is
limited to routing metadata required by the orchestrator: ids, seq, kind,
timestamps (including `last_activity_at`), and online state. Never add a
content-bearing uplink path that bypasses `sealUplink`/`openDownlink`.

Copy link
Copy Markdown

Choose a reason for hiding this comment

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

🔒 Security & Privacy | 🟠 Major | 🏗️ Heavy lift

Fail closed when E2EE sealing fails

This says content-bearing payloads are sealed whenever a CEK exists, but internal/cloud/connector.go:270-281 currently logs the sealing error and sends the original plaintext. A transient crypto failure can therefore expose session content despite cloud.e2ee being enabled. Make sealUplink reject/drop the upload on sealing failure, or explicitly document this exception and its security implications.

🤖 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 `@AGENTS.md` around lines 223 - 228, Update the uplink sealing flow around
sealUplink so any sealing failure rejects or drops the content-bearing upload
instead of logging the error and sending the original plaintext. Preserve
plaintext transmission only for the explicitly allowed pre-CEK or
cloud.e2ee:false cases, and ensure callers handle the rejected upload without
bypassing E2EE.

@cnjack
cnjack merged commit 7fd8fb4 into main Jul 24, 2026
4 checks passed
@cnjack
cnjack deleted the jack-wang1-hpi-fix-mobile-conversation-contract branch July 24, 2026 03:28
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.

1 participant