← Back to Plugins
Tools

Datarobot Sync

datarobot-labs By datarobot-labs ⭐ 2 stars 👁 42 views ▲ 0 votes

rsync for DataRobot Files. Persist AI agent state (skills, memories, sessions) across restarts for always-on agent harnesses like Hermes and OpenClaw. Incremental, single Go binary and dr plugin

Configuration Example

endpoint: https://app.datarobot.com/api/v2
catalog-id: 68a1f0c9d2e3b4a5c6d7e8f9

README

<p align="center">
  <a href="https://github.com/datarobot-labs/datarobot-sync">
    <img src="https://af.datarobot.com/img/datarobot_logo.avif" width="600px" alt="DataRobot Logo"/>
  </a>
</p>
<h2 align="center">datarobot-sync: DataRobot rsync</h2>
<p align="center">
  <a href="https://datarobot-labs.github.io/datarobot-sync/">
    <img src="https://img.shields.io/badge/documentation-a?label=Docs&labelColor=30373D&color=5B8FF9" alt="Documentation">
  </a>
</p>

rsync for the DataRobot Files API. It makes a local directory and a DataRobot Files container
match, transferring only the difference, xz-compressed.

It runs as a plugin of the [DataRobot CLI](https://cli.datarobot.com), and as a single static Go
binary for containers and services. Full documentation:
[Docs Site](https://datarobot-labs.github.io/datarobot-sync/).

```sh
datarobot sync init                          # create a container, print its id
datarobot sync upload   ./my-project         # push, sending only what changed
datarobot sync download ./my-project         # pull, fetching only what is missing
```

## Requirements

Works against DataRobot 11.4 and later. The Files API's `allFiles` listing endpoint gained a
`recursive` parameter in 11.7; against an older instance datarobot-sync detects the resulting 400 and
falls back to the equivalent request automatically, no configuration needed.

## Give an always-on agent a memory

A restart wipes a container's disk, and with it the skills, memories, and sessions an agent harness
such as [Hermes](https://github.com/datarobot-labs/datarobot-labs-hermes) or OpenClaw built up.
`dr-sync download` on boot restores them, `watch` or a timed `upload` keeps them current, and one
final `upload` on shutdown captures the last changes. Retained generations make a corrupted memory
recoverable instead of lost. See the
[agent harnesses guide](https://datarobot-labs.github.io/datarobot-sync/docs/agent-harnesses/).

## Why it is fast

- **Nothing unchanged is re-sent.** The Files API records a SHA-256 for every stored object, so
  the diff is exact rather than heuristic.
- **A rename or a revert transfers nothing.** Content is stored under its own digest, so moving
  a 5 GB file is a manifest edit.
- **No-op runs do not re-read the tree.** The remote manifest records size, mode and mtime per
  path, which is the same baseline rsync gets from its destination.
- **Files are batched, not sent one by one.** Each upload request costs about 0.2 s even in
  parallel, so a thousand small files go up as a handful of compressed packs.
- **A pull reads only the packs it needs**, and stops reading each one as soon as it has the
  last member it wanted.

## Install

As a `dr` plugin, from a release or a local archive:

```sh
dr plugin install sync --url https://github.com/datarobot-labs/datarobot-sync/releases/latest/download/sync-<version>.tar.xz
dr plugin install sync --file dist/sync-<version>.tar.xz    # from `task plugin:package`
datarobot sync --help
```

That installs under `$XDG_CONFIG_HOME/datarobot/plugins/sync/`. For development, a
`dr-`-prefixed binary anywhere on `PATH` is discovered just as well — which is what
`task install:local` does.

As a standalone binary — download the archive for your platform from
[releases](https://github.com/datarobot-labs/datarobot-sync/releases), or build it:

```sh
task install:local     # builds into ~/.local/bin/dr-sync
```

Building from source needs Go as pinned in `.tool-versions`; with asdf, run `asdf reshim` after
first checkout so the `go` shim exists.

As a container. The mount target is `/tmp/data`, not `/data`: a DataRobot execution
environment's container only has `/tmp` writable, and mounting there means this works
unmodified whether it runs on your own infra or inside one of those.

```sh
docker run --rm -v "$PWD:/tmp/data" \
  -e DATAROBOT_ENDPOINT -e DATAROBOT_API_TOKEN -e DR_SYNC_CATALOG_ID \
  ghcr.io/datarobot-labs/dr-sync:latest upload /tmp/data
```

See [`docs/quickstart.md`](docs/quickstart.md) for a full walkthrough, including running
`watch` as a resident container/systemd/launchd service and `upload` on a cron schedule.

## Configuration

| What | Flag | Environment |
|---|---|---|
| Container to sync | `--catalog-id` | `DR_SYNC_CATALOG_ID` |
| API endpoint | `--endpoint` | `DATAROBOT_ENDPOINT` |
| API token | *(none, deliberately)* | `DATAROBOT_API_TOKEN` |
| Config file | `--config` | `DATAROBOT_CONFIG` |

Precedence is flag, then environment, then config file. Under the `dr` CLI the endpoint and
token arrive already validated; standalone, datarobot-sync falls back to
`~/.config/datarobot/drconfig.yaml` — the same file `dr auth login` writes. Because the flag
names double as config keys, that file can also carry settings for datarobot-sync:

```yaml
endpoint: https://app.datarobot.com/api/v2
catalog-id: 68a1f0c9d2e3b4a5c6d7e8f9
```

There is no `--token` flag on purpose: a flag would put the token in the process table and the
shell history.

## Usage

```sh
# First time
datarobot sync init --name "my project"
export DR_SYNC_CATALOG_ID=<the id it printed>

# Push and pull
datarobot sync upload   ./src
datarobot sync download ./restored

# See what a push would do, change nothing
datarobot sync status ./src

# Make the remote mirror the local tree exactly, dropping paths that are gone
datarobot sync upload ./src --delete

# Make the local tree mirror the remote exactly
datarobot sync download ./src --delete

# Only part of the tree
datarobot sync download ./src --prefix docs/ --prefix src/api/

# Roll back to a retained older push
datarobot sync ls --generation 41
datarobot sync download ./src --generation 41

# What is in there, and what is reclaimable
datarobot sync inspect --packs
datarobot sync compact ./src     # rewrite sparse packs, run from a complete checkout
datarobot sync gc                # delete unreferenced objects, moves no data
```

Shell completions come from cobra:

```sh
dr-sync completion bash > /usr/local/etc/bash_completion.d/dr-sync
dr-sync completion zsh  > "${fpath[1]}/_dr-sync"
```

Exclusions use gitignore syntax, from `--exclude`, `--exclude-from`, or a `.drsyncignore` in
the sync root:

```
node_modules/
*.log
!important.log
```

## Watch

`datarobot sync watch <path>` is a resident process that keeps pushing a directory as it changes,
instead of running `upload` in a loop yourself:

```sh
datarobot sync watch ./src
```

It shares every `upload` flag, but `--delete` defaults **on** — watch mirrors the local tree
rather than only adding to it — and `--timeout` bounds one push, not the whole process. A
filesystem event only ever triggers a rescan; it is never trusted on its own, so a missed
event costs latency, not correctness.

Flags worth knowing:

| Flag | Default | |
|---|---|---|
| `--debounce` | `2s` | wait for the tree to go quiet before pushing |
| `--min-interval` | `15s` | floor between the start of two pushes |
| `--poll-interval` | `60s` | rescan on a timer regardless of events (`0` disables it) |
| `--poll-only` | off | skip the OS watcher entirely — for network filesystems |
| `--gc-interval` | `10m` | how often unreferenced objects are swept (`0` disables it) |
| `--writer-ttl` | `5m` | how long this process advertises itself as active |

`watch` uses your OS's native filesystem notifications (inotify on Linux, kqueue on
macOS/BSD, ReadDirectoryChangesW on Windows), one watch per directory. Each one costs a
watch descriptor: a large tree, or several `watch` processes at once, can hit a
platform limit.

- **Linux (inotify):** raise `fs.inotify.max_user_watches` (and `max_user_instances` if
  running several watchers), e.g. `sudo sysctl fs.inotify.max_user_watches=524288`.
- **macOS/BSD (kqueue):** kqueue needs one open file descriptor per watched directory, so
  raise the process's open-file limit, e.g. `ulimit -n 65536`.

If a limit is hit mid-run, `watch` warns and falls back to polling for the rest of that
directory tree rather than exiting — pass `--poll-only` to skip straight to that and avoid
the warning.

## What the container looks like

datarobot-sync does not store your files at their own paths. Everything lives under a reserved
prefix as immutable objects:

```
.dr-sync/manifests/0000000042-7f3a9c2b.json.xz   the file tree, one per push
.dr-sync/packs/p_9f2a1c8d3e4f5061.tar.xz         contents, named after their own digest
.dr-sync/locks/<client>-<n>.lock                 advisory claims held during a push
.dr-sync/writers/<client>-<n>.json               resident `watch` processes announcing themselves
```

That is a deliberate trade. DataRobot's intake cannot unpack xz, so genuine LZMA2 compression
means datarobot-sync owns the layout on both sides. The container is not browsable as files; in
exchange the transfer is compressed, deduplicated, and resumable. `datarobot sync ls` shows the tree
without downloading anything.

Nothing in the container is ever overwritten, and the current manifest is the
highest-numbered one — which is what makes an interrupted push invisible rather than
corrupting.

## Concurrency

Two clients pushing to one container is guarded, not prevented: the API has no
compare-and-swap. datarobot-sync takes an advisory lease, revalidates immediately before committing,
and reports a fork afterwards if one happened. A genuine simultaneous push means one side's
update is not in the head — detected, never silently corrupted, and healed by re-running.
`--force` overrides the lease when the holder is known to be gone.

A resident `watch` process additionally announces itself in `.dr-sync/writers/`, so a peer
can tell it is active. If the container moves while `watch` is running, it holds back
`--delete` for that cycle and warns instead of racing another writer's paths; `--force`
overrides that too.

## Exit codes

| Code | Meaning |
|---|---|
| 0 | success, including "nothing to do" |
| 1 | unexpected failure |
| 2 | usage, config, or precondition error |
| 3 | API or authentication failure |
| 4

... (truncated)
tools

Comments

Sign in to leave a comment

Loading comments...