Skip to content

feat: cloud push SDK (drives the ai-hist binary) + installer test fix - #38

Merged
khaliqgant merged 7 commits into
mainfrom
feat/cloud-push-autosync
Jul 7, 2026
Merged

feat: cloud push SDK (drives the ai-hist binary) + installer test fix#38
khaliqgant merged 7 commits into
mainfrom
feat/cloud-push-autosync

Conversation

@khaliqgant

@khaliqgant khaliqgant commented Jul 6, 2026

Copy link
Copy Markdown
Member

Makes relayhistory cloud sync automatic and available as an SDK, without re-implementing the push logic.

1. sdk-ts: cloud push that drives the Rust binary

ai-hist/cloud exports pushToCloud, a thin wrapper over the real ai-hist push --json binary — it resolves the binary ($AI_HIST_RUST_BIN → install.sh location → ai-hist on PATH), spawns it, and parses {sent, accepted, batchId, cursor}.

This replaces an earlier ~500-line TypeScript port of the Rust push pipeline. Per review discussion, a port was the wrong call: it duplicated batching/cursor/dedup logic that must stay byte-for-byte wire-compatible with the Rust client forever. Driving the binary keeps one source of truth (the Rust ai-hist push) and drops the sql.js DB reading entirely. pushToCloud resolves null (no-op) when the binary is missing or the user isn't authenticated; rejects on other failures. Net −900 lines.

2. Installer test fix (was breaking CI verify)

