Skip to content

WS-9: cloud-sync (convergence mapper + outbox + push CLI) → main - #22

Merged
kjgbot merged 1 commit into
mainfrom
codex/ws9-on-main
Jun 21, 2026
Merged

WS-9: cloud-sync (convergence mapper + outbox + push CLI) → main#22
kjgbot merged 1 commit into
mainfrom
codex/ws9-on-main

Conversation

@kjgbot

@kjgbot kjgbot commented Jun 21, 2026

Copy link
Copy Markdown
Contributor

WS-9 — relayhistory cloud-sync lens (Agent Relay Loop)

Lands the WS-9 client (Capture→push→store) directly on main, superseding the scaffold-stacked chain (#21#20).

Why this supersedes #21/#20

While closing the chain I found main already has a more advanced Rust CLI than the codex/rust-core-phase1-scaffold branch (main's ai-hist-core has XDG_DATA_HOME / git_branch / WAL; crates/ai-hist/src/main.rs is a full clap CLI; deps include chrono/flate2/rusqlite). Merging the scaffold (#21) into main would have regressed it. So WS-9 is re-applied cleanly on top of current main — purely additive, no scaffold needed. Please close #21 (obsolete) and #20 (scaffold-stacked) in favor of this PR.

Contents (additive; 30 tests, no new clippy warnings)

  • ai-hist-core/convergence.rs — local store → ratified WS-1 ConvergenceEnvelope + IngestRequest/IngestResponse; map_trajectory (decisions+retro fan-out, finding/reflection/decision kinds, collision-free kind-namespaced eventIds keyed off trajectoryId, source/lens="trajectories", structured taskTitle/etc., no client Task: prefix — server enriches), map_history_entry, self-contained epoch→ISO, home-dir path scrub, confidence float-on-wire. Handles all six trajectory-blob edge cases.
  • ai-hist-core/outbox.rsbuild_outbox_batch over local SQLite past a JSON-persistable SyncCursor + incognito exclusion (pure sync, no network).
  • ai-hist/cloud.rs + CLI: ai-hist login (/v1/cli/login), ai-hist admin-mint (dev /v1/admin/mint), ai-hist push (incremental, idempotent, --incognito, --json). ureq HTTP behind an Ingestor trait (orchestration unit-tested without a server); rth_at_/rth_rt_ token storage (0600); cursor persistence + server-confirmed advance; deterministic retry-safe batch_id.
  • docs/cloud-sync.md — human quickstart + automation/agent guide.

Verification

End-to-end via the shipped CLI across in-process → local wrangler devdeployed dev Workerprod posture (admin-mint→404, unauth→401 surfaced cleanly by the CLI). Trajectory rows byte-identical across 7 independent runs, signed off by trajectories-expert. Contract pinned to the WS-1 ADR; shares the traj_abc fixture with relayhistory-cloud's PGlite applyIngest test.

🤖 Generated with Claude Code

WS-9: add the relayhistory cloud-sync lens (Agent Relay Loop) directly
on main. Re-applied onto current main rather than via the stale
codex/rust-core-phase1-scaffold branch (#21), which main has since
superseded with a more advanced Rust CLI. Additive only — no regression
to main's existing crates.

ai-hist-core:
- convergence.rs — map the local recall store onto the ratified WS-1
  convergence envelope (relayhistory-cloud ADR 2026-06-21): ConvergenceEnvelope
  + IngestRequest/IngestResponse; map_trajectory (decisions+retro fan-out,
  finding/reflection/decision kinds, collision-free kind-namespaced eventIds,
  source/lens=trajectories, structured task fields, no client Task: prefix —
  server enriches); map_history_entry; self-contained epoch→ISO; home-dir path
  scrub; confidence float-on-wire (server toBasisPoints); all six trajectory
  blob edge cases.
- outbox.rs — build_outbox_batch over local SQLite past a JSON-persistable
  SyncCursor + incognito exclusion; pure sync, no network.

ai-hist (binary):
- cloud.rs — ureq HTTP behind an Ingestor trait (push orchestration unit-tested
  without a server); rth_at_/rth_rt_ token storage (0600); cursor persistence +
  server-confirmed advance; deterministic retry-safe batch_id; stable machine_id.
- login / admin-mint / push subcommands.

docs/cloud-sync.md — human quickstart + automation/agent guide.

Verified end-to-end across in-process, local wrangler dev, deployed dev
Worker, and prod posture (admin-mint→404, unauth→401) via the shipped CLI;
trajectory rows byte-identical across 7 runs. 30 tests (5 cli + 25 core),
no new clippy warnings. Supersedes PRs #21 + #20.

Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
@coderabbitai

coderabbitai Bot commented Jun 21, 2026

Copy link
Copy Markdown

Review Change Stack

Caution

Review failed

The pull request is closed.

ℹ️ Recent review info
⚙️ Run configuration

Configuration used: Organization UI

Review profile: CHILL

Plan: Pro Plus

Run ID: 7b27d18d-bc1a-44c2-a037-6b474364d012

📥 Commits

Reviewing files that changed from the base of the PR and between a015ebc and af469b7.

⛔ Files ignored due to path filters (1)
  • Cargo.lock is excluded by !**/*.lock
📒 Files selected for processing (8)
  • WS9_SCAFFOLD_PLAN.md
  • crates/ai-hist-core/src/convergence.rs
  • crates/ai-hist-core/src/lib.rs
  • crates/ai-hist-core/src/outbox.rs
  • crates/ai-hist/Cargo.toml
  • crates/ai-hist/src/cloud.rs
  • crates/ai-hist/src/main.rs
  • docs/cloud-sync.md

📝 Walkthrough

Walkthrough

Adds WS-9 cloud-sync to the relayhistory CLI. A new convergence module in ai-hist-core maps local HistoryEntry and trajectory rows into POST /v1/ingest wire envelopes. An outbox module builds cursor-advancing batches with incognito filtering. A new cloud module in ai-hist handles auth persistence, machine identity, HTTP push, and token bootstrap. Three CLI subcommands (login, admin-mint, push) are wired in main.rs. A planning document and user-facing docs are included.

Changes

WS-9 Cloud Sync

Layer / File(s) Summary
WS-9 scaffold plan
WS9_SCAFFOLD_PLAN.md
Complete written plan defining scope correction, build status, ingest endpoint contract, convergence envelope semantics (deterministic eventId, trajectory fan-out, scrub rules), repo topology, local store design, incognito acceptance criteria, sync orchestration, dependency gates, and open questions.
Convergence wire schema and utility functions
crates/ai-hist-core/src/convergence.rs, crates/ai-hist-core/src/lib.rs
Adds MachineIdentity, IngestRequest, IngestResponse, and ConvergenceEnvelope structs with camelCase serde renames; implements chrono-free epoch_ms_to_iso, POSIX/Windows normalize_home_path, and JSON field extraction helpers. Exposes convergence and outbox from the crate root.
HistoryEntry and TrajectoryRow convergence mappers
crates/ai-hist-core/src/convergence.rs
Implements map_history_entry (deterministic event_id, home-path scrubbing) and map_trajectory (decision + reflection/finding fan-out from decisions_json/retrospective_json with stable indexing and missing-id guards). Comprehensive unit tests cover all mapping paths, wire shape, and confidence null emission.
Outbox batch builder
crates/ai-hist-core/src/outbox.rs
Adds SyncCursor (monotonic history_id/trajectory_rowid watermarks), OutboxBatch, and build_outbox_batch which queries SQLite in order, skips incognito sessions while still advancing the cursor, and caps per-source output. Unit tests cover cursor advancement, incognito exclusion, fan-out, limiting, and JSON round-tripping.
Cloud binding layer
crates/ai-hist/src/cloud.rs, crates/ai-hist/Cargo.toml
Adds StoredAuth persistence (auth.json, 0600 best-effort permissions), cursor.json persistence, stable machine-id generation, deterministic batch_id, the Ingestor trait, push orchestration (outbox → IngestRequest → ingest → advance cursor), UreqIngestor HTTP transport for POST /v1/ingest, and admin_mint/login token bootstrap. Adds serde, ureq, and tempfile dependencies.
CLI subcommands and user docs
crates/ai-hist/src/main.rs, docs/cloud-sync.md
Extends the Command enum with Login, AdminMint, and Push variants; wires main match arms with MachineIdentity construction, cursor loading, and JSON/human-readable output. Adds docs/cloud-sync.md covering auth flows, incremental idempotent push, incognito exclusion, launchd/cron automation, troubleshooting, and internal implementation mapping.

Sequence Diagram

sequenceDiagram
  participant User
  participant CLI as ai-hist (main.rs)
  participant Cloud as cloud.rs
  participant Core as ai-hist-core outbox
  participant SQLite as Local SQLite DB
  participant Relay as POST /v1/ingest

  User->>CLI: ai-hist push [--limit N] [--incognito S]
  CLI->>Cloud: load_auth() / load_cursor()
  CLI->>Cloud: push(conn, client, auth, machine, cursor, limit, incognito)
  Cloud->>Core: build_outbox_batch(conn, cursor, limit, incognito)
  Core->>SQLite: SELECT history rows > history_id
  Core->>SQLite: SELECT trajectory rows > trajectory_rowid
  Core-->>Cloud: OutboxBatch { records, advanced_cursor }
  Cloud->>Relay: POST /v1/ingest { batch_id, machine, records, cursors }
  Relay-->>Cloud: IngestResponse { accepted, cursors }
  Cloud->>Cloud: save_cursor(advanced_cursor)
  Cloud-->>CLI: PushReport { sent, accepted, batch_id }
  CLI-->>User: JSON or human-readable summary
Loading

Estimated code review effort

🎯 4 (Complex) | ⏱️ ~60 minutes

Poem

🐇 Hop hop, the history flies,
Through outbox queues to cloudy skies!
A cursor leaps from row to row,
While incognito stays below.
Each envelope stamped and sealed with care —
The rabbit's sync is almost there! 🌥️

✨ Finishing Touches
📝 Generate docstrings
  • Create stacked PR
  • Commit on current branch
🧪 Generate unit tests (beta)
  • Create PR with unit tests
  • Commit unit tests in branch codex/ws9-on-main

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 and usage tips.

@kjgbot
kjgbot merged commit 7c4e88d into main Jun 21, 2026
2 of 3 checks passed
@kjgbot
kjgbot deleted the codex/ws9-on-main branch June 21, 2026 17:27

@gemini-code-assist gemini-code-assist 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.

Code Review

This pull request implements the WS-9 cloud-sync feature, enabling the local recall store (prompts and trajectory reasoning events) to be pushed to relayhistory-cloud. It introduces mapping logic to the WS-1 convergence envelope, an outbox batch builder with incognito session filtering, and a client transport layer using ureq with CLI commands for login, admin token minting, and pushing. The review feedback highlights a security vulnerability regarding non-atomic file permission setting for private files, cross-platform compatibility issues with hostname resolution on Windows, and a reliability concern due to missing timeouts on HTTP requests.

Important

The consumer version of Gemini Code Assist on GitHub is being sunset. Starting June 18, 2026, new organization installations will be blocked, and all code review activity will officially cease on July 17, 2026.
For more details on the timeline and next steps, please review the Help Documentation.

Comment on lines +61 to +73
fn write_private(path: &std::path::Path, body: &str) -> Result<()> {
if let Some(parent) = path.parent() {
fs::create_dir_all(parent)?;
}
fs::write(path, body)?;
// best-effort 0600 on unix (token/secret hygiene)
#[cfg(unix)]
{
use std::os::unix::fs::PermissionsExt;
let _ = fs::set_permissions(path, fs::Permissions::from_mode(0o600));
}
Ok(())
}

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-high high

The write_private function writes sensitive authentication tokens to disk using fs::write (which creates the file with default permissions) and only restricts the permissions to 0600 afterwards. This creates a race condition where another local user could read the sensitive tokens before the permissions are restricted. To prevent this, the file should be created with 0600 permissions atomically using OpenOptions with mode(0o600) on Unix.

fn write_private(path: &std::path::Path, body: &str) -> Result<()> {
    if let Some(parent) = path.parent() {
        fs::create_dir_all(parent)?;
    }
    use std::fs::OpenOptions;
    let mut options = OpenOptions::new();
    options.write(true).create(true).truncate(true);
    #[cfg(unix)]
    {
        use std::os::unix::fs::OpenOptionsExt;
        options.mode(0o600);
    }
    use std::io::Write;
    let mut file = options.open(path)?;
    file.write_all(body.as_bytes())?;
    Ok(())
}

Comment on lines +122 to +127
fn hostname() -> String {
std::env::var("HOSTNAME")
.ok()
.filter(|s| !s.is_empty())
.unwrap_or_else(|| "unknown-host".to_string())
}

Copy link
Copy Markdown

Choose a reason for hiding this comment

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

medium

The hostname function only checks the HOSTNAME environment variable, which is typically not set on Windows systems. On Windows, the standard environment variable for the host name is COMPUTERNAME. Adding a fallback to COMPUTERNAME ensures cross-platform compatibility.

Suggested change
fn hostname() -> String {
std::env::var("HOSTNAME")
.ok()
.filter(|s| !s.is_empty())
.unwrap_or_else(|| "unknown-host".to_string())
}
fn hostname() -> String {
std::env::var("HOSTNAME")
.or_else(|_| std::env::var("COMPUTERNAME"))
.ok()
.filter(|s| !s.is_empty())
.unwrap_or_else(|| "unknown-host".to_string())
}

.context("not authenticated — run `ai-hist login` or `ai-hist admin-mint` first")?;
let machine = MachineIdentity {
id: cloud::machine_id()?,
hostname: std::env::var("HOSTNAME").ok().filter(|s| !s.is_empty()),

Copy link
Copy Markdown

Choose a reason for hiding this comment

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

medium

For consistency and Windows compatibility, the hostname resolution here should also fall back to COMPUTERNAME if HOSTNAME is not set.

Suggested change
hostname: std::env::var("HOSTNAME").ok().filter(|s| !s.is_empty()),
hostname: std::env::var("HOSTNAME").or_else(|_| std::env::var("COMPUTERNAME")).ok().filter(|s| !s.is_empty()),

Comment on lines +206 to +209
let resp = ureq::post(&url)
.set("Authorization", &format!("Bearer {}", auth.access_token))
.set("Content-Type", "application/json")
.send_json(serde_json::to_value(req)?);

Copy link
Copy Markdown

Choose a reason for hiding this comment

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

medium

The ureq HTTP requests are initiated without any timeout. If the server is slow or hangs, the CLI sync process will block indefinitely. It is highly recommended to set a reasonable timeout (e.g., 30 seconds) on all network requests to ensure reliability, especially when run in automated environments like cron or launchd.

        let resp = ureq::post(&url)
            .timeout(std::time::Duration::from_secs(30))
            .set("Authorization", &format!("Bearer {}", auth.access_token))
            .set("Content-Type", "application/json")
            .send_json(serde_json::to_value(req)?);

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