The from-source install test sandboxed $HOME, which blinds rustup (it resolves the toolchain via ~/.rustup / ~/.cargo) during cargo buildrustup could not choose a version of cargo. AI_HIST_NO_AUTOSYNC=1 alone is the real launchd-pollution fix; the HOME override is removed from the cargo path (kept on binary-mode tests, which don't compile).

3. ai-hist push --install-service (CLI parity)

Standalone launchd/cron push service (com.ai-hist.push), independent of the SDK path above.

Testing

  • cargo build/cargo test --no-run
  • sdk-ts npm test ✅ (wrapper tests: arg forwarding, JSON parse, ENOENT→null, not-authed→null, error→reject)
  • pytest test_install.py / test_cli_dispatch.py

🤖 Generated with Claude Code

…obber

Two related fixes so local and cloud history sync are both automatic and
never fight the developer's real machine state:

- Add `ai-hist push --install-service/--uninstall-service/--interval`. The
  launchd/cron plumbing is generalized behind a `ServiceSpec` shared by the
  existing `sync` service and the new `push` service (com.ai-hist.push,
  default 300s). `push --install-service` doesn't require auth so it can be
  scheduled before first login; the job itself authenticates with the stored
  rth_at_ token. This is what `agent-relay reflex on` will call to make cloud
  upload automatic.

- Fix test isolation in test_install.py: both installer runs inherited the
  real $HOME and omitted AI_HIST_NO_AUTOSYNC, so install.sh's auto-sync step
  overwrote ~/Library/LaunchAgents/com.ai-hist.sync.plist with a pointer to a
  pytest tmp binary — silently breaking the developer's real sync service
  (EX_CONFIG, launchd penalty box). Sandbox HOME + set AI_HIST_NO_AUTOSYNC=1
  for the install subprocesses, and add a regression test asserting the
  opt-out writes no plist.

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

cursor Bot commented Jul 6, 2026

Copy link
Copy Markdown

Bugbot is not enabled for your account, so this pull request was not reviewed.

Enable Bugbot in the Cursor dashboard to get automatic reviews on future PRs.

@coderabbitai

coderabbitai Bot commented Jul 6, 2026

Copy link
Copy Markdown

Review Change Stack

Warning

Review limit reached

@khaliqgant, you've reached your PR review limit, so we couldn't start this review.

Next review available in: 52 minutes

Enable usage-based reviews in Billing to review now. Otherwise, wait until the next included review is available.
You're only billed for reviews past your plan's rate limits ($0.25/file).

How can I continue?

After more reviews become available, a review can be triggered using the @coderabbitai review command as a PR comment. Alternatively, push new commits to this PR.

To avoid repeated limits, reduce automatic review volume by pausing incremental auto-reviews earlier, using label-based review opt-in, excluding WIP or generated PR titles, or requesting reviews manually when the PR is ready. If your team needs uninterrupted high-volume reviews, an organization admin can enable usage-based reviews.

How do review limits work?

CodeRabbit enforces per-developer PR review limits for each organization. Most developers receive the normal plan review availability.

For paid Pro and Pro+ PR reviews, CodeRabbit uses adaptive limits for sustained high-volume activity. When a developer's recent PR review activity reaches the 95th percentile or higher among CodeRabbit users, additional reviews become available more gradually as earlier reviews age out of the rolling window.

Please refer docs for additional details.

Review details
⚙️ Run configuration

Configuration used: Organization UI

Review profile: CHILL

Plan: Pro Plus

Run ID: 34a7a0f7-af5c-4d23-806f-cdc05270c513

📥 Commits

Reviewing files that changed from the base of the PR and between d19c79d and 78581be.

📒 Files selected for processing (1)
  • crates/ai-hist/src/main.rs
📝 Walkthrough

Walkthrough

Adds a shared managed-service path for sync and push, documents continuous cloud push, and introduces a TypeScript wrapper that resolves and runs ai-hist push --json with SDK exports, tests, and release metadata.

Changes

Managed push/sync service

Layer / File(s) Summary
ServiceSpec and service plumbing
crates/ai-hist/src/main.rs
Introduces ServiceSpec-driven launchd and cron generation, shared install/uninstall helpers, and spec-based service messages for sync and push.
CLI wiring for Push and Sync commands
crates/ai-hist/src/main.rs
Updates Push interval and service flags, and routes Sync and Push install/uninstall paths through the shared managed-service helpers.
Push docs and installer isolation
README.md, test_install.py
Documents continuous cloud push and updates installer tests to opt out of autosync, sandbox HOME, and verify launchd remains untouched when autosync is disabled.

TypeScript cloud push wrapper

Layer / File(s) Summary
Cloud push wrapper and resolution
sdk-ts/src/cloud-push.ts
Defines push report/options types, resolves the Rust binary, and implements pushToCloud by spawning ai-hist push --json and parsing its output.
SDK exports and release metadata
sdk-ts/src/cloud-client.ts, sdk-ts/CHANGELOG.md, sdk-ts/package.json
Re-exports the cloud push API, records it in the changelog, and bumps the package version to 0.4.0.
Cloud push wrapper tests
sdk-ts/src/cloud-push.test.ts
Adds spawn stubs and coverage for JSON parsing, argument forwarding, missing-binary handling, authentication failures, non-zero exits, and empty stdout.

Estimated code review effort: 4 (Complex) | ~60 minutes

Possibly related PRs

Poem

A rabbit hops where cron ticks gently tune,
Then darts to cloud under a silver moon.
One spec, one push, one tidy binary trail,
And type-safe tests that help the burrows sail. 🐇

🚥 Pre-merge checks | ✅ 5
✅ Passed checks (5 passed)
Check name Status Explanation
Title check ✅ Passed The title captures the main SDK cloud push change and the installer test fix, both central parts of the PR.
Description check ✅ Passed The description matches the PR scope, covering the SDK wrapper, installer test fix, and push-service support.
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.
✨ Finishing Touches
🧪 Generate unit tests (beta)
  • Create PR with unit tests
  • Commit unit tests in branch feat/cloud-push-autosync

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.

@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 introduces a continuous cloud push background service (push) alongside the existing local sync service, refactoring the background service management into a generic ServiceSpec struct to share launchd and cron installation logic. It also updates the documentation and sandboxes the installation tests to prevent overwriting real user configurations. Feedback on the changes suggests improving the Linux cron service installation by supporting step-value intervals (e.g., every 5 minutes), properly quoting the binary path to handle spaces, and dynamically displaying the scheduled interval in the output message.

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 +1787 to 1815
fn install_cron_service(spec: &ServiceSpec, bin: &str, interval: u64) -> Result<()> {
if interval != 60 {
eprintln!(
"Note: cron runs at 1-minute granularity; ignoring --interval={interval} and \
scheduling every minute."
);
}
let line = format!("* * * * * {bin} sync >> /tmp/ai-hist-sync.log 2>&1 {CRON_MARKER}");
let marker = cron_marker(spec);
let line = format!(
"* * * * * {bin} {} >> /tmp/{}.log 2>&1 {marker}",
spec.subcommand, spec.log_stem
);
// Drop any previously managed line, then append the current one.
let mut lines: Vec<String> = read_crontab()
.lines()
.filter(|l| !l.contains(CRON_MARKER))
.filter(|l| !l.contains(&marker))
.map(str::to_string)
.collect();
lines.push(line);
write_crontab(&format!("{}\n", lines.join("\n")))?;

println!("Installed cron sync job; syncing every minute.");
println!("Installed cron {} job; running every minute.", spec.human);
println!(" view: crontab -l");
println!(" remove: ai-hist sync --uninstall-service");
println!(
" remove: ai-hist {} --uninstall-service",
spec.subcommand
);
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.

medium

There are three opportunities to improve the cron service installation on Linux:

  1. Interval Granularity: Currently, any interval other than 60 seconds (including the default 300 seconds for the push service) is ignored and scheduled to run every minute (* * * * *). Since cron supports step values (e.g., */5 * * * *), we can support intervals that are multiples of 60 seconds up to 59 minutes.
  2. Shell Quoting: The binary path (bin) is inserted directly into the crontab line without quoting. If the installation path contains spaces or other shell-sensitive characters, the cron job will fail to parse. We can use the existing sh_single_quote helper to safely escape the binary path.
  3. Dynamic Output Message: The confirmation message is hardcoded to say "running every minute." If a custom interval (like 5 minutes) is successfully scheduled, the output should dynamically reflect the actual interval.
fn install_cron_service(spec: &ServiceSpec, bin: &str, interval: u64) -> Result<()> {
    let minutes = (interval + 29) / 60;
    let minutes = minutes.max(1);
    if interval % 60 != 0 {
        eprintln!(
            "Note: cron runs at 1-minute granularity; rounding --interval={interval}s to {minutes} minute(s)."
        );
    }
    let schedule = if minutes > 1 && minutes < 60 {
        format!("*/{minutes} * * * *")
    } else {
        "* * * * *".to_string()
    };
    let marker = cron_marker(spec);
    let safe_bin = sh_single_quote(bin);
    let line = format!(
        "{schedule} {safe_bin} {} >> /tmp/{}.log 2>&1 {marker}",
        spec.subcommand, spec.log_stem
    );
    // Drop any previously managed line, then append the current one.
    let mut lines: Vec<String> = read_crontab()
        .lines()
        .filter(|l| !l.contains(&marker))
        .map(str::to_string)
        .collect();
    lines.push(line);
    write_crontab(&format!("{}\n", lines.join("\n")))?;

    if minutes > 1 {
        println!("Installed cron {} job; running every {} minutes.", spec.human, minutes);
    } else {
        println!("Installed cron {} job; running every minute.", spec.human);
    }
    println!("  view:   crontab -l");
    println!(
        "  remove: ai-hist {} --uninstall-service",
        spec.subcommand
    );
    Ok(())
}

@devin-ai-integration devin-ai-integration 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.

Devin Review found 2 potential issues.

Open in Devin Review

Comment on lines 1690 to 1694
anyhow::bail!(
"Automatic sync service install is only supported on macOS and Linux. \
Run `ai-hist watch` to keep syncing in the foreground instead."
"Automatic {} service install is only supported on macOS and Linux. \
Run `ai-hist watch` to keep syncing in the foreground instead.",
spec.human
)

Copy link
Copy Markdown

Choose a reason for hiding this comment

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

🟡 Error message on unsupported platforms incorrectly suggests 'watch' as a workaround for cloud push

The fallback error message for unsupported platforms always suggests running ai-hist watch (install_managed_service at crates/ai-hist/src/main.rs:1692), but watch only performs local sync — it does not push to the cloud, so the suggestion is wrong when installing the push service.

Impact: Users on unsupported platforms who try ai-hist push --install-service receive an incorrect workaround that won't actually push history to the cloud.

Mechanism: the generic error message doesn't account for the push service

The install_managed_service function at crates/ai-hist/src/main.rs:1682-1696 is shared by both SYNC_SERVICE and PUSH_SERVICE. The error message on line 1692 unconditionally says "Run ai-hist watch to keep syncing in the foreground instead." However, watch_loop at crates/ai-hist/src/main.rs:1621 only calls sync_basic, which does local history sync — it never calls cloud::push. When spec.human is "cloud push", the message reads: "Automatic cloud push service install is only supported on macOS and Linux. Run ai-hist watch to keep syncing in the foreground instead." — suggesting a command that doesn't perform the desired action.

Suggested change
anyhow::bail!(
"Automatic sync service install is only supported on macOS and Linux. \
Run `ai-hist watch` to keep syncing in the foreground instead."
"Automatic {} service install is only supported on macOS and Linux. \
Run `ai-hist watch` to keep syncing in the foreground instead.",
spec.human
)
anyhow::bail!(
"Automatic {} service install is only supported on macOS and Linux.",
spec.human
)
Open in Devin Review

Was this helpful? React with 👍 or 👎 to provide feedback.

Comment on lines +1787 to 1792
fn install_cron_service(spec: &ServiceSpec, bin: &str, interval: u64) -> Result<()> {
if interval != 60 {
eprintln!(
"Note: cron runs at 1-minute granularity; ignoring --interval={interval} and \
scheduling every minute."
);

Copy link
Copy Markdown

Choose a reason for hiding this comment

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

🔍 Push service on Linux will run every minute instead of every 5 minutes

The push service's default interval is 300 seconds (crates/ai-hist/src/main.rs:284), but install_cron_service at crates/ai-hist/src/main.rs:1787-1792 ignores the interval and always schedules * * * * * (every minute) because cron only supports 1-minute granularity. This means on Linux, ai-hist push --install-service will push every 60 seconds rather than every 300 seconds. The function does print a warning, but this is a significant behavioral difference from macOS. Consider whether the push command should implement its own interval guard (e.g. checking a timestamp file to skip runs within the interval window).

Open in Devin Review

Was this helpful? React with 👍 or 👎 to provide feedback.

@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: 183ba7bf67

ℹ️ About Codex in GitHub

Your team has set up Codex to 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 👍.

Codex can also answer questions or update the PR. Try commenting "@codex address that feedback".

Comment on lines +740 to +741
if install_service {
return install_managed_service(&PUSH_SERVICE, interval);

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 Reject push filters when installing the service

With inputs such as ai-hist push --install-service --incognito <session>, clap accepts the privacy filter but this early return discards it and installs a service that later runs plain ai-hist push. That can upload the very session the user tried to exclude; either make service installation conflict with one-off push options like --incognito/--limit or persist those arguments into the scheduled command.

Useful? React with 👍 / 👎.

Comment thread crates/ai-hist/src/main.rs Outdated
Comment on lines +1795 to +1797
let line = format!(
"* * * * * {bin} {} >> /tmp/{}.log 2>&1 {marker}",
spec.subcommand, spec.log_stem

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 Honor the push service interval on cron

On Linux, ai-hist push --install-service uses the new default interval of 300 seconds, but this shared cron installer always writes * * * * *, so the push job runs every minute after ignoring --interval=300. That contradicts the documented/default five-minute cadence and can increase cloud traffic significantly; render an appropriate cron schedule for push's default or make the Linux/default behavior explicitly 60 seconds.

Useful? React with 👍 / 👎.

Comment thread test_install.py Outdated
# plist pointing at this throwaway tmp binary. Sandbox HOME too as
# defense in depth.
"AI_HIST_NO_AUTOSYNC": "1",
"HOME": str(tmp_path / "install-home"),

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 the Rust toolchain home in install tests

In environments where cargo is the rustup shim and RUSTUP_HOME/CARGO_HOME are not exported, this source-mode installer test now runs cargo build with HOME pointed at a fresh temp directory, so rustup cannot find the configured toolchain and the test fails before it reaches the autosync regression. Keep the launchd sandboxing without hiding the Rust toolchain, for example by preserving the original cargo/rustup homes or only sandboxing HOME for the service-install step.

Useful? React with 👍 / 👎.

Reflex is documented in relayhistory-cloud, not here; keep the ai-hist docs
scoped to the `push`/`sync` services themselves.

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

cursor Bot commented Jul 6, 2026

Copy link
Copy Markdown

Bugbot is not enabled for your account, so this pull request was not reviewed.

Enable Bugbot in the Cursor dashboard to get automatic reviews on future PRs.

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

🧹 Nitpick comments (1)
test_install.py (1)

210-259: 📐 Maintainability & Code Quality | 🔵 Trivial | ⚡ Quick win

Duplicate fake-binary/env setup with the adjacent test.

The fake_binary script (lines 224-231) and the binary-mode env dict (lines 233-245) are near-identical copies of lines 141-148 and 151-164 in test_install_script_binary_mode_does_not_require_cargo. Extracting a small helper (e.g. _write_fake_ai_hist_binary(path) and a _binary_install_env(bin_dir, install_dir, home, extra=None) builder) would avoid drift between the two tests as install.sh's env contract evolves.

♻️ Sketch of shared helper
def _write_fake_ai_hist_binary(path: Path) -> None:
    path.write_text(
        "#!/bin/sh\n"
        "if [ \"$1\" = \"--version\" ]; then echo 'ai-hist 9.9.9'; exit 0; fi\n"
        "if [ \"$1\" = \"recent\" ]; then echo '[]'; exit 0; fi\n"
        "echo \"fake ai-hist: $*\"\n"
    )
    path.chmod(0o755)

Also note fake_tools (line 221-222) is created but unused in this test (no fake_cargo stub is added here), which the extraction would naturally clean up.

🤖 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 `@test_install.py` around lines 210 - 259, This test duplicates the fake
ai-hist binary setup and binary-mode environment construction from the adjacent
install test, and the unused fake_tools setup adds extra noise. Extract shared
helpers for creating the fake binary and building the binary-install env, then
update this test and test_install_script_binary_mode_does_not_require_cargo to
call them so the install.sh env contract stays consistent and the dead
fake_tools setup can be removed.
🤖 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 `@crates/ai-hist/src/main.rs`:
- Around line 1787-1798: The warning in install_cron_service is misleading
because it treats the push command’s default interval as if the user explicitly
passed it. Update the message logic around install_cron_service and the interval
check so it only implies an override when the caller actually supplied a
non-default value, or otherwise phrase it as cron’s fixed 1-minute granularity
without referencing --interval={interval}. Keep the cron scheduling behavior
unchanged and use the existing symbols install_cron_service, ServiceSpec, and
interval to locate the message.

In `@README.md`:
- Around line 249-265: The push service quick-reference currently advertises the
default cadence without the Linux cron limitation, which can mislead users.
Update the README’s push section to match the sync section by mentioning the
`--interval` option and adding the same cron granularity caveat used by
`install_cron_service` in `main.rs` so Linux users know cron still runs at
1-minute intervals.

---

Nitpick comments:
In `@test_install.py`:
- Around line 210-259: This test duplicates the fake ai-hist binary setup and
binary-mode environment construction from the adjacent install test, and the
unused fake_tools setup adds extra noise. Extract shared helpers for creating
the fake binary and building the binary-install env, then update this test and
test_install_script_binary_mode_does_not_require_cargo to call them so the
install.sh env contract stays consistent and the dead fake_tools setup can be
removed.
🪄 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 Plus

Run ID: a52fc8a9-b45b-4db6-8c76-a0a009fbfb8d

📥 Commits

Reviewing files that changed from the base of the PR and between 7fdc30f and 183ba7b.

📒 Files selected for processing (3)
  • README.md
  • crates/ai-hist/src/main.rs
  • test_install.py

Comment thread crates/ai-hist/src/main.rs
Comment thread README.md
@agent-relay-code

Copy link
Copy Markdown
Contributor

ℹ️ pr-reviewer: review only — no file changes were applied to the PR (nothing to commit after review). The notes below are advisory and were not pushed.

The reflex implementation lives in agent-relay (a separate tool/repo, per the README text "agent-relay reflex on"), not in this repo — so the README's claim about reflex installing the push service is cross-repo and out of scope to verify here. The main.rs mention of reflex is just the doc comment I already reviewed. This is consistent and not a defect in this PR.

The review is complete. Let me summarize findings.

Review Summary

PR #38 (feat/cloud-push-autosync) refactors the single-purpose sync background-service installer into a generic ServiceSpec-driven abstraction and adds a parallel push (cloud upload) background service, plus installer test hardening.

Verification performed (as CI runs it)

  • cargo build -q -p ai-hist-clipassed, no warnings
  • cargo test --workspacepassed (28 + 27 + 0 tests)
  • python -m pytest test_install.pypassed (3/3)
  • python -m pytest test_ai_hist.py test_cli_dispatch.pypassed (187)

One initial test_install.py failure was a sandbox-only artifact: the source-build test overrides $HOME, and my freshly-installed rustup stores its default toolchain under $HOME/.rustup, so cargo couldn't resolve inside the subprocess. Setting RUSTUP_HOME explicitly made all 3 pass. CI uses dtolnay/rust-toolchain@stable (system-wide default), so this does not affect CI. Not a PR defect; no code change made.

Findings

  • Refactor correctness: All callers of the renamed functions (install_managed_service, uninstall_managed_service, launchd_plist_path(spec), cron_marker(spec)) are updated. No stale references to old symbols (install_sync_service, SYNC_SERVICE_LABEL, CRON_MARKER, launchd_plist_path()) remain anywhere in the repo.
  • CLI wiring: Push command's new install_service / uninstall_service / interval args are correctly destructured and dispatched (main.rs:740-745), mirroring Sync (main.rs:610-616). uninstall_service has conflicts_with = "install_service" on both.
  • Distinct service identity: SYNC_SERVICE and PUSH_SERVICE use distinct labels, subcommands, log stems, and cron markers, so the two services don't collide in launchd, cron, or /tmp log files. The cron filter uses the spec-specific marker, so uninstalling one job won't remove the other.
  • install.sh / test alignment: The new AI_HIST_NO_AUTOSYNC guard (install.sh:295-296) emits "skipping auto-sync service install", exactly matching the new regression test's assertion, and short-circuits before the sync --install-service call. The HOME sandboxing added to the two existing tests is a genuine safety fix (prevents clobbering the developer's real ~/Library/LaunchAgents).

No semantic, safety, or fail-closed defaults were altered. No lifecycle/reaper/dispatch code touched. No mechanical (lint/format/typo) issues found requiring auto-edit. The working tree is unchanged from the PR head.

Addressed comments

  • No bot or reviewer comments are present in .workforce/ (context.json contains no review threads), so there is nothing to address.

Advisory Notes

  • The README states agent-relay reflex on/off installs/removes the push service. That reflex logic lives in the separate agent-relay tool, not this repo, so the end-to-end claim can't be verified from this checkout. Worth confirming the corresponding agent-relay change ships alongside this PR — but it is out of scope for this repo's diff and requires no change here.

The PR is coherent, well-scoped, and all CI-equivalent build/test steps pass locally. The remaining decision (merge, and the cross-repo reflex coordination) requires human judgment.

@cubic-dev-ai cubic-dev-ai 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.

6 issues found and verified against the latest diff

Prompt for AI agents (unresolved issues)

Check if these issues are valid — if so, understand the root cause of each and fix them. If appropriate, use sub-agents to investigate and fix each issue separately.


<file name="crates/ai-hist/src/main.rs">

<violation number="1" location="crates/ai-hist/src/main.rs:741">
P1: Installing the push service currently ignores one-off push filters like `--incognito` and `--limit` because this early return bypasses the normal push execution path. It would be safer to reject those combinations (or persist them) so users don’t think privacy/filter flags are applied to the scheduled job.</violation>

<violation number="2" location="crates/ai-hist/src/main.rs:1692">
P3: For `push --install-service` on unsupported platforms, this fallback suggests `ai-hist watch`, but `watch` is a local sync loop and does not perform cloud upload. A subcommand-specific fallback message would avoid sending users to a workflow that doesn’t match cloud push.</violation>

<violation number="3" location="crates/ai-hist/src/main.rs:1796">
P2: Linux scheduled push/sync can fail to execute when the ai-hist binary path contains spaces or shell-significant characters. The crontab command interpolates `{bin}` unquoted, so quoting the executable path before writing the cron line would make the managed job robust.</violation>

<violation number="4" location="crates/ai-hist/src/main.rs:1796">
P2: This cron entry runs every minute regardless of `--interval`, so `ai-hist push --install-service` on Linux runs at 60s even with the 300s default. That changes upload cadence and can increase cloud traffic; consider mapping whole-minute intervals to cron step schedules or aligning Linux defaults/docs to one minute.</violation>
</file>

<file name="README.md">

<violation number="1" location="README.md:256">
P3: This quick-reference line advertises a 300s default without noting that Linux cron currently runs every minute. Adding the same cron-granularity caveat used in the sync section would prevent a platform-specific cadence mismatch in the docs.</violation>
</file>

<file name="test_install.py">

<violation number="1" location="test_install.py:26">
P2: Setting `HOME` to a fresh temp directory here can hide default `CARGO_HOME`/`RUSTUP_HOME` locations in source-mode installs, so `cargo build` may fail before the autosync regression is exercised. Preserving or explicitly passing cargo/rustup home paths would keep the launchd sandboxing without breaking toolchain discovery.</violation>
</file>

Reply with feedback, questions, or to request a fix.

Re-trigger cubic

Comment thread crates/ai-hist/src/main.rs
Comment thread crates/ai-hist/src/main.rs Outdated
Comment thread crates/ai-hist/src/main.rs Outdated
Comment thread test_install.py Outdated
Comment thread crates/ai-hist/src/main.rs Outdated
Comment thread README.md Outdated
@agent-relay-code

Copy link
Copy Markdown
Contributor

Review Summary

PR #38 (feat/cloud-push-autosync) generalizes the launchd/cron service plumbing into a shared ServiceSpec and adds ai-hist push --install-service/--uninstall-service, plus installer auto-sync opt-out tests and README docs.

What I verified

Rust refactor (crates/ai-hist/src/main.rs) — Correct and clean:

  • ServiceSpec abstraction with SYNC_SERVICE/PUSH_SERVICE is sound. All call sites updated: Command::Sync (main.rs:610-613) and Command::Push (main.rs:740-744).
  • Backward-compat preserved: cron_marker(&SYNC_SERVICE) yields # ai-hist sync (managed) — byte-identical to the old CRON_MARKER constant, so existing installed sync crontab lines are still matched and cleaned on reinstall/uninstall. The old plist label com.ai-hist.sync is likewise preserved.
  • Old constants/functions (CRON_MARKER, SYNC_SERVICE_LABEL, install_sync_service, uninstall_sync_service, zero-arg launchd_plist_path) are fully removed with no dangling references.
  • Push install/uninstall branches return early before the auth check, so --install-service works without a token (correct — the scheduled job itself authenticates later).
  • cargo build -p ai-hist-cli and cargo test --workspace both pass (55 tests, 0 failures, no warnings).

Required fix (CI-failing) — left for human, not auto-edited

test_install.py:26 breaks CI. The PR added "HOME": str(tmp_path / "install-home") to test_install_script_installs_working_launchers_from_source. That test builds ai-hist from source (AI_HIST_SOURCE_DIR + AI_HIST_BUILD_PROFILE=debug), which runs cargo build. dtolnay/rust-toolchain@stable (used in .github/workflows/ci.yml) makes the toolchain available via rustup default, stored in $HOME/.rustup/settings.toml — it does not export RUSTUP_TOOLCHAIN. Overriding HOME to a fresh tmp dir strips that default, so cargo fails:

error: rustup could not choose a version of cargo to run, because one wasn't specified explicitly, and no default is configured.

I reproduced this deterministically (env -u RUSTUP_TOOLCHAIN HOME=/tmp/fresh cargo build → exit 1). The sibling test_install_script_binary_mode_does_not_require_cargo also got the HOME override but passes, because binary mode never invokes cargo. Only the source-build test is affected, and it does run in CI.

I did not edit the test. The rules forbid me from modifying tests to make CI pass — how to reconcile the launchd-sandboxing intent with rustup resolution is a design decision. Suggested fix for the author (choose one):

  • Drop the HOME override from the source-build test and rely on the primary guard AI_HIST_NO_AUTOSYNC=1 (already present), which alone prevents the plist write; or
  • Keep HOME sandboxed but also pass through RUSTUP_HOME/CARGO_HOME (and PATH) from the real environment so the toolchain still resolves.

The binary-mode tests and the new test_install_script_autosync_opt_out_leaves_launchd_untouched are correct as written.

Addressed comments

  • No bot or human review comments were provided in .workforce/context.json or the .workforce/ directory, so there are no external threads to account for.

Advisory Notes

  • None. The change is well-scoped to its stated purpose (cloud-push autosync + shared service plumbing).

I made no file edits; the working tree is unchanged (aside from the pre-existing, unrelated memory/workspace/.relay/state.json modification I did not touch). Because a required CI check (test_install.py) will fail until the human resolves the HOME-override issue above, this PR is not ready to hand back.

Adds `pushToCloud` (exported from `ai-hist/cloud`) plus the primitives it
needs — `buildOutboxBatch`, `promptHash`, `batchId`, `machineId`, cursor
load/save, `normalizeHomePath`. This is a faithful TypeScript port of the
Rust `ai-hist push` (crates/ai-hist/src/cloud.rs + ai-hist-core outbox/
convergence): identical envelope shapes, eventId formats, prompt/batch
hashing, and cursor semantics, so the server's (orgId, machineId, batchId)
batch dedup and per-event upsert keys line up regardless of which client
pushes.

Auth reuses the SDK's existing ~/.config/ai-hist/auth.json; cursor and
machine-id persist alongside it. Lets the Agent Relay runtime sync Reflex
history in-process instead of shelling out to the CLI.

Tests cover the hashing vectors, path scrub, cursor advance (incl. incognito
skip), and history/trajectory/compacted mapping against a real sql.js DB.

Bumps sdk-ts to 0.4.0 (new public API).

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

cursor Bot commented Jul 6, 2026

Copy link
Copy Markdown

Bugbot is not enabled for your account, so this pull request was not reviewed.

Enable Bugbot in the Cursor dashboard to get automatic reviews on future PRs.

@cubic-dev-ai cubic-dev-ai 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.

1 issue found across 5 files (changes from recent commits).

Prompt for AI agents (unresolved issues)

Check if these issues are valid — if so, understand the root cause of each and fix them. If appropriate, use sub-agents to investigate and fix each issue separately.


<file name="README.md">

<violation number="1" location="README.md:256">
P3: This quick-reference line advertises a 300s default without noting that Linux cron currently runs every minute. Adding the same cron-granularity caveat used in the sync section would prevent a platform-specific cadence mismatch in the docs.</violation>
</file>

<file name="crates/ai-hist/src/main.rs">

<violation number="1" location="crates/ai-hist/src/main.rs:741">
P1: Installing the push service currently ignores one-off push filters like `--incognito` and `--limit` because this early return bypasses the normal push execution path. It would be safer to reject those combinations (or persist them) so users don’t think privacy/filter flags are applied to the scheduled job.</violation>

<violation number="2" location="crates/ai-hist/src/main.rs:1692">
P3: For `push --install-service` on unsupported platforms, this fallback suggests `ai-hist watch`, but `watch` is a local sync loop and does not perform cloud upload. A subcommand-specific fallback message would avoid sending users to a workflow that doesn’t match cloud push.</violation>

<violation number="3" location="crates/ai-hist/src/main.rs:1796">
P2: Linux scheduled push/sync can fail to execute when the ai-hist binary path contains spaces or shell-significant characters. The crontab command interpolates `{bin}` unquoted, so quoting the executable path before writing the cron line would make the managed job robust.</violation>

<violation number="4" location="crates/ai-hist/src/main.rs:1796">
P2: This cron entry runs every minute regardless of `--interval`, so `ai-hist push --install-service` on Linux runs at 60s even with the 300s default. That changes upload cadence and can increase cloud traffic; consider mapping whole-minute intervals to cron step schedules or aligning Linux defaults/docs to one minute.</violation>
</file>

<file name="sdk-ts/src/cloud-client.ts">

<violation number="1" location="sdk-ts/src/cloud-client.ts:22">
P2: Circular dependency introduced between `cloud-client.ts` and `cloud-push.ts`. The re-export block (lines 6–22) makes `cloud-client` depend on `cloud-push`, while `cloud-push` already imports `loadStoredRelayhistoryAuth` and `RelayhistoryAuth` from `cloud-client`. This works today because function declarations are hoisted in ESM, but the cycle makes the module graph fragile: adding a top-level constant, expression, or side effect in either module could silently produce `undefined` bindings at module-evaluation time. Consider breaking the cycle by extracting the shared auth types/function into a separate module (e.g. `cloud-auth.ts`) or removing the re-exports and having consumers import from `cloud-push` directly.</violation>
</file>

Tip: Review your code locally with the cubic CLI to iterate faster.

Re-trigger cubic

Comment thread sdk-ts/src/cloud-client.ts Outdated
type PushReport,
type PushOptions,
type OutboxBatch,
} from './cloud-push.js';

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: Circular dependency introduced between cloud-client.ts and cloud-push.ts. The re-export block (lines 6–22) makes cloud-client depend on cloud-push, while cloud-push already imports loadStoredRelayhistoryAuth and RelayhistoryAuth from cloud-client. This works today because function declarations are hoisted in ESM, but the cycle makes the module graph fragile: adding a top-level constant, expression, or side effect in either module could silently produce undefined bindings at module-evaluation time. Consider breaking the cycle by extracting the shared auth types/function into a separate module (e.g. cloud-auth.ts) or removing the re-exports and having consumers import from cloud-push directly.

Prompt for AI agents
Check if this issue is valid — if so, understand the root cause and fix it. At sdk-ts/src/cloud-client.ts, line 22:

<comment>Circular dependency introduced between `cloud-client.ts` and `cloud-push.ts`. The re-export block (lines 6–22) makes `cloud-client` depend on `cloud-push`, while `cloud-push` already imports `loadStoredRelayhistoryAuth` and `RelayhistoryAuth` from `cloud-client`. This works today because function declarations are hoisted in ESM, but the cycle makes the module graph fragile: adding a top-level constant, expression, or side effect in either module could silently produce `undefined` bindings at module-evaluation time. Consider breaking the cycle by extracting the shared auth types/function into a separate module (e.g. `cloud-auth.ts`) or removing the re-exports and having consumers import from `cloud-push` directly.</comment>

<file context>
@@ -2,6 +2,25 @@ import { readFile, writeFile, mkdir } from 'node:fs/promises';
+  type PushReport,
+  type PushOptions,
+  type OutboxBatch,
+} from './cloud-push.js';
+
 export interface RelayhistoryAuth {
</file context>

@khaliqgant khaliqgant changed the title feat(ai-hist): schedulable cloud push + fix installer test launchd clobber feat: in-process cloud push SDK + push service + installer test fix Jul 6, 2026
… to TS

Replaces the ~500-line TypeScript port of the Rust push pipeline with a thin
wrapper: `pushToCloud` now resolves the `ai-hist` binary
($AI_HIST_RUST_BIN → install.sh location → PATH) and spawns
`ai-hist push --json`, parsing `{sent, accepted, batchId, cursor}`. The Rust
binary stays the single source of truth for batching/cursor/dedup — no more
wire-compatibility duplication to keep in sync. Resolves null (no-op) when the
binary is missing or the user isn't authenticated; rejects on other failures.
Also exports `resolveAiHistBinary`. Drops the sql.js-based DB reading and the
outbox/convergence/hash re-implementations.

Also fixes the from-source install test that broke CI `verify`: it sandboxed
$HOME, which blinds rustup (it resolves the toolchain via ~/.rustup / ~/.cargo)
during `cargo build`. AI_HIST_NO_AUTOSYNC=1 alone is the real launchd-pollution
fix; the HOME override is removed from that path (kept on the binary-mode tests,
which don't run cargo).

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

cursor Bot commented Jul 6, 2026

Copy link
Copy Markdown

Bugbot is not enabled for your account, so this pull request was not reviewed.

Enable Bugbot in the Cursor dashboard to get automatic reviews on future PRs.

@khaliqgant khaliqgant changed the title feat: in-process cloud push SDK + push service + installer test fix feat: cloud push SDK (drives the ai-hist binary) + installer test fix Jul 6, 2026
@agent-relay-code

Copy link
Copy Markdown
Contributor

⚠️ pr-reviewer did not push — the PR branch advanced during the review, so fixes were withheld to avoid overwriting newer commits. Re-trigger the review once the branch settles. The notes below are advisory and were not pushed.

Let me verify one last edge case in describeError-based channel_not_found detection, since the instructions warn about matching-based logic that could turn fail-closed into fail-open. The guard describeError(error).includes('channel_not_found') only adds a retry path; a non-matching error just warns and drops (fail-closed, no publish). That's a safe default — I will not change it.

I have completed the review. No code changes were needed — the PR is mechanically clean, typechecks, builds, and all 65 tests pass against the current checkout.


Review: PR #38 — Agent-scoped channel creation, retry rate-limiting, single-serve lock

Summary

This PR adds three coordinated capabilities to the node package:

  1. A channel ensurer (createChannelEnsurer in serve.ts) that registers a dedicated chief-bridge agent identity via @agent-relay/sdk and creates hosted channels before the bridge's first publish, treating "already exists" as success.
  2. channel_not_found self-healing in BrokerStreamBridge (stream-bridge.ts): queues posts while an ensure is in flight, re-ensures + retries once on a not-found publish, coalesces concurrent failures, and rate-limits re-ensures to one per ENSURE_RETRY_COOLDOWN_MS (30s).
  3. A single-instance lock (acquireInstanceLock) preventing two serves from one workspace dir fighting over the same fleet-node identity, with stale-pid reclamation.

Verification (ran the canonical commands CI uses, from node/package.json)

  • npm ci — installed cleanly (@agent-relay/sdk@9.2.1 resolved).
  • npm run typecheck (tsc --noEmit) — pass, no errors.
  • npm run build (tsc -p tsconfig.build.json) — pass.
  • npm test (vitest run) — pass, 65/65 tests across 3 files.

I also validated the SDK surface the ensurer depends on against the installed @agent-relay/sdk@9.2.1 types (the code casts through as unknown as EnsurerRelay, so this wouldn't be caught by typecheck alone):

  • AgentRelay constructor accepts { workspaceKey, agentToken, baseUrl } (via RelaycastMessagingOptions). ✓
  • agents.registerOrRotate({ name, type }) returns { token }; RelayAgentType includes 'system'. ✓
  • channels.create({ name }) and channels.get(name) exist. ✓

Findings

No blocking issues. The safety-sensitive parts hold up:

  • The channel_not_found retry path is additive — a non-matching publish error still just warns and drops (fail-closed); it never turns a failure into a spurious success.
  • The retry is bounded to one re-ensure per post (retried flag) and one re-ensure per cooldown window per channel, so no infinite loop / no per-post ensure storm.
  • Concurrent not-found failures correctly coalesce onto the single in-flight ensure via pendingPosts, preserving post order.
  • The lock's isProcessAlive treats EPERM as alive (correct — don't steal another user's live lock) and only reclaims on ESRCH/unreadable; release is pid-checked before unlink. Sound.
  • The sensitive workspaceKey is passed to the SDK but never logged.

Addressed comments

  • No bot or human review comments were present in the PR metadata (.workforce/ contains only pr.diff, changed-files.txt, context.json; no comment/review artifacts). Nothing to reconcile.

Advisory Notes

  • Lock cleanup when autostart is off (advisory, no change): acquireInstanceLock runs whenever CHIEF_INSTANCE_LOCK !== '0', but SIGTERM/SIGINT handlers are only registered when a bridge started (if (serveContext.bridge)). On a lock-acquired-but-bridge-off serve, signal-driven dispose() won't fire; cleanup then relies solely on the process.on('exit', release) hook, which does not run for an un-trapped SIGTERM. In practice this leaves a stale lock that the next serve reclaims via the dead-pid path, so it self-corrects — noting only for awareness. This is lifecycle/cleanup territory, so I'm leaving it for a human rather than touching it.

No files were modified — the PR is clean as submitted and passes the full typecheck/build/test suite.

I am not printing READY: I cannot observe the PR's live CI check status or mergeability from this sandbox (no CI workflow files exist in the repo, and check/merge state is a post-harness/GitHub concern), so I cannot assert that every required check has completed and passed.

@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

🧹 Nitpick comments (1)
sdk-ts/src/cloud-push.test.ts (1)

35-68: 📐 Maintainability & Code Quality | 🔵 Trivial | ⚡ Quick win

Missing coverage for malformed JSON output.

Tests cover valid JSON, empty stdout, ENOENT, auth failure, and other non-zero exits, but not the JSON.parse failure path (Lines 123-127 in cloud-push.ts) — e.g. truncated/corrupted stdout on exit 0. Worth a test asserting the rejection message.

🤖 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 `@sdk-ts/src/cloud-push.test.ts` around lines 35 - 68, Add a test in
pushToCloud that covers malformed JSON from the spawn result, since
cloud-push.test.ts currently misses the JSON.parse failure path in pushToCloud.
Use fakeSpawn with exit 0 and truncated/corrupted stdout, then assert
pushToCloud rejects with the parse error message or a clear wrapped failure from
the pushToCloud implementation. Keep the new case alongside the existing
pushToCloud tests so it validates the handling around the JSON parsing branch.
🤖 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 `@sdk-ts/src/cloud-push.ts`:
- Around line 54-63: The explicit binPath override in resolveAiHistBinary is
still treated as a discovery candidate instead of a true override. Update
resolveAiHistBinary so that when explicit is provided it is returned directly
(or fails immediately if invalid) without falling through to AI_HIST_RUST_BIN,
the install path, or PATH-based ai-hist resolution; keep fallback discovery only
for the non-explicit case.

---

Nitpick comments:
In `@sdk-ts/src/cloud-push.test.ts`:
- Around line 35-68: Add a test in pushToCloud that covers malformed JSON from
the spawn result, since cloud-push.test.ts currently misses the JSON.parse
failure path in pushToCloud. Use fakeSpawn with exit 0 and truncated/corrupted
stdout, then assert pushToCloud rejects with the parse error message or a clear
wrapped failure from the pushToCloud implementation. Keep the new case alongside
the existing pushToCloud tests so it validates the handling around the JSON
parsing branch.
🪄 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 Plus

Run ID: 6024641a-f0b4-411b-b304-0dc3555defc7

📥 Commits

Reviewing files that changed from the base of the PR and between 183ba7b and 8c59871.

📒 Files selected for processing (8)
  • README.md
  • crates/ai-hist/src/main.rs
  • sdk-ts/CHANGELOG.md
  • sdk-ts/package.json
  • sdk-ts/src/cloud-client.ts
  • sdk-ts/src/cloud-push.test.ts
  • sdk-ts/src/cloud-push.ts
  • test_install.py
✅ Files skipped from review due to trivial changes (4)
  • sdk-ts/package.json
  • sdk-ts/CHANGELOG.md
  • sdk-ts/src/cloud-client.ts
  • README.md
🚧 Files skipped from review as they are similar to previous changes (2)
  • test_install.py
  • crates/ai-hist/src/main.rs

Comment thread sdk-ts/src/cloud-push.ts

@cubic-dev-ai cubic-dev-ai 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.

2 issues found across 5 files (changes from recent commits).

Prompt for AI agents (unresolved issues)

Check if these issues are valid — if so, understand the root cause of each and fix them. If appropriate, use sub-agents to investigate and fix each issue separately.


<file name="sdk-ts/src/cloud-client.ts">

<violation number="1" location="sdk-ts/src/cloud-client.ts:22">
P2: Circular dependency introduced between `cloud-client.ts` and `cloud-push.ts`. The re-export block (lines 6–22) makes `cloud-client` depend on `cloud-push`, while `cloud-push` already imports `loadStoredRelayhistoryAuth` and `RelayhistoryAuth` from `cloud-client`. This works today because function declarations are hoisted in ESM, but the cycle makes the module graph fragile: adding a top-level constant, expression, or side effect in either module could silently produce `undefined` bindings at module-evaluation time. Consider breaking the cycle by extracting the shared auth types/function into a separate module (e.g. `cloud-auth.ts`) or removing the re-exports and having consumers import from `cloud-push` directly.</violation>
</file>

Tip: Review your code locally with the cubic CLI to iterate faster.

Re-trigger cubic

Comment thread sdk-ts/src/cloud-push.ts
Comment thread sdk-ts/src/cloud-push.ts
sdk-ts:
- resolveAiHistBinary: an explicit binPath is now authoritative (returned
  verbatim), never falling through to $AI_HIST_RUST_BIN / install path / PATH,
  so callers can't get a different binary than they asked for. Fix the JSDoc
  (always returns a string). Add a test for the malformed-JSON reject path.

ai-hist CLI (push --install-service):
- Reject `--incognito` when installing the service: it's a per-run privacy
  filter that would be silently dropped on the scheduled job.
- Linux cron now honors --interval via step schedules (300s -> `*/5 * * * *`)
  instead of always running every minute; the confirmation prints the real
  cadence. Add cron_schedule unit tests.
- Quote the binary path in the cron line so paths with spaces/metacharacters
  work (shell_single_quote + test).
- Unsupported-platform message no longer points to `ai-hist watch` (a local
  sync loop); it's now subcommand-specific.
- README: note the Linux cron granularity caveat for push.

test_install.py: drop the unused fake-tools/PATH setup from the opt-out
regression test (binary mode runs no cargo).

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

cursor Bot commented Jul 6, 2026

Copy link
Copy Markdown

Bugbot is not enabled for your account, so this pull request was not reviewed.

Enable Bugbot in the Cursor dashboard to get automatic reviews on future PRs.

@khaliqgant

Copy link
Copy Markdown
Member Author

Addressed the review feedback (latest commit). Disposition:

Fixed

  • resolveAiHistBinary override (CodeRabbit major, cubic P2): an explicit binPath is now authoritative — returned verbatim, never falling through to $AI_HIST_RUST_BIN/install-path/PATH. JSDoc corrected (always returns a string). (cubic P3)
  • Malformed-JSON path (CodeRabbit): added a test asserting pushToCloud rejects on truncated stdout.
  • push --install-service --incognito (cubic P1, Codex P2): now rejected with a clear message — the privacy filter can't silently vanish onto the scheduled job.
  • Cron honors --interval (cubic P2, Codex P2, Devin, gemini): whole-minute intervals become step schedules (300s → */5 * * * *); the confirmation prints the real cadence; the granularity note only fires for genuinely sub-/non-whole-minute intervals (CodeRabbit). + cron_schedule unit tests.
  • Cron path quoting (cubic P2, gemini): binary path is single-quoted (shell_single_quote + test) so spaces/metacharacters work.
  • Unsupported-platform message (cubic P3, Devin): no longer points to ai-hist watch (a local sync loop); it's subcommand-specific.
  • README (cubic P3, CodeRabbit): added the Linux cron-granularity caveat to the push section.
  • Install-test $HOME (Codex P2, cubic P2): the from-source test no longer overrides HOME (which blinded rustup's ~/.rustup/~/.cargo) — AI_HIST_NO_AUTOSYNC=1 alone is the launchd-pollution fix. This is what unbroke the verify check.
  • Circular dep (cubic P2): resolved by the wrapper rework — cloud-push.ts now imports only node builtins, so cloud-client → cloud-push is one-way.

Skipped (with reason)

  • CodeRabbit nitpick to extract a shared fake-binary/env helper across the two binary-mode install tests: did the concrete part (removed the dead fake_tools setup); left the helper extraction as not worth the churn/risk for two small tests.

Note: much of the earlier round targeted the pre-rework TS port of the push pipeline — that code was replaced by the thin binary wrapper, so those are moot.

@cubic-dev-ai cubic-dev-ai 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.

2 issues found across 5 files (changes from recent commits).

Prompt for AI agents (unresolved issues)

Check if these issues are valid — if so, understand the root cause of each and fix them. If appropriate, use sub-agents to investigate and fix each issue separately.


<file name="sdk-ts/src/cloud-client.ts">

<violation number="1" location="sdk-ts/src/cloud-client.ts:22">
P2: Circular dependency introduced between `cloud-client.ts` and `cloud-push.ts`. The re-export block (lines 6–22) makes `cloud-client` depend on `cloud-push`, while `cloud-push` already imports `loadStoredRelayhistoryAuth` and `RelayhistoryAuth` from `cloud-client`. This works today because function declarations are hoisted in ESM, but the cycle makes the module graph fragile: adding a top-level constant, expression, or side effect in either module could silently produce `undefined` bindings at module-evaluation time. Consider breaking the cycle by extracting the shared auth types/function into a separate module (e.g. `cloud-auth.ts`) or removing the re-exports and having consumers import from `cloud-push` directly.</violation>
</file>

Tip: Review your code locally with the cubic CLI to iterate faster.

Re-trigger cubic

Comment thread crates/ai-hist/src/main.rs Outdated
Comment thread sdk-ts/src/cloud-push.ts
@agent-relay-code

Copy link
Copy Markdown
Contributor

ℹ️ pr-reviewer: review only — no file changes were applied to the PR (nothing to commit after review). The notes below are advisory and were not pushed.

Review: PR #38feat/cloud-push-autosync

Summary

The PR generalizes the existing sync background-service plumbing (launchd/cron) into a shared ServiceSpec so a new push --install-service/--uninstall-service/--interval service can reuse it, adds a cron step-schedule mapper, adds a thin TS pushToCloud wrapper that drives the real ai-hist push --json binary, bumps the SDK to 0.4.0, and adds an AI_HIST_NO_AUTOSYNC regression guard to the installer tests. I traced every changed symbol across callers, types, tests, config, and the installer, and ran the full CI pipeline.

Verification (ran the way CI does)

  • cargo test --workspace30 passed in ai-hist-cli (incl. new cron_schedule_maps_intervals_to_step_expressions, shell_single_quote_survives_spaces_and_quotes), 27 passed in ai-hist-core. EXIT 0.
  • cargo build -q -p ai-hist-cli → EXIT 0. Toolchain rustc 1.96.1, so u64::is_multiple_of (stable since 1.87) is fine under CI's dtolnay/rust-toolchain@stable; edition-2021 workspace sets no MSRV floor.
  • Python (test_ai_hist.py test_cli_dispatch.py test_install.py) → 190 passed, incl. the new test_install_script_autosync_opt_out_leaves_launchd_untouched. (install.sh does support AI_HIST_NO_AUTOSYNC and prints "skipping auto-sync" — assertions match.)
  • npm ci && npm test (sdk-ts) → the 6 new pushToCloud tests pass; build/typecheck clean under strict: true.

One TS test fails — pre-existing and unrelated to this PR

SDK fallback ingests OpenCode rows committed in WAL files (sdk-ts/src/mcp-smoke.test.ts:180) fails consistently. It exercises node:sqlite reading another process's uncheckpointed WAL (wal_autocheckpoint=0) written by a python3 subprocess — an environment/runtime-dependent path. The PR does not touch index.ts, mcp-server.ts, or trajectory-sources.ts (the OpenCode/WAL read path); it only adds cloud-push.* and a one-line re-export in cloud-client.ts. The equivalent Rust test (opencode_sync_reads_committed_wal_rows) passes. This is a sandbox/environment artifact, not a regression from this PR, so I left it untouched (fixing it would be out of scope and would require touching code the PR doesn't own).

Findings

No blocking issues. The change is well-scoped and internally consistent:

  • No dangling references to the renamed symbols (install_sync_service, uninstall_sync_service, SYNC_SERVICE_LABEL, CRON_MARKER); both Sync and Push command arms call the new install_managed_service/uninstall_managed_service correctly.
  • The Rust push --json output shape (sent/accepted/batchId/cursor, main.rs:777) exactly matches the TS PushReport parser, and the fail-soft paths (ENOENT → null, "not authenticated" → null) are intentional and correct — no fail-closed→fail-open change.
  • Binary discovery ~/.local/share/ai-hist/ai-hist-rust-bin matches where install.sh actually places it ($PREFIX/share/ai-hist, PREFIX=$HOME/.local).
  • cron/launchd inputs are constants or properly escaped (xml_escape for plist, shell_single_quote for crontab); no injection surface.

I made no code edits — nothing needed a mechanical fix, and every behavior detail checked out, so there was nothing safe to auto-apply.

Advisory Notes

  • install_cron_service (main.rs:1832): the branch else if interval < 60 && interval != 60 has a redundant && interval != 60 (already implied by interval < 60). Purely cosmetic dead condition; no behavior impact. Left as-is (not worth a churn edit).

Addressed comments

  • No bot or human review comments were present in .workforce/context.json (no threads/reviews included in the provided metadata), so there were none to reconcile against the current checkout.

The one failing CI-visible check (the OpenCode WAL TS test) is a pre-existing environmental failure independent of this PR, but because it is currently red I cannot represent CI as fully green.

Two real bugs the re-review caught in the prior fix:

- cron_schedule over-ran for large intervals: a daily (86400s) or 90-minute
  interval fell through to the hourly fallback (up to 24x too frequent). It now
  rounds toward a *coarser* cadence (90 min -> every 2h) and caps at a daily run,
  never firing more often than requested. It also returns the effective period
  so install_cron_service only prints the granularity note on an actual
  mismatch (not on the plain 300s default). Tests cover 90 min / 1 day / 25h.

- pushToCloud only treated ENOENT as "binary unavailable", so an explicit
  binPath that exists but isn't executable (EACCES/EPERM) or is a directory
  (ENOTDIR/EISDIR) rejected instead of resolving null, breaking the background
  no-op contract. Now all of these no-op. Added an EACCES test and aligned the
  JSDoc.

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

cursor Bot commented Jul 7, 2026

Copy link
Copy Markdown

Bugbot is not enabled for your account, so this pull request was not reviewed.

Enable Bugbot in the Cursor dashboard to get automatic reviews on future PRs.

@cubic-dev-ai cubic-dev-ai 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.

1 issue found across 3 files (changes from recent commits).

Prompt for AI agents (unresolved issues)

Check if these issues are valid — if so, understand the root cause of each and fix them. If appropriate, use sub-agents to investigate and fix each issue separately.


<file name="sdk-ts/src/cloud-client.ts">

<violation number="1" location="sdk-ts/src/cloud-client.ts:22">
P2: Circular dependency introduced between `cloud-client.ts` and `cloud-push.ts`. The re-export block (lines 6–22) makes `cloud-client` depend on `cloud-push`, while `cloud-push` already imports `loadStoredRelayhistoryAuth` and `RelayhistoryAuth` from `cloud-client`. This works today because function declarations are hoisted in ESM, but the cycle makes the module graph fragile: adding a top-level constant, expression, or side effect in either module could silently produce `undefined` bindings at module-evaluation time. Consider breaking the cycle by extracting the shared auth types/function into a separate module (e.g. `cloud-auth.ts`) or removing the re-exports and having consumers import from `cloud-push` directly.</violation>
</file>

Tip: Review your code locally with the cubic CLI to iterate faster.

Re-trigger cubic

Comment thread crates/ai-hist/src/main.rs
`*/N` in a cron field is only uniform when N divides the field's range — a
non-divisor step fires a short interval at the rollover (`*/45` → :00, :45,
:00 = a 15-minute gap, i.e. faster than intended). cron_schedule now rounds the
minute step up to the next divisor of 60 (7 min → */10, 45 min → hourly) and the
hour step up to the next divisor of 24 (7h → */8), keeping every schedule
uniform and never faster than requested. Tests cover the non-divisor cases.

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

cursor Bot commented Jul 7, 2026

Copy link
Copy Markdown

Bugbot is not enabled for your account, so this pull request was not reviewed.

Enable Bugbot in the Cursor dashboard to get automatic reviews on future PRs.

@khaliqgant
khaliqgant merged commit 726d778 into main Jul 7, 2026
3 checks passed
khaliqgant added a commit to AgentWorkforce/relay that referenced this pull request Jul 8, 2026
…SDK (#1233)

* feat(cli): reflex on/off schedules automatic cloud sync

`agent-relay reflex on` previously only flipped a flag and did a one-time
cloud login — nothing ever pushed history to relayhistory-cloud, so "Reflex is
on" never actually synced anything.

Now `reflex on` also schedules the ai-hist background services (local `sync`
+ cloud `push`, via `ai-hist <cmd> --install-service`), `reflex off` removes
the push service (leaving local capture in place), and `reflex status` reports
whether cloud push is scheduled. Scheduling is best-effort: if ai-hist isn't
on PATH the flag still flips and a clear hint is printed, matching the existing
cloud-login resilience.

Wiring goes through injectable ReflexDependencies (installCloudSync /
uninstallCloudSync / cloudSyncInstalled) so it stays unit-testable without
shelling out. Requires ai-hist with `push --install-service`
(AgentWorkforce/relayhistory#38).

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

* style: auto-format with Prettier

* feat(cli): reflex syncs to cloud in-process via ai-hist SDK (no CLI shell-out)

Replaces the earlier shell-out approach. `agent-relay reflex on` no longer
spawns `ai-hist ... --install-service`; instead the long-running `agent-relay
up` host pushes new local session history to relayhistory-cloud in-process,
gated on the reflex flag.

- `@agent-relay/config`: `reflex-config.ts` is the single source of truth for
  the `~/.agentworkforce/reflex.json` shape/location — `isReflexEnabled()`,
  `readReflexState()`, `writeReflexState()`. `reflex.ts` now uses it and drops
  its private copy + the CLI shell-out deps; `reflex on` = flip flag + cloud
  login only.
- `reflex-capture.ts`: an unref'd periodic push loop (mirrors the telemetry
  client) started after the fleet sidecar in `broker-lifecycle.ts` and stopped
  (with a final flush) in `shutdownOnce`. It calls `ai-hist/cloud`'s
  `pushToCloud` via a lazy, non-analyzable dynamic import so the CLI does not
  statically depend on it — a silent no-op if ai-hist is unavailable or the
  user isn't authed.
- Adds `ai-hist@^0.4.0` (the SDK gains `pushToCloud` in that release) and marks
  `ai-hist`/`sql.js` external in the esbuild library bundle.

Auth is self-consistent: reflex login and the SDK push both use
~/.config/ai-hist/auth.json.

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

* fix(cli): address reflex PR review + unbreak CI

- Drop the `ai-hist@^0.4.0` dependency: that version isn't published yet
  (it ships in relayhistory#38), so `npm ci` failed with ETARGET across every
  CI job. The capture loop already loads `ai-hist/cloud` via a lazy dynamic
  import and no-ops gracefully when absent, so it's a runtime-optional peer;
  declare it as a real dependency once 0.4.0 is published. Reverts the now-moot
  esbuild external entry too.
- reflex-capture: re-check `isEnabled()` on every tick so `reflex off` (or `on`)
  takes effect immediately in a running `agent-relay up`, and start the interval
  only after the initial delay so the first push can't fire before
  initialDelayMs when intervalMs is smaller. Adds tests for both.
- reflex on: only print "History syncs automatically…" when cloud login
  actually succeeded, so it no longer contradicts the not-logged-in / login-
  failed warnings. Adds negative assertions.

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

* chore: apply pr-reviewer fixes for #1233

* fix(cli): reflex writes Rust-binary auth; capture drives the binary

Follows the SDK pivot to a binary-backed push (relayhistory sdk-ts). Since the
in-process push now spawns `ai-hist push`, reflex login must persist the
rth_at_ session where that binary reads it: $RELAYHISTORY_HOME/auth.json
(default ~/.agentworkforce/relayhistory/auth.json), in the Rust snake_case
shape (base_url/access_token/refresh_token) — not the old camelCase
~/.config/ai-hist path.

The capture loop no longer pre-checks auth via the TS SDK; it just calls
pushToCloud, which drives the binary and returns null when the binary is
missing or the user isn't logged in.

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

* feat(cli): reflex capture syncs+pushes via the bundled ai-hist binary

Toward `agent-relay reflex on` "just working" with no extra user commands:

- Add `ai-hist-path.ts`: resolves the ai-hist binary like the broker resolves
  its own — `$AI_HIST_RUST_BIN` -> the per-platform optional-dep package
  (`ai-hist-bin-<platform>-<arch>`) -> the install.sh location -> `ai-hist` on
  PATH. Once the binary ships as an optional dependency, a plain agent-relay
  install has it with zero setup.
- Rework the capture loop to drive that binary directly: it now runs
  `ai-hist sync` (populate the local DB from the user's agent history) then
  `ai-hist push --json` each tick — previously it only pushed, so a fresh
  machine had nothing to upload. Dropped the lazy `ai-hist/cloud` npm import, so
  relay has no dependency to publish/resolve; unavailable binary or missing
  auth is a silent no-op.

Tests cover the resolver (override + package-name mapping) and sync→push
(happy path, binary-unavailable skip, not-authenticated, hard failure).

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

* style: auto-format with Prettier

* feat(cli): reflex capture runs in-process via napi (no subprocess)

Replaces the spawn-based capture with an in-process call to the `ai-hist-native`
napi addon's `syncAndPush()` — no `ai-hist` subprocess at all, per the
requirement not to shell out to the CLI. The addon is lazy-loaded via a
non-analyzable dynamic import (so it stays out of the esbuild bundle and
resolves from its per-platform optional-dependency package), and is a silent
no-op when unavailable or the user isn't authenticated.

Removes ai-hist-path.ts (the binary resolver) and the spawn plumbing. Tests now
inject the native addon instead of a fake child process.

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

* feat(cli): depend on ai-hist-native for in-process reflex capture

Now that ai-hist-native@0.4.1 is published, declare it as an optional
dependency so a plain `agent-relay` install pulls the addon (and npm
auto-selects the matching per-platform binary via os/cpu). The reflex capture
loop loads it and calls syncAndPush() in-process; it stays a graceful no-op if
the addon isn't available for a platform. Verified: npm install resolves
ai-hist-native + ai-hist-native-darwin-arm64 and `require('ai-hist-native')`
exposes syncAndPush.

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

* fix(cli): surface broken ai-hist-native addon instead of silently no-op'ing

reflex-capture's loadNative() caught every dynamic-import error and returned
null, so an installed-but-broken addon (ABI mismatch, missing system lib, init
failure) looked identical to "not installed" and the capture loop silently did
nothing. Now only ERR_MODULE_NOT_FOUND / MODULE_NOT_FOUND is treated as a
no-op; any other error is rethrown so the loop logs it
(`[reflex] cloud sync failed: …`).

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

---------

Co-authored-by: Proactive Runtime Bot <agent@agent-relay.com>
Co-authored-by: Claude Opus 4.8 <noreply@anthropic.com>
Co-authored-by: github-actions[bot] <github-actions[bot]@users.noreply.github.com>
Co-authored-by: agent-relay-code[bot] <agent-relay-code[bot]@users.noreply.github.com>
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